summaryrefslogtreecommitdiffstats
path: root/src/cinput.cpp
blob: ad7face1c532a968195185fb6d730675d19d1de2 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
// cinput.cpp — CInput discovery, usercmd CRC, get/fix helpers
#include "hl2h.h"
uintptr_t g_input = 0;

// ---------------------------------------------------------------------------
// CRC-32 (standard IEEE reflected, matches Valve's implementation)
// ---------------------------------------------------------------------------

static uint32_t g_crc_table[256];
static bool g_crc_inited = false;

static void crc32_init_table() {
  if (g_crc_inited)
    return;
  for (uint32_t i = 0; i < 256; i++) {
    uint32_t c = i;
    for (int k = 0; k < 8; k++)
      c = (c & 1) ? 0xEDB88320u ^ (c >> 1) : c >> 1;
    g_crc_table[i] = c;
  }
  g_crc_inited = true;
}

// field set verified from client.so GetChecksum disassembly
uint32_t usercmd_checksum(const CUserCmd *c) {
  crc32_init_table();
  uint32_t crc = 0xFFFFFFFFu;
  auto upd = [&crc](const void *p, size_t n) {
    const uint8_t *b = (const uint8_t *)p;
    while (n--)
      crc = g_crc_table[(crc ^ *b++) & 0xFF] ^ (crc >> 8);
  };
  upd(&c->command_number, 4);
  upd(&c->tick_count, 4);
  upd(&c->viewangles, 12);
  upd(&c->forwardmove, 4);
  upd(&c->sidemove, 4);
  upd(&c->upmove, 4);
  upd(&c->buttons, 4);
  upd(&c->impulse, 1);
  upd(&c->weaponselect, 4);
  upd(&c->weaponsubtype, 4);
  upd(&c->random_seed, 4);
  upd(&c->mousedx, 2);
  upd(&c->mousedy, 2);
  return ~crc;
}

// CInput: +0xE4 = m_pCommands, +0xE8 = m_pVerifiedCommands
static constexpr size_t CMDOFF_M_PCOMMANDS = 0xE4;
static constexpr size_t CMDOFF_M_PVERIFIED = 0xE8;
static constexpr size_t VERIFIED_STRIDE = 0x58;
static constexpr size_t VERIFIED_CRC_OFF = 0x54;

CUserCmd *get_usercmd(int seq) {
  if (!g_input)
    return nullptr;
  uintptr_t cmds = *(uintptr_t *)(g_input + CMDOFF_M_PCOMMANDS);
  if (!cmds)
    return nullptr;
  auto *cmd = (CUserCmd *)(cmds + (size_t)(seq % 90) * 0x54);
  if (cmd->command_number != seq)
    return nullptr;
  return cmd;
}

void fix_verified_crc(int seq, uint32_t crc) {
  if (!g_input)
    return;
  uintptr_t verified = *(uintptr_t *)(g_input + CMDOFF_M_PVERIFIED);
  if (verified)
    *(uint32_t *)(verified + (size_t)(seq % 90) * VERIFIED_STRIDE +
                  VERIFIED_CRC_OFF) = crc;
}

// ---------------------------------------------------------------------------
// CInput::CreateMove hook
// ---------------------------------------------------------------------------
//
// The CHLClient::CreateMove hook runs *inside* CInput::CreateMove. After it
// returns, CInput::CreateMove overwrites cmd->random_seed with
// MD5_PseudoRandom(sequence_number), then copies the command into the verified
// command table. To choose the bullet-spread seed we must hook CInput itself
// and run after that assignment.

using InputCreateMoveFn = void (*)(void *, int, float, bool);
static InputCreateMoveFn oInputCreateMove = nullptr;
using InputExtraMouseFn = void (*)(void *, float, bool);
static InputExtraMouseFn oInputExtraMouseSample = nullptr;

// ExtraMouseSample runs AdjustAngles at render-frame rate, between movement
// commands. While the GUI is open it must not touch the view; movement still
// comes through CreateMove.
static void hkdInputExtraMouseSample(void *self, float frametime, bool active) {
  if (gui_is_open())
    return;
  if (oInputExtraMouseSample)
    oInputExtraMouseSample(self, frametime, active);
}

void input_clear_mouse_states() {
  if (!g_input)
    return;
  // CInput::ClearStates (IInput slot 20): zeros both the software mouse
  // accumulator and the engine raw-input accumulator.
  using Fn = void (*)(void *);
  vfunc<Fn>((void *)g_input, 20)((void *)g_input);
}

