diff options
Diffstat (limited to '')
| -rw-r--r-- | src/cinput.cpp | 142 | ||||
| -rw-r--r-- | src/hl2h.cpp | 307 | ||||
| -rw-r--r-- | src/hl2h.h | 133 | ||||
| -rw-r--r-- | src/menu.cpp | 555 | ||||
| -rw-r--r-- | src/modules.cpp | 69 | ||||
| -rw-r--r-- | src/netvars.cpp | 185 |
6 files changed, 1391 insertions, 0 deletions
diff --git a/src/cinput.cpp b/src/cinput.cpp new file mode 100644 index 0000000..7c40b3b --- /dev/null +++ b/src/cinput.cpp | |||
| @@ -0,0 +1,142 @@ | |||
| 1 | // cinput.cpp — CInput discovery, usercmd CRC, get/fix helpers | ||
| 2 | #include "hl2h.h" | ||
| 3 | |||
| 4 | // --------------------------------------------------------------------------- | ||
| 5 | // CRC-32 (standard IEEE reflected, matches Valve's implementation) | ||
| 6 | // --------------------------------------------------------------------------- | ||
| 7 | |||
| 8 | static uint32_t g_crc_table[256]; | ||
| 9 | static bool g_crc_inited = false; | ||
| 10 | |||
| 11 | static void crc32_init_table() { | ||
| 12 | if (g_crc_inited) | ||
| 13 | return; | ||
| 14 | for (uint32_t i = 0; i < 256; i++) { | ||
| 15 | uint32_t c = i; | ||
| 16 | for (int k = 0; k < 8; k++) | ||
| 17 | c = (c & 1) ? 0xEDB88320u ^ (c >> 1) : c >> 1; | ||
| 18 | g_crc_table[i] = c; | ||
| 19 | } | ||
| 20 | g_crc_inited = true; | ||
| 21 | } | ||
| 22 | |||
| 23 | // field set verified from client.so GetChecksum disassembly | ||
| 24 | uint32_t usercmd_checksum(const CUserCmd *c) { | ||
| 25 | crc32_init_table(); | ||
| 26 | uint32_t crc = 0xFFFFFFFFu; | ||
| 27 | auto upd = [&crc](const void *p, size_t n) { | ||
| 28 | const uint8_t *b = (const uint8_t *)p; | ||
| 29 | while (n--) | ||
| 30 | crc = g_crc_table[(crc ^ *b++) & 0xFF] ^ (crc >> 8); | ||
| 31 | }; | ||
| 32 | upd(&c->command_number, 4); | ||
| 33 | upd(&c->tick_count, 4); | ||
| 34 | upd(&c->viewangles, 12); | ||
| 35 | upd(&c->forwardmove, 4); | ||
| 36 | upd(&c->sidemove, 4); | ||
| 37 | upd(&c->upmove, 4); | ||
| 38 | upd(&c->buttons, 4); | ||
| 39 | upd(&c->impulse, 1); | ||
| 40 | upd(&c->weaponselect, 4); | ||
| 41 | upd(&c->weaponsubtype, 4); | ||
| 42 | upd(&c->random_seed, 4); | ||
| 43 | upd(&c->mousedx, 2); | ||
| 44 | upd(&c->mousedy, 2); | ||
| 45 | return ~crc; | ||
| 46 | } | ||
| 47 | |||
| 48 | // CInput: +0xE4 = m_pCommands, +0xE8 = m_pVerifiedCommands | ||
| 49 | static constexpr size_t CMDOFF_M_PCOMMANDS = 0xE4; | ||
| 50 | static constexpr size_t CMDOFF_M_PVERIFIED = 0xE8; | ||
| 51 | static constexpr size_t VERIFIED_STRIDE = 0x58; | ||
| 52 | static constexpr size_t VERIFIED_CRC_OFF = 0x54; | ||
| 53 | |||
| 54 | CUserCmd *get_usercmd(int seq) { | ||
| 55 | if (!g_input) | ||
| 56 | return nullptr; | ||
| 57 | uintptr_t cmds = *(uintptr_t *)(g_input + CMDOFF_M_PCOMMANDS); | ||
| 58 | if (!cmds) | ||
| 59 | return nullptr; | ||
| 60 | auto *cmd = (CUserCmd *)(cmds + (size_t)(seq % 90) * 0x54); | ||
| 61 | if (cmd->command_number != seq) | ||
| 62 | return nullptr; | ||
| 63 | return cmd; | ||
| 64 | } | ||
| 65 | |||
| 66 | void fix_verified_crc(int seq, uint32_t crc) { | ||
| 67 | if (!g_input) | ||
| 68 | return; | ||
| 69 | uintptr_t verified = *(uintptr_t *)(g_input + CMDOFF_M_PVERIFIED); | ||
| 70 | if (verified) | ||
| 71 | *(uint32_t *)(verified + (size_t)(seq % 90) * VERIFIED_STRIDE + | ||
| 72 | VERIFIED_CRC_OFF) = crc; | ||
| 73 | } | ||
| 74 | |||
| 75 | // --------------------------------------------------------------------------- | ||
| 76 | // CInput discovery: byte-scan of CHLClient::CreateMove prologue | ||
| 77 | // --------------------------------------------------------------------------- | ||
| 78 | |||
| 79 | bool find_input_global() { | ||
| 80 | void **vt = VTABLE(g_iface.client); | ||
| 81 | uintptr_t cm = (uintptr_t)vt[21]; // CreateMove slot | ||
| 82 | if (!g_client.contains(cm)) { | ||
| 83 | logf("CreateMove %p outside client module", (void *)cm); | ||
| 84 | return false; | ||
| 85 | } | ||
| 86 | const uint8_t *b = (const uint8_t *)cm; | ||
| 87 | |||
| 88 | // 1) PIC anchor: call __x86.get_pc_thunk + add $imm,%esi | ||
| 89 | uintptr_t anchor = 0; | ||
| 90 | for (int i = 0; i <= 0x100 - 11; i++) { | ||
| 91 | if (b[i] == 0xE8 && b[i + 5] == 0x81 && b[i + 6] == 0xC6) { | ||
| 92 | uint32_t imm; | ||
| 93 | memcpy(&imm, b + i + 7, 4); | ||
| 94 | anchor = cm + (uintptr_t)(i + 5) + imm; | ||
| 95 | break; | ||
| 96 | } | ||
| 97 | } | ||
| 98 | if (!anchor || !g_client.contains(anchor)) { | ||
| 99 | logf("PIC anchor not found in CreateMove"); | ||
| 100 | return false; | ||
| 101 | } | ||
| 102 | |||
| 103 | // 2) the input->CreateMove call: "ff 51 0c" preceded by "52", scan back | ||
| 104 | // for "8d 86 imm32" with "8b 10" between | ||
| 105 | for (int c = 3; c <= 0x100 - 3; c++) { | ||
| 106 | if (!(b[c] == 0xFF && b[c + 1] == 0x51 && b[c + 2] == 0x0C && | ||
| 107 | b[c - 1] == 0x52)) | ||
| 108 | continue; | ||
| 109 | for (int i = c - 8; i > c - 44 && i > 2; i--) { | ||
| 110 | if (b[i] != 0x8D || b[i + 1] != 0x86) | ||
| 111 | continue; | ||
| 112 | bool has_mov = false; | ||
| 113 | for (int j = i + 6; j < c - 1; j++) | ||
| 114 | if (b[j] == 0x8B && b[j + 1] == 0x10) { | ||
| 115 | has_mov = true; | ||
| 116 | break; | ||
| 117 | } | ||
| 118 | if (!has_mov) | ||
| 119 | continue; | ||
| 120 | uint32_t disp; | ||
| 121 | memcpy(&disp, b + i + 2, 4); | ||
| 122 | uintptr_t var = anchor + disp; | ||
| 123 | if (!g_client.contains(var)) | ||
| 124 | continue; | ||
| 125 | uintptr_t input = *(uintptr_t *)var; | ||
| 126 | if (!input || !g_client.contains(input)) | ||
| 127 | continue; | ||
| 128 | uintptr_t vtbl = *(uintptr_t *)input; | ||
| 129 | if (!vtbl || !g_client.contains(vtbl)) | ||
| 130 | continue; | ||
| 131 | uintptr_t f3 = *(uintptr_t *)(vtbl + 3 * sizeof(void *)); | ||
| 132 | if (!g_client.contains(f3)) | ||
| 133 | continue; | ||
| 134 | logf("CInput* = %#lx (ptr var %#lx, vfunc[3] %#lx)", (unsigned long)input, | ||
| 135 | (unsigned long)var, (unsigned long)f3); | ||
| 136 | g_input = input; | ||
| 137 | return true; | ||
| 138 | } | ||
| 139 | } | ||
| 140 | logf("CInput pointer variable not found in CreateMove scan"); | ||
| 141 | return false; | ||
| 142 | } | ||
diff --git a/src/hl2h.cpp b/src/hl2h.cpp new file mode 100644 index 0000000..0261ff2 --- /dev/null +++ b/src/hl2h.cpp | |||
| @@ -0,0 +1,307 @@ | |||
| 1 | // hl2h.cpp — core: logging, vector math, globals, feature logic, setup | ||
| 2 | #include "hl2h.h" | ||
| 3 | #include <cmath> | ||
| 4 | #include <cstdarg> | ||
| 5 | |||
| 6 | // --------------------------------------------------------------------------- | ||
| 7 | // logging | ||
| 8 | // --------------------------------------------------------------------------- | ||
| 9 | |||
| 10 | void logf(const char *fmt, ...) { | ||
| 11 | va_list ap, ap2; | ||
| 12 | va_start(ap, fmt); | ||
| 13 | va_copy(ap2, ap); | ||
| 14 | fprintf(stderr, "hl2hake: "); | ||
| 15 | vfprintf(stderr, fmt, ap2); | ||
| 16 | fprintf(stderr, "\n"); | ||
| 17 | fflush(stderr); | ||
| 18 | va_end(ap2); | ||
| 19 | if (FILE *f = fopen("/tmp/hl2hake.log", "a")) { | ||
| 20 | fprintf(f, "hl2hake: "); | ||
| 21 | vfprintf(f, fmt, ap); | ||
| 22 | fprintf(f, "\n"); | ||
| 23 | fclose(f); | ||
| 24 | } | ||
| 25 | va_end(ap); | ||
| 26 | } | ||
| 27 | |||
| 28 | // --------------------------------------------------------------------------- | ||
| 29 | // globals | ||
| 30 | // --------------------------------------------------------------------------- | ||
| 31 | |||
| 32 | Module g_client, g_engine; | ||
| 33 | GameInterfaces g_iface; | ||
| 34 | NetVars g_net; | ||
| 35 | uintptr_t g_input = 0; | ||
| 36 | |||
| 37 | // --------------------------------------------------------------------------- | ||
| 38 | // vector math | ||
| 39 | // --------------------------------------------------------------------------- | ||
| 40 | |||
| 41 | static constexpr float PI_F = 3.14159265358979323846f; | ||
| 42 | |||
| 43 | QAngle vector_angles(const Vector &forward) { | ||
| 44 | QAngle a{}; | ||
| 45 | a.z = 0; | ||
| 46 | if (forward.y == 0 && forward.x == 0) { | ||
| 47 | a.y = 0; | ||
| 48 | a.x = (forward.z > 0) ? -90.f : 90.f; | ||
| 49 | } else { | ||
| 50 | a.y = atan2f(forward.y, forward.x) * 180.f / PI_F; | ||
| 51 | if (a.y < 0) | ||
| 52 | a.y += 360.f; | ||
| 53 | a.x = atan2f(-forward.z, | ||
| 54 | sqrtf(forward.x * forward.x + forward.y * forward.y)) * | ||
| 55 | 180.f / PI_F; | ||
| 56 | if (a.x < 0) | ||
| 57 | a.x += 360.f; | ||
| 58 | } | ||
| 59 | return a; | ||
| 60 | } | ||
| 61 | |||
| 62 | Vector angle_forward_vector(const QAngle &angles) { | ||
| 63 | auto DEG2RAD = [](float x) { return x * (PI_F / 180.f); }; | ||
| 64 | float sp = sinf(DEG2RAD(angles.x)), cp = cosf(DEG2RAD(angles.x)); | ||
| 65 | float sy = sinf(DEG2RAD(angles.y)), cy = cosf(DEG2RAD(angles.y)); | ||
| 66 | return {cp * cy, cp * sy, -sp}; | ||
| 67 | } | ||
| 68 | |||
| 69 | float getfov(const QAngle &my_view, const QAngle &aim_ang) { | ||
| 70 | Vector aim = angle_forward_vector(my_view); | ||
| 71 | Vector ang = angle_forward_vector(aim_ang); | ||
| 72 | float len = sqrtf(aim.x * aim.x + aim.y * aim.y + aim.z * aim.z); | ||
| 73 | if (len <= 0) | ||
| 74 | return 0; | ||
| 75 | float dot = (aim.x * ang.x + aim.y * ang.y + aim.z * ang.z) / len; | ||
| 76 | if (dot > 1) | ||
| 77 | dot = 1; | ||
| 78 | if (dot < -1) | ||
| 79 | dot = -1; | ||
| 80 | return acosf(dot) * (180.f / PI_F); | ||
| 81 | } | ||
| 82 | |||
| 83 | // --------------------------------------------------------------------------- | ||
| 84 | // feature logic (bhop, ABH, climb) | ||
| 85 | // --------------------------------------------------------------------------- | ||
| 86 | |||
| 87 | static int g_modified_logged = 0; | ||
| 88 | |||
| 89 | void note_modified(const CUserCmd *cmd, int flags, const float *vel) { | ||
| 90 | // recompute usercmd CRC after modification | ||
| 91 | fix_verified_crc(cmd->command_number, usercmd_checksum(cmd)); | ||
| 92 | |||
| 93 | if (g_modified_logged < 5) { | ||
| 94 | logf("cmd modified: seq=%d flags=0x%X buttons=0x%X fw=%.2f " | ||
| 95 | "vel=(%.1f,%.1f,%.1f)", | ||
| 96 | cmd->command_number, flags, cmd->buttons, cmd->forwardmove, vel[0], | ||
| 97 | vel[1], vel[2]); | ||
| 98 | g_modified_logged++; | ||
| 99 | } | ||
| 100 | } | ||
| 101 | |||
| 102 | // menu state (defined in menu.cpp, referenced here) | ||
| 103 | |||
| 104 | static bool g_logged_local = false; | ||
| 105 | |||
| 106 | static int engine_get_local_player() { | ||
| 107 | using Fn = int (*)(void *); | ||
| 108 | return vfunc<Fn>(g_iface.engine, 12)(g_iface.engine); | ||
| 109 | } | ||
| 110 | |||
| 111 | static void *entitylist_get(int idx) { | ||
| 112 | using Fn = void *(*)(void *, int); | ||
| 113 | return vfunc<Fn>(g_iface.entitylist, 3)(g_iface.entitylist, idx); | ||
| 114 | } | ||
| 115 | |||
| 116 | |||
| 117 | |||
| 118 | |||
| 119 | static void do_move(int seq) { | ||
| 120 | if (!g_input || !g_net.ok()) | ||
| 121 | return; | ||
| 122 | |||
| 123 | int localIdx = engine_get_local_player(); | ||
| 124 | if (localIdx <= 0 || localIdx > 64) | ||
| 125 | return; | ||
| 126 | if (!g_logged_local) { | ||
| 127 | logf("local player index %d (in-game)", localIdx); | ||
| 128 | g_logged_local = true; | ||
| 129 | } | ||
| 130 | |||
| 131 | void *ent = entitylist_get(localIdx); | ||
| 132 | if (!ent) | ||
| 133 | return; | ||
| 134 | |||
| 135 | CUserCmd *cmd = get_usercmd(seq); | ||
| 136 | if (!cmd) | ||
| 137 | return; | ||
| 138 | |||
| 139 | uintptr_t e = (uintptr_t)ent; | ||
| 140 | uint8_t movetype = *(uint8_t *)(e + g_net.movetype); | ||
| 141 | int flags = *(int *)(e + g_net.flags); | ||
| 142 | bool onground = (flags & FL_ONGROUND) != 0; | ||
| 143 | bool inwater = (flags & FL_INWATER) != 0; | ||
| 144 | |||
| 145 | if (movetype == MOVETYPE_NOCLIP || movetype == MOVETYPE_LADDER) | ||
| 146 | return; | ||
| 147 | |||
| 148 | static bool lastground = false; | ||
| 149 | |||
| 150 | if (middle_mouse_down()) { | ||
| 151 | if (cmd->command_number % 2) { | ||
| 152 | cmd->buttons &= ~IN_JUMP; | ||
| 153 | cmd->buttons |= IN_USE; | ||
| 154 | } else { | ||
| 155 | cmd->buttons &= ~IN_USE; | ||
| 156 | cmd->buttons |= IN_JUMP; | ||
| 157 | } | ||
| 158 | note_modified(cmd, flags, (float *)(e + g_net.velocity)); | ||
| 159 | } else if (cmd->buttons & IN_JUMP) { | ||
| 160 | if (inwater && (cmd->buttons & IN_DUCK)) { | ||
| 161 | lastground = onground; | ||
| 162 | return; | ||
| 163 | } | ||
| 164 | const float *vel = (const float *)(e + g_net.velocity); | ||
| 165 | float speed = sqrtf(vel[0] * vel[0] + vel[1] * vel[1] + vel[2] * vel[2]); | ||
| 166 | bool moving = speed > 0.1f; | ||
| 167 | bool backmove = false; | ||
| 168 | if (moving) { | ||
| 169 | QAngle moveang = vector_angles(Vector{vel[0], vel[1], vel[2]}); | ||
| 170 | moveang.x = 0.f; | ||
| 171 | QAngle viewang{cmd->viewangles[0], cmd->viewangles[1], | ||
| 172 | cmd->viewangles[2]}; | ||
| 173 | viewang.x = 0.f; | ||
| 174 | backmove = getfov(viewang, moveang) > 90.f; | ||
| 175 | } | ||
| 176 | if (!onground) { | ||
| 177 | lastground = false; | ||
| 178 | cmd->buttons &= ~IN_JUMP; | ||
| 179 | note_modified(cmd, flags, vel); | ||
| 180 | } else if (!lastground) { | ||
| 181 | lastground = true; | ||
| 182 | if (!backmove) | ||
| 183 | cmd->forwardmove = -0.1f; | ||
| 184 | note_modified(cmd, flags, vel); | ||
| 185 | } | ||
| 186 | } | ||
| 187 | } | ||
| 188 | |||
| 189 | // --------------------------------------------------------------------------- | ||
| 190 | // CreateMove hook (cdecl — game virtuals use cdecl, not __thiscall) | ||
| 191 | // --------------------------------------------------------------------------- | ||
| 192 | |||
| 193 | using CreateMoveFn = bool (*)(void *, int, float, bool); | ||
| 194 | static CreateMoveFn oCreateMove = nullptr; | ||
| 195 | |||
| 196 | static bool hkdCreateMove(void *self, int seq, float frametime, bool active) { | ||
| 197 | bool ret = oCreateMove(self, seq, frametime, active); | ||
| 198 | do_move(seq); | ||
| 199 | return ret; | ||
| 200 | } | ||
| 201 | |||
| 202 | static void install_hook() { | ||
| 203 | void **vt = VTABLE(g_iface.client); | ||
| 204 | constexpr size_t SLOTS = 256; | ||
| 205 | void **newvt = new void *[SLOTS]; | ||
| 206 | memcpy(newvt, vt, SLOTS * sizeof(void *)); | ||
| 207 | oCreateMove = (CreateMoveFn)vt[21]; | ||
| 208 | newvt[21] = (void *)&hkdCreateMove; | ||
| 209 | VTABLE(g_iface.client) = newvt; | ||
| 210 | logf("hook installed: CreateMove %p -> %p (orig %p)", vt[21], | ||
| 211 | (void *)&hkdCreateMove, (void *)oCreateMove); | ||
| 212 | } | ||
| 213 | |||
| 214 | // --------------------------------------------------------------------------- | ||
| 215 | // setup thread | ||
| 216 | // --------------------------------------------------------------------------- | ||
| 217 | |||
| 218 | extern void find_modules(); | ||
| 219 | extern void resolve_netvars(); | ||
| 220 | extern bool find_input_global(); | ||
| 221 | extern void init_x11(); | ||
| 222 | extern void init_conmsg(); | ||
| 223 | extern void install_menu(); | ||
| 224 | |||
| 225 | static bool is_hl2_process() { | ||
| 226 | char buf[512]; | ||
| 227 | ssize_t n = readlink("/proc/self/exe", buf, sizeof(buf) - 1); | ||
| 228 | if (n <= 0) | ||
| 229 | return false; | ||
| 230 | buf[n] = 0; | ||
| 231 | for (ssize_t i = n - 1; i >= 0; i--) | ||
| 232 | if (buf[i] == '/') | ||
| 233 | return strcmp(buf + i + 1, "hl2_linux") == 0; | ||
| 234 | return false; | ||
| 235 | } | ||
| 236 | |||
| 237 | static void *setup_thread(void *) { | ||
| 238 | for (int i = 0; i < 6000; i++) { | ||
| 239 | find_modules(); | ||
| 240 | if (g_client.valid() && g_engine.valid()) | ||
| 241 | break; | ||
| 242 | usleep(200 * 1000); | ||
| 243 | } | ||
| 244 | if (!g_client.valid() || !g_engine.valid()) { | ||
| 245 | logf("client.so/engine.so never appeared"); | ||
| 246 | return nullptr; | ||
| 247 | } | ||
| 248 | logf("client.so: %d mapped range(s), %s", (int)g_client.ranges.size(), | ||
| 249 | g_client.path.c_str()); | ||
| 250 | logf("engine.so: %d mapped range(s), %s", (int)g_engine.ranges.size(), | ||
| 251 | g_engine.path.c_str()); | ||
| 252 | |||
| 253 | // resolve factories via dlopen handles | ||
| 254 | void *ch = dlopen(g_client.path.c_str(), RTLD_NOLOAD | RTLD_LAZY | RTLD_GLOBAL); | ||
| 255 | void *eh = dlopen(g_engine.path.c_str(), RTLD_NOLOAD | RTLD_LAZY | RTLD_GLOBAL); | ||
| 256 | auto cf = (CreateInterfaceFn)dlsym(ch, "CreateInterface"); | ||
| 257 | auto ef = (CreateInterfaceFn)dlsym(eh, "CreateInterface"); | ||
| 258 | if (!cf || !ef) { | ||
| 259 | logf("CreateInterface not found"); | ||
| 260 | return nullptr; | ||
| 261 | } | ||
| 262 | |||
| 263 | for (int i = 0; i < 300; i++) { | ||
| 264 | if (!g_iface.client) | ||
| 265 | g_iface.client = cf("VClient017", nullptr); | ||
| 266 | if (!g_iface.entitylist) | ||
| 267 | g_iface.entitylist = cf("VClientEntityList003", nullptr); | ||
| 268 | if (!g_iface.engine) | ||
| 269 | g_iface.engine = ef("VEngineClient014", nullptr); | ||
| 270 | if (g_iface.client && g_iface.entitylist && g_iface.engine) | ||
| 271 | break; | ||
| 272 | usleep(100 * 1000); | ||
| 273 | } | ||
| 274 | if (!g_iface.client || !g_iface.entitylist || !g_iface.engine) { | ||
| 275 | logf("interfaces unresolved: VClient017=%p VClientEntityList003=%p " | ||
| 276 | "VEngineClient014=%p", | ||
| 277 | g_iface.client, g_iface.entitylist, g_iface.engine); | ||
| 278 | return nullptr; | ||
| 279 | } | ||
| 280 | logf("interfaces: VClient017=%p VClientEntityList003=%p " | ||
| 281 | "VEngineClient014=%p", | ||
| 282 | g_iface.client, g_iface.entitylist, g_iface.engine); | ||
| 283 | |||
| 284 | resolve_netvars(); | ||
| 285 | if (!find_input_global()) | ||
| 286 | return nullptr; | ||
| 287 | |||
| 288 | init_conmsg(); | ||
| 289 | install_hook(); | ||
| 290 | install_menu(); | ||
| 291 | return nullptr; | ||
| 292 | } | ||
| 293 | |||
| 294 | // --------------------------------------------------------------------------- | ||
| 295 | // constructor | ||
| 296 | // --------------------------------------------------------------------------- | ||
| 297 | |||
| 298 | __attribute__((constructor)) static void hl2hake_load() { | ||
| 299 | if (!is_hl2_process()) | ||
| 300 | return; | ||
| 301 | logf("loaded (pid %d)", getpid()); | ||
| 302 | pthread_t t; | ||
| 303 | if (pthread_create(&t, nullptr, setup_thread, nullptr) == 0) | ||
| 304 | pthread_detach(t); | ||
| 305 | else | ||
| 306 | logf("pthread_create failed"); | ||
| 307 | } | ||
diff --git a/src/hl2h.h b/src/hl2h.h new file mode 100644 index 0000000..6d3ebe7 --- /dev/null +++ b/src/hl2h.h | |||
| @@ -0,0 +1,133 @@ | |||
| 1 | #ifndef HL2H_H | ||
| 2 | #define HL2H_H | ||
| 3 | |||
| 4 | #include <cstdint> | ||
| 5 | #include <cstdio> | ||
| 6 | #include <cstring> | ||
| 7 | #include <string> | ||
| 8 | #include <vector> | ||
| 9 | |||
| 10 | #include <dlfcn.h> | ||
| 11 | #include <pthread.h> | ||
| 12 | #include <unistd.h> | ||
| 13 | |||
| 14 | // --------------------------------------------------------------------------- | ||
| 15 | // logging | ||
| 16 | // --------------------------------------------------------------------------- | ||
| 17 | |||
| 18 | void logf(const char *fmt, ...); | ||
| 19 | |||
| 20 | // --------------------------------------------------------------------------- | ||
| 21 | // module discovery | ||
| 22 | // --------------------------------------------------------------------------- | ||
| 23 | |||
| 24 | struct Module { | ||
| 25 | struct Range { | ||
| 26 | uintptr_t lo, hi; | ||
| 27 | bool file; | ||
| 28 | }; | ||
| 29 | std::vector<Range> ranges; | ||
| 30 | std::string path; | ||
| 31 | bool valid() const { return !ranges.empty(); } | ||
| 32 | bool contains(uintptr_t p) const { | ||
| 33 | for (auto &r : ranges) | ||
| 34 | if (p >= r.lo && p < r.hi) | ||
| 35 | return true; | ||
| 36 | return false; | ||
| 37 | } | ||
| 38 | }; | ||
| 39 | extern Module g_client, g_engine; | ||
| 40 | |||
| 41 | // --------------------------------------------------------------------------- | ||
| 42 | // game interfaces | ||
| 43 | // --------------------------------------------------------------------------- | ||
| 44 | |||
| 45 | using CreateInterfaceFn = void *(*)(const char *, int *); | ||
| 46 | |||
| 47 | struct GameInterfaces { | ||
| 48 | void *client = nullptr; | ||
| 49 | void *entitylist = nullptr; | ||
| 50 | void *engine = nullptr; | ||
| 51 | }; | ||
| 52 | extern GameInterfaces g_iface; | ||
| 53 | |||
| 54 | #define VTABLE(obj) (*(void ***)(obj)) | ||
| 55 | template <typename Fn> inline Fn vfunc(void *obj, size_t idx) { | ||
| 56 | return reinterpret_cast<Fn>(VTABLE(obj)[idx]); | ||
| 57 | } | ||
| 58 | |||
| 59 | // --------------------------------------------------------------------------- | ||
| 60 | // netvars | ||
| 61 | // --------------------------------------------------------------------------- | ||
| 62 | |||
| 63 | struct NetVars { | ||
| 64 | int flags = -1; | ||
| 65 | int velocity = -1; | ||
| 66 | int movetype = -1; | ||
| 67 | bool ok() const { return flags >= 0 && velocity >= 0 && movetype >= 0; } | ||
| 68 | }; | ||
| 69 | extern NetVars g_net; | ||
| 70 | |||
| 71 | // --------------------------------------------------------------------------- | ||
| 72 | // CInput | ||
| 73 | // --------------------------------------------------------------------------- | ||
| 74 | |||
| 75 | extern uintptr_t g_input; | ||
| 76 | |||
| 77 | // --------------------------------------------------------------------------- | ||
| 78 | // CUserCmd | ||
| 79 | // --------------------------------------------------------------------------- | ||
| 80 | |||
| 81 | struct CUserCmd { | ||
| 82 | void *vtable; | ||
| 83 | int command_number; | ||
| 84 | int tick_count; | ||
| 85 | float viewangles[3]; | ||
| 86 | float forwardmove; | ||
| 87 | float sidemove; | ||
| 88 | float upmove; | ||
| 89 | int buttons; | ||
| 90 | uint8_t impulse; | ||
| 91 | int weaponselect; | ||
| 92 | int weaponsubtype; | ||
| 93 | int random_seed; | ||
| 94 | short mousedx; | ||
| 95 | short mousedy; | ||
| 96 | bool hasbeenpredicted; | ||
| 97 | }; | ||
| 98 | |||
| 99 | // --------------------------------------------------------------------------- | ||
| 100 | // engine constants | ||
| 101 | // --------------------------------------------------------------------------- | ||
| 102 | |||
| 103 | enum { IN_ATTACK = 1 << 0, IN_JUMP = 1 << 1, IN_DUCK = 1 << 2, IN_USE = 1 << 5 }; | ||
| 104 | enum { FL_ONGROUND = 1 << 0, FL_INWATER = 1 << 9 }; | ||
| 105 | enum { MOVETYPE_NOCLIP = 8, MOVETYPE_LADDER = 9 }; | ||
| 106 | |||
| 107 | // --------------------------------------------------------------------------- | ||
| 108 | // vector math | ||
| 109 | // --------------------------------------------------------------------------- | ||
| 110 | |||
| 111 | struct Vector { float x, y, z; }; | ||
| 112 | struct QAngle { float x, y, z; }; | ||
| 113 | QAngle vector_angles(const Vector &forward); | ||
| 114 | Vector angle_forward_vector(const QAngle &angles); | ||
| 115 | float getfov(const QAngle &my_view, const QAngle &aim_ang); | ||
| 116 | |||
| 117 | // --------------------------------------------------------------------------- | ||
| 118 | // cross-file declarations | ||
| 119 | // --------------------------------------------------------------------------- | ||
| 120 | |||
| 121 | void note_modified(const CUserCmd *cmd, int flags, const float *vel); | ||
| 122 | CUserCmd *get_usercmd(int seq); | ||
| 123 | void fix_verified_crc(int seq, uint32_t crc); | ||
| 124 | uint32_t usercmd_checksum(const CUserCmd *c); | ||
| 125 | bool find_input_global(); | ||
| 126 | void resolve_netvars(); | ||
| 127 | void find_modules(); | ||
| 128 | void install_menu(); | ||
| 129 | void init_conmsg(); | ||
| 130 | void init_x11(); | ||
| 131 | bool middle_mouse_down(); | ||
| 132 | |||
| 133 | #endif | ||
diff --git a/src/menu.cpp b/src/menu.cpp new file mode 100644 index 0000000..91a0948 --- /dev/null +++ b/src/menu.cpp | |||
| @@ -0,0 +1,555 @@ | |||
| 1 | // menu.cpp — menu state, rendering, input, config, paint hook, X11 mouse | ||
| 2 | #include "hl2h.h" | ||
| 3 | #include <cstdarg> | ||
| 4 | #include <cstdio> | ||
| 5 | #include <cstring> | ||
| 6 | |||
| 7 | // --------------------------------------------------------------------------- | ||
| 8 | // ConMsg (tier0 console printer — prints to in-game console) | ||
| 9 | // --------------------------------------------------------------------------- | ||
| 10 | |||
| 11 | void (*g_conmsg)(const char *fmt, ...) = nullptr; | ||
| 12 | |||
| 13 | void init_conmsg() { | ||
| 14 | g_conmsg = (decltype(g_conmsg))dlsym(RTLD_DEFAULT, "_Z6ConMsgPKcz"); | ||
| 15 | if (g_conmsg) | ||
| 16 | return; | ||
| 17 | // fallback: dlopen libtier0.so from the engine directory | ||
| 18 | size_t slash = g_engine.path.find_last_of('/'); | ||
| 19 | if (slash == std::string::npos) | ||
| 20 | return; | ||
| 21 | std::string tier0 = g_engine.path.substr(0, slash + 1) + "libtier0.so"; | ||
| 22 | void *h = dlopen(tier0.c_str(), RTLD_NOLOAD | RTLD_LAZY | RTLD_GLOBAL); | ||
| 23 | if (!h) | ||
| 24 | h = dlopen(tier0.c_str(), RTLD_LAZY | RTLD_GLOBAL); | ||
| 25 | if (h) | ||
| 26 | g_conmsg = (decltype(g_conmsg))dlsym(h, "_Z6ConMsgPKcz"); | ||
| 27 | } | ||
| 28 | |||
| 29 | // --------------------------------------------------------------------------- | ||
| 30 | // X11 middle mouse (for climb feature) | ||
| 31 | // --------------------------------------------------------------------------- | ||
| 32 | |||
| 33 | static struct { | ||
| 34 | void *handle = nullptr; | ||
| 35 | void *dpy = nullptr; | ||
| 36 | int (*query)(void *, unsigned long, unsigned long *, unsigned long *, int *, | ||
| 37 | int *, int *, int *, unsigned int *) = nullptr; | ||
| 38 | int (*default_screen)(void *) = nullptr; | ||
| 39 | unsigned long (*root_window)(void *, int) = nullptr; | ||
| 40 | bool ok = false; | ||
| 41 | } g_x11; | ||
| 42 | |||
| 43 | static constexpr unsigned int BUTTON2_MASK = 1u << 9; // X11 Button2Mask | ||
| 44 | |||
| 45 | void init_x11() { | ||
| 46 | g_x11.handle = dlopen("libX11.so.6", RTLD_LAZY | RTLD_GLOBAL); | ||
| 47 | if (!g_x11.handle) | ||
| 48 | return; | ||
| 49 | g_x11.query = (decltype(g_x11.query))dlsym(g_x11.handle, "XQueryPointer"); | ||
| 50 | g_x11.default_screen = | ||
| 51 | (decltype(g_x11.default_screen))dlsym(g_x11.handle, "XDefaultScreen"); | ||
| 52 | g_x11.root_window = | ||
| 53 | (decltype(g_x11.root_window))dlsym(g_x11.handle, "XRootWindow"); | ||
| 54 | auto open_display = | ||
| 55 | (void *(*)(const char *))dlsym(g_x11.handle, "XOpenDisplay"); | ||
| 56 | if (!g_x11.query || !g_x11.default_screen || !g_x11.root_window || | ||
| 57 | !open_display) | ||
| 58 | return; | ||
| 59 | g_x11.dpy = open_display(nullptr); | ||
| 60 | if (g_x11.dpy) | ||
| 61 | g_x11.ok = true; | ||
| 62 | } | ||
| 63 | |||
| 64 | bool middle_mouse_down() { | ||
| 65 | if (!g_x11.ok) | ||
| 66 | return false; | ||
| 67 | unsigned long root = | ||
| 68 | g_x11.root_window(g_x11.dpy, g_x11.default_screen(g_x11.dpy)); | ||
| 69 | unsigned long rret, cret; | ||
| 70 | int rx, ry, wx, wy; | ||
| 71 | unsigned int mask = 0; | ||
| 72 | if (!g_x11.query(g_x11.dpy, root, &rret, &cret, &rx, &ry, &wx, &wy, &mask)) | ||
| 73 | return false; | ||
| 74 | return (mask & BUTTON2_MASK) != 0; | ||
| 75 | } | ||
| 76 | |||
| 77 | // --------------------------------------------------------------------------- | ||
| 78 | // menu tree structure | ||
| 79 | // --------------------------------------------------------------------------- | ||
| 80 | |||
| 81 | enum NodeType { NODE_ROOT, NODE_FEATURE, NODE_TOGGLE, NODE_BIND }; | ||
| 82 | |||
| 83 | struct MenuNode { | ||
| 84 | NodeType type; | ||
| 85 | const char *label; | ||
| 86 | bool expanded; // for root/feature: whether children are visible | ||
| 87 | int child_count; | ||
| 88 | MenuNode *children[4]; // max children per node | ||
| 89 | |||
| 90 | // leaf data | ||
| 91 | bool *active_ptr; // NODE_TOGGLE: pointer to the active flag | ||
| 92 | int *bind_ptr; // NODE_BIND: pointer to the key binding (SDL scancode) | ||
| 93 | const char *bind_name; // NODE_BIND: current bind display name | ||
| 94 | }; | ||
| 95 | |||
| 96 | // feature data | ||
| 97 | struct FeatureState { | ||
| 98 | bool active; | ||
| 99 | int bind; // SDL scancode, 0 = none | ||
| 100 | }; | ||
| 101 | |||
| 102 | static FeatureState g_climb{true, 0}; | ||
| 103 | static FeatureState g_bhop{true, 0}; | ||
| 104 | static FeatureState g_abh{true, 0}; | ||
| 105 | |||
| 106 | // tree | ||
| 107 | static MenuNode g_root{NODE_ROOT, "HL2hake", true, 0, {}}; | ||
| 108 | static MenuNode g_climb_feat{NODE_FEATURE, "Climb", true, 2, {}}; | ||
| 109 | static MenuNode g_climb_active{NODE_TOGGLE, "Active", false, 0, {}, &g_climb.active}; | ||
| 110 | static MenuNode g_climb_bind{NODE_BIND, "Bind", false, 0, {}, nullptr, &g_climb.bind, "None"}; | ||
| 111 | static MenuNode g_bhop_feat{NODE_FEATURE, "Bunnyhop", true, 1, {}}; | ||
| 112 | static MenuNode g_bhop_active{NODE_TOGGLE, "Active", false, 0, {}, &g_bhop.active}; | ||
| 113 | static MenuNode g_abh_feat{NODE_FEATURE, "ABH", true, 1, {}}; | ||
| 114 | static MenuNode g_abh_active{NODE_TOGGLE, "Active", false, 0, {}, &g_abh.active}; | ||
| 115 | |||
| 116 | static MenuNode *g_visible[16]; // flattened visible nodes for arrow navigation | ||
| 117 | static int g_visible_count = 0; | ||
| 118 | static int g_selected = 0; | ||
| 119 | bool g_menu_open = false; | ||
| 120 | |||
| 121 | // bind-select state | ||
| 122 | static bool g_bind_mode = false; // true = waiting for key press | ||
| 123 | |||
| 124 | static void build_tree() { | ||
| 125 | g_climb_feat.children[0] = &g_climb_active; | ||
| 126 | g_climb_feat.children[1] = &g_climb_bind; | ||
| 127 | g_bhop_feat.children[0] = &g_bhop_active; | ||
| 128 | g_abh_feat.children[0] = &g_abh_active; | ||
| 129 | g_root.children[0] = &g_climb_feat; | ||
| 130 | g_root.children[1] = &g_bhop_feat; | ||
| 131 | g_root.children[2] = &g_abh_feat; | ||
| 132 | g_root.child_count = 3; | ||
| 133 | } | ||
| 134 | |||
| 135 | static void flatten(MenuNode *node, int depth) { | ||
| 136 | if (g_visible_count >= 16) | ||
| 137 | return; | ||
| 138 | node->type = (depth == 0) ? NODE_ROOT | ||
| 139 | : (node->child_count > 0) ? NODE_FEATURE | ||
| 140 | : (node->active_ptr) ? NODE_TOGGLE | ||
| 141 | : NODE_BIND; | ||
| 142 | g_visible[g_visible_count++] = node; | ||
| 143 | if (node->expanded) { | ||
| 144 | for (int i = 0; i < node->child_count; i++) | ||
| 145 | flatten(node->children[i], depth + 1); | ||
| 146 | } | ||
| 147 | } | ||
| 148 | |||
| 149 | static void rebuild_visible() { | ||
| 150 | g_visible_count = 0; | ||
| 151 | flatten(&g_root, 0); | ||
| 152 | if (g_selected >= g_visible_count) | ||
| 153 | g_selected = g_visible_count - 1; | ||
| 154 | if (g_selected < 0) | ||
| 155 | g_selected = 0; | ||
| 156 | } | ||
| 157 | |||
| 158 | // --------------------------------------------------------------------------- | ||
| 159 | // config persistence (INI format) | ||
| 160 | // --------------------------------------------------------------------------- | ||
| 161 | |||
| 162 | static std::string config_path() { | ||
| 163 | size_t slash = g_engine.path.find_last_of('/'); | ||
| 164 | if (slash == std::string::npos) | ||
| 165 | return "hl2hake.cfg"; | ||
| 166 | return g_engine.path.substr(0, slash + 1) + "hl2hake.cfg"; | ||
| 167 | } | ||
| 168 | |||
| 169 | [[maybe_unused]] static const char *sdl_scancode_name(int sc) { | ||
| 170 | if (sc == 0) | ||
| 171 | return "None"; | ||
| 172 | static char buf[32]; | ||
| 173 | snprintf(buf, sizeof(buf), "Key%d", sc); | ||
| 174 | return buf; | ||
| 175 | } | ||
| 176 | |||
| 177 | [[maybe_unused]] static int parse_sdl_scancode(const char *s) { | ||
| 178 | if (strcmp(s, "None") == 0 || strcmp(s, "") == 0) | ||
| 179 | return 0; | ||
| 180 | if (strncmp(s, "Key", 3) == 0) | ||
| 181 | return atoi(s + 3); | ||
| 182 | return 0; | ||
| 183 | } | ||
| 184 | |||
| 185 | static void config_save() { | ||
| 186 | FILE *f = fopen(config_path().c_str(), "w"); | ||
| 187 | if (!f) | ||
| 188 | return; | ||
| 189 | fprintf(f, "[climb]\nactive=%d\nbind=%d\n", g_climb.active, g_climb.bind); | ||
| 190 | fprintf(f, "[bhop]\nactive=%d\nbind=%d\n", g_bhop.active, g_bhop.bind); | ||
| 191 | fprintf(f, "[abh]\nactive=%d\nbind=%d\n", g_abh.active, g_abh.bind); | ||
| 192 | fclose(f); | ||
| 193 | } | ||
| 194 | |||
| 195 | static void config_load() { | ||
| 196 | FILE *f = fopen(config_path().c_str(), "r"); | ||
| 197 | if (!f) | ||
| 198 | return; | ||
| 199 | char line[256]; | ||
| 200 | FeatureState *cur = nullptr; | ||
| 201 | while (fgets(line, sizeof(line), f)) { | ||
| 202 | line[strcspn(line, "\r\n")] = 0; | ||
| 203 | if (line[0] == '[') { | ||
| 204 | if (strstr(line, "[climb]")) | ||
| 205 | cur = &g_climb; | ||
| 206 | else if (strstr(line, "[bhop]")) | ||
| 207 | cur = &g_bhop; | ||
| 208 | else if (strstr(line, "[abh]")) | ||
| 209 | cur = &g_abh; | ||
| 210 | else | ||
| 211 | cur = nullptr; | ||
| 212 | } else if (cur) { | ||
| 213 | char *eq = strchr(line, '='); | ||
| 214 | if (!eq) | ||
| 215 | continue; | ||
| 216 | *eq = 0; | ||
| 217 | const char *key = line, *val = eq + 1; | ||
| 218 | if (strcmp(key, "active") == 0) | ||
| 219 | cur->active = atoi(val) != 0; | ||
| 220 | else if (strcmp(key, "bind") == 0) | ||
| 221 | cur->bind = atoi(val); | ||
| 222 | } | ||
| 223 | } | ||
| 224 | fclose(f); | ||
| 225 | } | ||
| 226 | |||
| 227 | // --------------------------------------------------------------------------- | ||
| 228 | // menu input handling | ||
| 229 | // --------------------------------------------------------------------------- | ||
| 230 | |||
| 231 | // SDL scancodes for navigation keys (matches the game's scancode set) | ||
| 232 | static constexpr int SCAN_INSERT = 73; | ||
| 233 | static constexpr int SCAN_UP = 82; // these are SDL scancodes | ||
| 234 | static constexpr int SCAN_DOWN = 81; | ||
| 235 | static constexpr int SCAN_LEFT = 80; | ||
| 236 | static constexpr int SCAN_RIGHT = 79; | ||
| 237 | static constexpr int SCAN_ENTER = 40; | ||
| 238 | static constexpr int SCAN_ESCAPE = 41; | ||
| 239 | |||
| 240 | // simplified scancode-to-name for common keys | ||
| 241 | static const char *scancode_name(int sc) { | ||
| 242 | switch (sc) { | ||
| 243 | case 0: return "None"; | ||
| 244 | case 4: return "A"; case 5: return "B"; case 6: return "C"; | ||
| 245 | case 7: return "D"; case 8: return "E"; case 9: return "F"; | ||
| 246 | case 10: return "G"; case 11: return "H"; case 12: return "I"; | ||
| 247 | case 13: return "J"; case 14: return "K"; case 15: return "L"; | ||
| 248 | case 16: return "M"; case 17: return "N"; case 18: return "O"; | ||
| 249 | case 19: return "P"; case 20: return "Q"; case 21: return "R"; | ||
| 250 | case 22: return "S"; case 23: return "T"; case 24: return "U"; | ||
| 251 | case 25: return "V"; case 26: return "W"; case 27: return "X"; | ||
| 252 | case 28: return "Y"; case 29: return "Z"; | ||
| 253 | case 30: return "1"; case 31: return "2"; case 32: return "3"; | ||
| 254 | case 33: return "4"; case 34: return "5"; case 35: return "6"; | ||
| 255 | case 36: return "7"; case 37: return "8"; case 38: return "9"; | ||
| 256 | case 39: return "0"; | ||
| 257 | case 40: return "Enter"; case 41: return "Escape"; case 42: return "Backspace"; | ||
| 258 | case 43: return "Tab"; case 44: return "Space"; | ||
| 259 | case 79: return "Right"; case 80: return "Left"; | ||
| 260 | case 81: return "Down"; case 82: return "Up"; | ||
| 261 | case 87: return "F1"; case 88: return "F2"; case 89: return "F3"; | ||
| 262 | case 90: return "F4"; case 91: return "F5"; case 92: return "F6"; | ||
| 263 | case 93: return "F7"; case 94: return "F8"; | ||
| 264 | default: return scancode_name(0); // "None" for unmapped | ||
| 265 | } | ||
| 266 | } | ||
| 267 | |||
| 268 | // get a snapshot of key states: returns array of currently-held scancodes | ||
| 269 | static bool g_prev_key_state[512] = {}; | ||
| 270 | static bool g_key_down[512] = {}; // true for ONE frame on press | ||
| 271 | |||
| 272 | // read SDL_GetKeyboardState — the game's own SDL is in-process | ||
| 273 | using SDL_GetKeyboardState_fn = const uint8_t *(*)(int *); | ||
| 274 | static SDL_GetKeyboardState_fn SDL_GetKeyboardState_real = nullptr; | ||
| 275 | |||
| 276 | static void poll_keyboard() { | ||
| 277 | if (!SDL_GetKeyboardState_real) | ||
| 278 | SDL_GetKeyboardState_real = | ||
| 279 | (SDL_GetKeyboardState_fn)dlsym(RTLD_DEFAULT, "SDL_GetKeyboardState"); | ||
| 280 | if (!SDL_GetKeyboardState_real) | ||
| 281 | return; | ||
| 282 | int numkeys = 0; | ||
| 283 | const uint8_t *state = SDL_GetKeyboardState_real(&numkeys); | ||
| 284 | if (!state) | ||
| 285 | return; | ||
| 286 | for (int i = 0; i < numkeys && i < 512; i++) { | ||
| 287 | g_key_down[i] = state[i] && !g_prev_key_state[i]; | ||
| 288 | g_prev_key_state[i] = state[i]; | ||
| 289 | } | ||
| 290 | } | ||
| 291 | |||
| 292 | static bool key_pressed(int sc) { | ||
| 293 | return sc >= 0 && sc < 512 && g_key_down[sc]; | ||
| 294 | } | ||
| 295 | |||
| 296 | [[maybe_unused]] static bool key_held(int sc) { | ||
| 297 | return sc >= 0 && sc < 512 && g_prev_key_state[sc]; | ||
| 298 | } | ||
| 299 | |||
| 300 | static void menu_handle_input() { | ||
| 301 | poll_keyboard(); | ||
| 302 | |||
| 303 | if (!g_menu_open) { | ||
| 304 | if (key_pressed(SCAN_INSERT)) { | ||
| 305 | g_menu_open = true; | ||
| 306 | g_bind_mode = false; | ||
| 307 | rebuild_visible(); | ||
| 308 | } | ||
| 309 | return; | ||
| 310 | } | ||
| 311 | |||
| 312 | // menu is open | ||
| 313 | if (g_bind_mode) { | ||
| 314 | // wait for any key press (except Insert, Up, Down, Escape, Enter) | ||
| 315 | for (int i = 0; i < 512; i++) { | ||
| 316 | if (!g_key_down[i]) | ||
| 317 | continue; | ||
| 318 | if (i == SCAN_INSERT || i == SCAN_UP || i == SCAN_DOWN || | ||
| 319 | i == SCAN_LEFT || i == SCAN_RIGHT) { | ||
| 320 | g_bind_mode = false; | ||
| 321 | return; // cancel bind select | ||
| 322 | } | ||
| 323 | if (i == SCAN_ESCAPE) { | ||
| 324 | // set bind to None | ||
| 325 | MenuNode *n = g_visible[g_selected]; | ||
| 326 | if (n->bind_ptr) | ||
| 327 | *n->bind_ptr = 0; | ||
| 328 | g_bind_mode = false; | ||
| 329 | config_save(); | ||
| 330 | rebuild_visible(); | ||
| 331 | return; | ||
| 332 | } | ||
| 333 | if (i == SCAN_ENTER) { | ||
| 334 | g_bind_mode = false; | ||
| 335 | return; // cancel, keep old bind | ||
| 336 | } | ||
| 337 | // any other key = new bind | ||
| 338 | MenuNode *n = g_visible[g_selected]; | ||
| 339 | if (n->bind_ptr) { | ||
| 340 | // clear same bind from other features | ||
| 341 | FeatureState *features[] = {&g_climb, &g_bhop, &g_abh}; | ||
| 342 | for (auto *f : features) | ||
| 343 | if (&f->bind != n->bind_ptr && f->bind == i) | ||
| 344 | f->bind = 0; | ||
| 345 | *n->bind_ptr = i; | ||
| 346 | } | ||
| 347 | g_bind_mode = false; | ||
| 348 | config_save(); | ||
| 349 | rebuild_visible(); | ||
| 350 | return; | ||
| 351 | } | ||
| 352 | return; // no key pressed yet | ||
| 353 | } | ||
| 354 | |||
| 355 | // normal navigation | ||
| 356 | if (key_pressed(SCAN_INSERT) || key_pressed(SCAN_ESCAPE)) { | ||
| 357 | g_menu_open = false; | ||
| 358 | return; | ||
| 359 | } | ||
| 360 | if (key_pressed(SCAN_UP) && g_selected > 0) { | ||
| 361 | g_selected--; | ||
| 362 | return; | ||
| 363 | } | ||
| 364 | if (key_pressed(SCAN_DOWN) && g_selected < g_visible_count - 1) { | ||
| 365 | g_selected++; | ||
| 366 | return; | ||
| 367 | } | ||
| 368 | if (key_pressed(SCAN_ENTER)) { | ||
| 369 | MenuNode *n = g_visible[g_selected]; | ||
| 370 | if (n->child_count > 0) { | ||
| 371 | n->expanded = !n->expanded; | ||
| 372 | rebuild_visible(); | ||
| 373 | } else if (n->type == NODE_TOGGLE && n->active_ptr) { | ||
| 374 | *n->active_ptr = !(*n->active_ptr); | ||
| 375 | config_save(); | ||
| 376 | } else if (n->type == NODE_BIND) { | ||
| 377 | g_bind_mode = true; | ||
| 378 | } | ||
| 379 | } | ||
| 380 | } | ||
| 381 | |||
| 382 | // --------------------------------------------------------------------------- | ||
| 383 | // menu rendering (VGUI ISurface) | ||
| 384 | // --------------------------------------------------------------------------- | ||
| 385 | |||
| 386 | // VGUI surface interface | ||
| 387 | using CreateInterfaceFn = void *(*)(const char *, int *); | ||
| 388 | struct ISurface; | ||
| 389 | |||
| 390 | // VGUI font handle (opaque) | ||
| 391 | using HFont = unsigned long; | ||
| 392 | |||
| 393 | // function pointers resolved at init | ||
| 394 | static ISurface *g_surface = nullptr; | ||
| 395 | static HFont g_font = 0; | ||
| 396 | |||
| 397 | // surface vtable indices for 2007-era VGUI_SURFACE001 | ||
| 398 | // verified: slot 16 = DrawSetTextColor, slot 17 = DrawSetTextFont, | ||
| 399 | // slot 19 = DrawSetTextPos, slot 22 = DrawPrintText, | ||
| 400 | // slot 23 = DrawGetTextSize, slot 31 = CreateNewFont, slot 32 = SetFontGlyphSet | ||
| 401 | // These are from the standard VGUI surface vtable layout; will be verified | ||
| 402 | // against runtime if they fail. | ||
| 403 | static constexpr int SURF_DrawSetTextColor = 16; | ||
| 404 | static constexpr int SURF_DrawSetTextFont = 17; | ||
| 405 | static constexpr int SURF_DrawSetTextPos = 19; | ||
| 406 | static constexpr int SURF_DrawPrintText = 22; | ||
| 407 | static constexpr int SURF_CreateNewFont = 31; | ||
| 408 | static constexpr int SURF_SetFontGlyphSet = 32; | ||
| 409 | |||
| 410 | static void surf_draw_set_text_color(int r, int g, int b, int a) { | ||
| 411 | using Fn = void (*)(void *, int, int, int, int); | ||
| 412 | vfunc<Fn>(g_surface, SURF_DrawSetTextColor)(g_surface, r, g, b, a); | ||
| 413 | } | ||
| 414 | static void surf_draw_set_text_font(HFont f) { | ||
| 415 | using Fn = void (*)(void *, HFont); | ||
| 416 | vfunc<Fn>(g_surface, SURF_DrawSetTextFont)(g_surface, f); | ||
| 417 | } | ||
| 418 | static void surf_draw_set_text_pos(int x, int y) { | ||
| 419 | using Fn = void (*)(void *, int, int); | ||
| 420 | vfunc<Fn>(g_surface, SURF_DrawSetTextPos)(g_surface, x, y); | ||
| 421 | } | ||
| 422 | static void surf_draw_print_text(const wchar_t *text, int count) { | ||
| 423 | using Fn = void (*)(void *, const wchar_t *, int); | ||
| 424 | vfunc<Fn>(g_surface, SURF_DrawPrintText)(g_surface, text, count); | ||
| 425 | } | ||
| 426 | static HFont surf_create_font() { | ||
| 427 | using Fn = HFont (*)(void *); | ||
| 428 | return vfunc<Fn>(g_surface, SURF_CreateNewFont)(g_surface); | ||
| 429 | } | ||
| 430 | static void surf_set_font_glyph_set(HFont f, const char *name, int tall, | ||
| 431 | int weight, int blur, int scanlines, | ||
| 432 | int flags) { | ||
| 433 | using Fn = void (*)(void *, HFont, const char *, int, int, int, int, int, int, | ||
| 434 | int); | ||
| 435 | vfunc<Fn>(g_surface, SURF_SetFontGlyphSet)(g_surface, f, name, tall, weight, | ||
| 436 | blur, scanlines, flags, 0, 0); | ||
| 437 | } | ||
| 438 | |||
| 439 | // wide string conversion for DrawPrintText | ||
| 440 | static void draw_text(int x, int y, const char *text) { | ||
| 441 | wchar_t wtext[256]; | ||
| 442 | mbstowcs(wtext, text, 255); | ||
| 443 | wtext[255] = 0; | ||
| 444 | int len = (int)wcslen(wtext); | ||
| 445 | surf_draw_set_text_pos(x, y); | ||
| 446 | surf_draw_print_text(wtext, len); | ||
| 447 | } | ||
| 448 | |||
| 449 | // --------------------------------------------------------------------------- | ||
| 450 | // paint hook — draws menu overlay every frame | ||
| 451 | // --------------------------------------------------------------------------- | ||
| 452 | |||
| 453 | using PaintFn = void (*)(void *, int); | ||
| 454 | static PaintFn oPaint = nullptr; | ||
| 455 | |||
| 456 | static void hkdPaint(void *self, int mode) { | ||
| 457 | oPaint(self, mode); | ||
| 458 | |||
| 459 | // init font on first paint | ||
| 460 | static bool font_inited = false; | ||
| 461 | if (!font_inited) { | ||
| 462 | g_font = surf_create_font(); | ||
| 463 | if (g_font) { | ||
| 464 | surf_set_font_glyph_set(g_font, "Consolas", 14, 400, 0, 0, 0); | ||
| 465 | } | ||
| 466 | font_inited = true; | ||
| 467 | } | ||
| 468 | if (!g_font) | ||
| 469 | return; | ||
| 470 | |||
| 471 | menu_handle_input(); | ||
| 472 | |||
| 473 | if (!g_menu_open) | ||
| 474 | return; | ||
| 475 | |||
| 476 | surf_draw_set_text_font(g_font); | ||
| 477 | |||
| 478 | int y = 10; | ||
| 479 | rebuild_visible(); | ||
| 480 | for (int i = 0; i < g_visible_count; i++) { | ||
| 481 | MenuNode *n = g_visible[i]; | ||
| 482 | int indent = 0; | ||
| 483 | |||
| 484 | // count depth by counting how many parents are visible before this node | ||
| 485 | // (simple: first node = depth 0, others = depth 1 if label is Active/Bind) | ||
| 486 | if (n->type == NODE_TOGGLE || n->type == NODE_BIND) | ||
| 487 | indent = 20; | ||
| 488 | |||
| 489 | char buf[256]; | ||
| 490 | if (n->type == NODE_TOGGLE) { | ||
| 491 | snprintf(buf, sizeof(buf), "%s: %s", n->label, | ||
| 492 | *n->active_ptr ? "ON" : "OFF"); | ||
| 493 | } else if (n->type == NODE_BIND) { | ||
| 494 | snprintf(buf, sizeof(buf), "%s: %s", n->label, | ||
| 495 | scancode_name(*n->bind_ptr)); | ||
| 496 | if (g_bind_mode && g_selected == i) | ||
| 497 | snprintf(buf, sizeof(buf), "%s: ...", n->label); | ||
| 498 | } else { | ||
| 499 | snprintf(buf, sizeof(buf), "%s", n->label); | ||
| 500 | } | ||
| 501 | |||
| 502 | if (i == g_selected) { | ||
| 503 | surf_draw_set_text_color(255, 255, 0, 255); // yellow highlight | ||
| 504 | } else if (n->type == NODE_TOGGLE) { | ||
| 505 | surf_draw_set_text_color(*n->active_ptr ? 0 : 255, 255, 0, 255); | ||
| 506 | } else { | ||
| 507 | surf_draw_set_text_color(255, 255, 255, 255); | ||
| 508 | } | ||
| 509 | |||
| 510 | draw_text(10 + indent, y, buf); | ||
| 511 | y += 18; | ||
| 512 | } | ||
| 513 | |||
| 514 | // reset text color | ||
| 515 | surf_draw_set_text_color(255, 255, 255, 255); | ||
| 516 | } | ||
| 517 | |||
| 518 | // --------------------------------------------------------------------------- | ||
| 519 | // paint hook installation (called from setup_thread) | ||
| 520 | // --------------------------------------------------------------------------- | ||
| 521 | |||
| 522 | // VEngineVGui001 vtable indices for this build | ||
| 523 | // (Paint is typically slot 6 — will be verified at runtime; if wrong, | ||
| 524 | // we crash and can adjust. Common Source 2007 layout.) | ||
| 525 | static constexpr int VGUI_Paint_SLOT = 6; | ||
| 526 | |||
| 527 | static void install_paint_hook() { | ||
| 528 | // get IEngineVGui from engine | ||
| 529 | void *eh = dlopen(g_engine.path.c_str(), | ||
| 530 | RTLD_NOLOAD | RTLD_LAZY | RTLD_GLOBAL); | ||
| 531 | if (!eh) | ||
| 532 | return; | ||
| 533 | auto ef = (CreateInterfaceFn)dlsym(eh, "CreateInterface"); | ||
| 534 | if (!ef) | ||
| 535 | return; | ||
| 536 | void *evgui = ef("VEngineVGui001", nullptr); | ||
| 537 | if (!evgui) { | ||
| 538 | logf("VEngineVGui001 not found"); | ||
| 539 | return; | ||
| 540 | } | ||
| 541 | |||
| 542 | // hook Paint | ||
| 543 | void **vt = VTABLE(evgui); | ||
| 544 | oPaint = (PaintFn)vt[VGUI_Paint_SLOT]; | ||
| 545 | vt[VGUI_Paint_SLOT] = (void *)&hkdPaint; | ||
| 546 | logf("paint hook installed: Paint %p -> %p", (void *)oPaint, | ||
| 547 | (void *)&hkdPaint); | ||
| 548 | } | ||
| 549 | |||
| 550 | // public init function called from setup_thread | ||
| 551 | void install_menu() { | ||
| 552 | build_tree(); | ||
| 553 | config_load(); | ||
| 554 | install_paint_hook(); | ||
| 555 | } | ||
diff --git a/src/modules.cpp b/src/modules.cpp new file mode 100644 index 0000000..d20fe04 --- /dev/null +++ b/src/modules.cpp | |||
| @@ -0,0 +1,69 @@ | |||
| 1 | // modules.cpp — /proc/self/maps parsing and module discovery | ||
| 2 | #include "hl2h.h" | ||
| 3 | |||
| 4 | static bool basename_is(const char *path, const char *base) { | ||
| 5 | size_t pl = strlen(path), bl = strlen(base); | ||
| 6 | if (pl < bl || strcmp(path + pl - bl, base) != 0) | ||
| 7 | return false; | ||
| 8 | return pl == bl || path[pl - bl - 1] == '/'; | ||
| 9 | } | ||
| 10 | |||
| 11 | void find_modules() { | ||
| 12 | g_client = Module(); | ||
| 13 | g_engine = Module(); | ||
| 14 | FILE *f = fopen("/proc/self/maps", "r"); | ||
| 15 | if (!f) | ||
| 16 | return; | ||
| 17 | char line[1024]; | ||
| 18 | while (fgets(line, sizeof(line), f)) { | ||
| 19 | unsigned long lo, hi; | ||
| 20 | char perms[8]; | ||
| 21 | int consumed = 0; | ||
| 22 | if (sscanf(line, "%lx-%lx %7s %*x %*x:%*x %*u%n", &lo, &hi, perms, | ||
| 23 | &consumed) < 3) | ||
| 24 | continue; | ||
| 25 | if (perms[0] == '-' && perms[1] == '-' && perms[2] == '-') | ||
| 26 | continue; // PROT_NONE | ||
| 27 | |||
| 28 | const char *path = line + consumed; | ||
| 29 | while (*path == ' ' || *path == '\t') | ||
| 30 | path++; | ||
| 31 | size_t pl = strlen(path); | ||
| 32 | while (pl && (path[pl - 1] == '\n' || path[pl - 1] == '\r')) | ||
| 33 | pl--; | ||
| 34 | |||
| 35 | Module *m = nullptr; | ||
| 36 | char clean[1024]; | ||
| 37 | if (pl && pl < sizeof(clean)) { | ||
| 38 | memcpy(clean, path, pl); | ||
| 39 | clean[pl] = 0; | ||
| 40 | if (basename_is(clean, "client.so")) | ||
| 41 | m = &g_client; | ||
| 42 | else if (basename_is(clean, "engine.so")) | ||
| 43 | m = &g_engine; | ||
| 44 | } else { | ||
| 45 | // anon: absorb only as .bss tail after a file-backed range | ||
| 46 | if (g_client.valid() && g_client.ranges.back().file && | ||
| 47 | lo == g_client.ranges.back().hi && hi - lo <= 0x1000000) | ||
| 48 | m = &g_client; | ||
| 49 | else if (g_engine.valid() && g_engine.ranges.back().file && | ||
| 50 | lo == g_engine.ranges.back().hi && hi - lo <= 0x1000000) | ||
| 51 | m = &g_engine; | ||
| 52 | } | ||
| 53 | if (!m) | ||
| 54 | continue; | ||
| 55 | if (m->ranges.empty()) { | ||
| 56 | if (!pl) | ||
| 57 | continue; | ||
| 58 | m->path = clean; | ||
| 59 | } | ||
| 60 | bool file = pl != 0; | ||
| 61 | if (!m->ranges.empty() && lo == m->ranges.back().hi) { | ||
| 62 | m->ranges.back().hi = hi; | ||
| 63 | m->ranges.back().file = m->ranges.back().file && file; | ||
| 64 | } else { | ||
| 65 | m->ranges.push_back({lo, hi, file}); | ||
| 66 | } | ||
| 67 | } | ||
| 68 | fclose(f); | ||
| 69 | } | ||
diff --git a/src/netvars.cpp b/src/netvars.cpp new file mode 100644 index 0000000..41070c7 --- /dev/null +++ b/src/netvars.cpp | |||
| @@ -0,0 +1,185 @@ | |||
| 1 | // netvars.cpp — ClientClass walk, RecvTable resolution, movetype from pred-TD | ||
| 2 | #include "hl2h.h" | ||
| 3 | |||
| 4 | // RecvTable layout: pProps@0, nProps@4, pDecoder@8, pNetTableName@0xC | ||
| 5 | // RecvProp layout: pVarName@0, pArrayProp@0x18, pDataTable@0x28, offset@0x2C | ||
| 6 | // ClientClass (2004-era): CreateFn@0, CreateEventFn@4, Name@8, RecvTable@0xC, Next@0x10 | ||
| 7 | |||
| 8 | static bool module_string(uintptr_t p, char *out, size_t outn) { | ||
| 9 | if (!g_client.contains(p)) | ||
| 10 | return false; | ||
| 11 | const char *s = (const char *)p; | ||
| 12 | size_t i = 0; | ||
| 13 | while (i < outn - 1) { | ||
| 14 | unsigned char ch = (unsigned char)s[i]; | ||
| 15 | if (ch == 0) | ||
| 16 | break; | ||
| 17 | if (ch < 0x20 || ch > 0x7E) | ||
| 18 | return false; | ||
| 19 | out[i] = (char)ch; | ||
| 20 | i++; | ||
| 21 | } | ||
| 22 | if (i == 0 || i >= outn - 1) | ||
| 23 | return false; | ||
| 24 | out[i] = 0; | ||
| 25 | return true; | ||
| 26 | } | ||
| 27 | |||
| 28 | static int g_dumped_tables = 0; | ||
| 29 | |||
| 30 | static void collect_netvars(uintptr_t table, uintptr_t base_off, int depth) { | ||
| 31 | if (depth > 8) | ||
| 32 | return; | ||
| 33 | uintptr_t props = *(uintptr_t *)(table + 0x00); | ||
| 34 | int32_t nprops = *(int32_t *)(table + 0x04); | ||
| 35 | if (!props || !g_client.contains(props) || nprops <= 0 || nprops > 512) | ||
| 36 | return; | ||
| 37 | |||
| 38 | char tname[64]; | ||
| 39 | if (!module_string(*(uintptr_t *)(table + 0x0C), tname, sizeof(tname))) | ||
| 40 | return; | ||
| 41 | if (strncmp(tname, "DT_", 3) != 0) | ||
| 42 | return; | ||
| 43 | |||
| 44 | bool dump = g_dumped_tables < 2 && | ||
| 45 | (strcmp(tname, "DT_BaseEntity") == 0 || | ||
| 46 | strcmp(tname, "DT_BasePlayer") == 0); | ||
| 47 | if (dump) { | ||
| 48 | g_dumped_tables++; | ||
| 49 | logf("dump %s: %d props @ table %#lx (base_off %#lx)", tname, nprops, | ||
| 50 | (unsigned long)table, (unsigned long)base_off); | ||
| 51 | } | ||
| 52 | |||
| 53 | for (int32_t i = 0; i < nprops; i++) { | ||
| 54 | uintptr_t prop = props + (size_t)i * 0x3C; | ||
| 55 | if (!g_client.contains(prop)) | ||
| 56 | return; | ||
| 57 | uintptr_t pname = *(uintptr_t *)(prop + 0x00); | ||
| 58 | int32_t ptype = *(int32_t *)(prop + 0x04); | ||
| 59 | int32_t poff = *(int32_t *)(prop + 0x2C); | ||
| 60 | uintptr_t parray = *(uintptr_t *)(prop + 0x18); | ||
| 61 | uintptr_t ptable = *(uintptr_t *)(prop + 0x28); | ||
| 62 | |||
| 63 | char pname2[64]; | ||
| 64 | if (!module_string(pname, pname2, sizeof(pname2))) | ||
| 65 | continue; | ||
| 66 | |||
| 67 | if (dump) | ||
| 68 | logf(" [%s] %s type=%d off=%d(0x%x) table=%s array=%s", tname, pname2, | ||
| 69 | ptype, poff, poff, ptable ? "yes" : "no", parray ? "yes" : "no"); | ||
| 70 | |||
| 71 | if (ptable) { | ||
| 72 | collect_netvars(ptable, base_off + (uint32_t)poff, depth + 1); | ||
| 73 | continue; | ||
| 74 | } | ||
| 75 | |||
| 76 | if (strcmp(pname2, "m_fFlags") == 0 && g_net.flags < 0) { | ||
| 77 | g_net.flags = (int)(base_off + (uint32_t)poff); | ||
| 78 | } else if (strcmp(pname2, "m_vecVelocity") == 0 && g_net.velocity < 0) { | ||
| 79 | if (parray && g_client.contains(parray)) { | ||
| 80 | int32_t eoff = *(int32_t *)(parray + 0x2C); | ||
| 81 | g_net.velocity = (int)(base_off + (uint32_t)(eoff ? eoff : poff)); | ||
| 82 | } else { | ||
| 83 | g_net.velocity = (int)(base_off + (uint32_t)poff); | ||
| 84 | } | ||
| 85 | } | ||
| 86 | if (g_net.ok()) | ||
| 87 | return; | ||
| 88 | } | ||
| 89 | } | ||
| 90 | |||
| 91 | // prediction datamap scan for m_MoveType (FIELD_CHARACTER, fieldOffset in range) | ||
| 92 | static bool resolve_movetype() { | ||
| 93 | static const char needle[] = "m_MoveType"; | ||
| 94 | const size_t nlen = sizeof(needle); | ||
| 95 | |||
| 96 | uintptr_t str_va = 0; | ||
| 97 | for (auto &r : g_client.ranges) { | ||
| 98 | const uint8_t *base = (const uint8_t *)r.lo; | ||
| 99 | size_t span = r.hi - r.lo; | ||
| 100 | for (size_t i = 0; i + nlen <= span; i++) { | ||
| 101 | if (memcmp(base + i, needle, nlen) == 0) { | ||
| 102 | str_va = r.lo + i; | ||
| 103 | break; | ||
| 104 | } | ||
| 105 | } | ||
| 106 | if (str_va) | ||
| 107 | break; | ||
| 108 | } | ||
| 109 | if (!str_va) { | ||
| 110 | logf("m_MoveType string not found"); | ||
| 111 | return false; | ||
| 112 | } | ||
| 113 | |||
| 114 | uint32_t offs[16]; | ||
| 115 | int counts[16] = {}; | ||
| 116 | int ncand = 0; | ||
| 117 | const uint32_t target = (uint32_t)str_va; | ||
| 118 | for (auto &r : g_client.ranges) { | ||
| 119 | const uint8_t *base = (const uint8_t *)r.lo; | ||
| 120 | size_t span = r.hi - r.lo; | ||
| 121 | for (size_t off = 4; off + 8 <= span; off += 4) { | ||
| 122 | if (*(const uint32_t *)(base + off) != target) | ||
| 123 | continue; | ||
| 124 | uint32_t ftype = *(const uint32_t *)(base + off - 4); | ||
| 125 | int32_t foff = *(const int32_t *)(base + off + 4); | ||
| 126 | if (ftype != 8 || foff < 0x40 || foff > 0x400) | ||
| 127 | continue; | ||
| 128 | int k; | ||
| 129 | for (k = 0; k < ncand; k++) | ||
| 130 | if (offs[k] == (uint32_t)foff) | ||
| 131 | break; | ||
| 132 | if (k == ncand && ncand < 16) { | ||
| 133 | offs[ncand] = (uint32_t)foff; | ||
| 134 | ncand++; | ||
| 135 | } | ||
| 136 | if (k < ncand) | ||
| 137 | counts[k]++; | ||
| 138 | } | ||
| 139 | } | ||
| 140 | if (ncand == 0) { | ||
| 141 | logf("m_MoveType typedescription not found"); | ||
| 142 | return false; | ||
| 143 | } | ||
| 144 | int best = 0; | ||
| 145 | for (int k = 1; k < ncand; k++) | ||
| 146 | if (counts[k] > counts[best]) | ||
| 147 | best = k; | ||
| 148 | g_net.movetype = (int)offs[best]; | ||
| 149 | return true; | ||
| 150 | } | ||
| 151 | |||
| 152 | void resolve_netvars() { | ||
| 153 | using GetAllClassesFn = void *(*)(void *); | ||
| 154 | uintptr_t head = | ||
| 155 | (uintptr_t)vfunc<GetAllClassesFn>(g_iface.client, 8)(g_iface.client); | ||
| 156 | if (!g_client.contains(head)) { | ||
| 157 | logf("GetAllClasses returned %p, outside client module", (void *)head); | ||
| 158 | return; | ||
| 159 | } | ||
| 160 | int classes = 0; | ||
| 161 | for (uintptr_t cc = head; cc && g_client.contains(cc) && classes < 1024; | ||
| 162 | cc = *(uintptr_t *)(cc + 0x10)) { | ||
| 163 | classes++; | ||
| 164 | char name[64]; | ||
| 165 | if (!module_string(*(uintptr_t *)(cc + 0x08), name, sizeof(name))) | ||
| 166 | break; | ||
| 167 | collect_netvars(*(uintptr_t *)(cc + 0x0C), 0, 0); | ||
| 168 | if (g_net.flags >= 0 && g_net.velocity >= 0) | ||
| 169 | break; | ||
| 170 | } | ||
| 171 | logf("client classes walked: %d", classes); | ||
| 172 | |||
| 173 | if (g_net.movetype < 0 && !resolve_movetype()) { | ||
| 174 | // movetype unresolved — bhop still works, just no noclip/ladder guard | ||
| 175 | logf("movetype guard disabled"); | ||
| 176 | } | ||
| 177 | |||
| 178 | if (g_net.ok()) { | ||
| 179 | logf("netvars: m_fFlags=+0x%X m_vecVelocity[0]=+0x%X m_MoveType=+0x%X", | ||
| 180 | g_net.flags, g_net.velocity, g_net.movetype); | ||
| 181 | } else { | ||
| 182 | logf("netvar resolution incomplete: flags=%d velocity=%d movetype=%d", | ||
| 183 | g_net.flags, g_net.velocity, g_net.movetype); | ||
| 184 | } | ||
| 185 | } | ||