static void hkdInputCreateMove(void *self, int seq, float frametime,
                               bool active) {
  // Mouse deltas are accumulated between ticks. With the custom GUI open we
  // must discard them before CInput::CreateMove applies mouse look, otherwise
  // the view moves for one frame and snaps back when do_move freezes it.
  if (gui_is_open())
    input_clear_mouse_states();
  if (oInputCreateMove)
    oInputCreateMove(self, seq, frametime, active);

  // No combat features may modify the command while the GUI is open.
  if (gui_is_open())
    return;

  CUserCmd *cmd = get_usercmd(seq);
  if (!cmd)
    return;
  int local = engine_get_local_player();
  if (local <= 0)
    return;
  void *ent = entitylist_get(local);
  if (!ent)
    return;
  if (!spread_control_apply(cmd, ent))
    return;

  // Keep the verified command copy and its CRC in sync with the modified
  // random_seed. CInput::CreateMove already copied all other (aim/move) fields.
  uintptr_t verified = *(uintptr_t *)(g_input + CMDOFF_M_PVERIFIED);
  if (verified) {
    CUserCmd *vc =
        (CUserCmd *)(verified + (size_t)(seq % 90) * VERIFIED_STRIDE);
    vc->random_seed = cmd->random_seed;
  }
  fix_verified_crc(seq, usercmd_checksum(cmd));
}

static bool install_input_hook() {
  if (!g_input)
    return false;
  void **vt = VTABLE((void *)g_input);
  if (!vt)
    return false;
  constexpr size_t N = 64;
  void **nvt = new void *[N];
  memcpy(nvt, vt, N * sizeof(void *));
  oInputCreateMove = (InputCreateMoveFn)vt[3];
  nvt[3] = (void *)&hkdInputCreateMove;
  if (N > 4) {
    oInputExtraMouseSample = (InputExtraMouseFn)vt[4];
    nvt[4] = (void *)&hkdInputExtraMouseSample;
  }
  VTABLE((void *)g_input) = nvt;
  logf("CInput CreateMove hooked: slot 3 orig=%p; ExtraMouseSample slot 4 "
       "orig=%p",
       (void *)oInputCreateMove, (void *)oInputExtraMouseSample);
  return true;
}

// ---------------------------------------------------------------------------
// CInput discovery: byte-scan of CHLClient::CreateMove prologue
// ---------------------------------------------------------------------------

bool find_input_global() {
  void **vt = VTABLE(g_iface.client);
  uintptr_t cm = (uintptr_t)vt[21]; // CreateMove slot
  if (!g_client.contains(cm)) {
    logf("CreateMove %p outside client module", (void *)cm);
    return false;
  }
  const uint8_t *b = (const uint8_t *)cm;

  // 1) PIC anchor: call __x86.get_pc_thunk + add $imm,%esi
  uintptr_t anchor = 0;
  for (int i = 0; i <= 0x100 - 11; i++) {
    if (b[i] == 0xE8 && b[i + 5] == 0x81 && b[i + 6] == 0xC6) {
      uint32_t imm;
      memcpy(&imm, b + i + 7, 4);
      anchor = cm + (uintptr_t)(i + 5) + imm;
      break;
    }
  }
  if (!anchor || !g_client.contains(anchor)) {
    logf("PIC anchor not found in CreateMove");
    return false;
  }

  // 2) the input->CreateMove call: "ff 51 0c" preceded by "52", scan back
  //    for "8d 86 imm32" with "8b 10" between
  for (int c = 3; c <= 0x100 - 3; c++) {
    if (!(b[c] == 0xFF && b[c + 1] == 0x51 && b[c + 2] == 0x0C &&
          b[c - 1] == 0x52))
      continue;
    for (int i = c - 8; i > c - 44 && i > 2; i--) {
      if (b[i] != 0x8D || b[i + 1] != 0x86)
        continue;
      bool has_mov = false;
      for (int j = i + 6; j < c - 1; j++)
        if (b[j] == 0x8B && b[j + 1] == 0x10) {
          has_mov = true;
          break;
        }
      if (!has_mov)
        continue;
      uint32_t disp;
      memcpy(&disp, b + i + 2, 4);
      uintptr_t var = anchor + disp;
      if (!g_client.contains(var))
        continue;
      uintptr_t input = *(uintptr_t *)var;
      if (!input || !g_client.contains(input))
        continue;
      uintptr_t vtbl = *(uintptr_t *)input;
      if (!vtbl || !g_client.contains(vtbl))
        continue;
      uintptr_t f3 = *(uintptr_t *)(vtbl + 3 * sizeof(void *));
      if (!g_client.contains(f3))
        continue;
      logf("CInput* = %#lx (ptr var %#lx, vfunc[3] %#lx)", (unsigned long)input,
           (unsigned long)var, (unsigned long)f3);
      g_input = input;
      install_input_hook();
      return true;
    }
  }
  logf("CInput pointer variable not found in CreateMove scan");
  return false;
}