-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmousefix.cpp
More file actions
1377 lines (1252 loc) · 54.8 KB
/
Copy pathmousefix.cpp
File metadata and controls
1377 lines (1252 loc) · 54.8 KB
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
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// ============================================================================
// Alien: Isolation Mouse Fix (mousefix.asi)
// ----------------------------------------------------------------------------
// Fixes four separate input problems in AI.exe (32-bit):
//
// 1. DEADZONE FIX - The engine compares each frame's mouse delta against
// a fixed epsilon; sub-threshold input gets applied and
// then clawed back by a "settle" routine that overwrites
// yaw/pitch with smoothed values. We NOP the two stores
// (16 bytes). Framerate-independent by construction.
//
// 2. SMOOTHING FIX - The cinematic camera (rewire panels, lockers, vents)
// uses a target/current pair: mouse moves the target,
// an interpolator eases current toward it (the "floaty"
// lag). We hook the interpolator's store and force
// current = target every frame. Look clamps still apply.
//
// 3. SENS FIX - The same cinematic camera runs ~12.4x (yaw) / ~3.9x
// (pitch) the FPS camera's measured response. We hook
// the cinematic input routine and scale each axis by a
// measured per-axis correction, multiplied by a live
// FOV-compensation factor (0% monitor-distance match
// between the cinematic camera's 80 deg vertical FOV
// and the player's configured FOV, read from memory).
//
// 4. CLAMP FIX - The cinematic camera's look limits are cramped and
// appear tuned for a controller. Known stock limit
// sets are recognised by value and replaced with
// wider ones read from the INI. Also covers the
// maintenance jack, the door lever and other
// shared-limit interactions (comms button, vent
// entrance), and the mantle (climb-up) animation -
// all locked by default.
//
// Additions (new behavior the stock game does not have; each can be
// disabled in the INI):
//
// 5. CLICK TO INTERACT - In cinematic interaction views (security access
// tuner, rewire panels, computer terminals and the
// terminal hack minigame) left click sends the view's
// confirm key (E, or Enter in the minigame) and right
// click sends Q (back/exit). At terminals a short
// right-click TAP means back while HOLD keeps the
// game's native freelook.
//
// 6. SCROLL FIX - Terminal logs scroll much further per mouse-wheel
// notch than the tiny stock step. Wheel notches are
// translated into PageDown/PageUp presses while a
// terminal is open; speed, backlog and pacing are all
// configurable.
//
// 7. MENU CLICK - Left click advances the 'press any key' title
// screen (one harmless key tap); right click sends
// Escape (back) throughout the menus. Menu state comes
// from a module-level flag the game maintains, and the
// title screen is told apart from the main menu by a
// field on the screen object captured by two hooks.
//
// All patch sites are located by AOB signature at runtime - no file edits.
// If any signature is not found, that fix is skipped and logged; the game
// runs stock for that subsystem. Never crashes on mismatch.
//
// Settings are read from mousefix.ini beside this DLL; defaults below.
//
// Build (x86 ONLY - the game is 32-bit):
// Open the "x86 Native Tools Command Prompt for VS" and run:
// cl /LD /O2 mousefix.cpp /link /OUT:mousefix.asi
// ============================================================================
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <math.h>
#include <stdio.h>
#include <time.h>
#pragma comment(lib, "user32.lib") // SendInput / window APIs
// Bump this on every release: rebuild, re-zip, tag to match
static const char* MOD_VERSION = "2.0.0";
// Paths resolved from this DLL's own location, so the INI and log always sit
// beside the .asi regardless of the game's working directory
static char g_iniPath[MAX_PATH] = {0};
static char g_logPath[MAX_PATH] = {0};
// ----------------------------------------------------------------------------
// Measured calibration constants
// ----------------------------------------------------------------------------
static const float BASE_KY = 0.080572f; // yaw angular-parity factor
static const float BASE_KP = 0.25465f; // pitch angular-parity factor
static const float CINE_FOV_DEG = 80.0f; // cinematic cam vertical FOV
static const float FOV_REF = 47.0f; // game's reference FOV
// (fov ratio in memory = FOV/47)
static const int FOV_POLL_MS = 250;
static const int CLAMP_POLL_MS = 100;
// ----------------------------------------------------------------------------
// Configuration (mousefix.ini, read once at startup; defaults = calibrated fix)
// ----------------------------------------------------------------------------
static int g_cfgDeadzoneFix = 1;
static int g_cfgSmoothingFix = 1;
static int g_cfgSensFix = 1;
static float g_cfgCineSensMul = 1.0f;
static float g_cfgCineYawMul = 1.0f;
static float g_cfgCinePitchMul = 1.0f;
static int g_cfgFovComp = 1;
static int g_cfgClampFix = 1;
static int g_cfgClickToInteract = 1;
static int g_cfgScrollFix = 1;
static int g_cfgMenuClick = 1;
static float g_cfgScrollMult = 2.5f;
static int g_cfgScrollBuffer = 25;
static int g_cfgTapSpacingMs = 6; // gap between queued page-taps
static int g_cfgLogging = 1;
// Clamp profiles: a stock limit set identifies a view; the fixed set replaces
// it. All values in radians. Order: yawMin, yawMax, pitchMin, pitchMax.
struct ClampProfile
{
const char* name;
const char* section; // INI section holding the fixed values
float stock[4];
float fixed[4]; // filled from the INI at startup
};
#define DEG(x) ((float)((x) * 3.14159265f / 180.0f))
static ClampProfile g_clampProfiles[] =
{
{ "locker", "Clamping.Locker",
{ DEG(-18), DEG(18), DEG(-25), DEG(10) },
{ DEG(-46), DEG(46), DEG(-25), DEG(32) } },
{ "security access tuner", "Clamping.SecurityAccessTuner",
{ DEG(-1), DEG(0), DEG(-1), DEG(1) },
{ DEG(-1), DEG(-1), DEG(1), DEG(1) } },
{ "maintenance jack", "Clamping.MaintenanceJack",
{ DEG(-5), DEG(5), DEG(-5), DEG(0) },
{ 0.0f, 0.0f, 0.0f, 0.0f } },
{ "door lever / comms / vent entrance", "Clamping.SharedInteractions",
{ DEG(-15), DEG(15), DEG(-15), DEG(15) },
{ 0.0f, 0.0f, 0.0f, 0.0f } },
{ "mantle animation", "Clamping.Mantle",
{ DEG(-10), DEG(10), DEG(-10), DEG(10) },
{ 0.0f, 0.0f, 0.0f, 0.0f } },
};
static const int G_CLAMP_COUNT = sizeof(g_clampProfiles) / sizeof(g_clampProfiles[0]);
// Cine camera struct offsets for the look limits
static const int OFF_YAW_MIN = 0x9C;
static const int OFF_YAW_MAX = 0xA0;
static const int OFF_PITCH_MIN = 0x8C;
static const int OFF_PITCH_MAX = 0x90;
// Live multipliers consumed by the hooks (updated by the FOV thread)
// Initialised for FOV = 47 (M = tan(40)/tan(23.5) = 1.9299) so behavior is
// sane before the camera pointer is captured; config multipliers are folded
// in by the FOV thread on its first tick
static volatile float g_Ky = 0.155487f;
static volatile float g_Kp = 0.491422f;
// Captured by the cinematic input hook: cine camera struct base (clamp fix)
static void* volatile g_cineCam = NULL;
// Captured by the yaw-writer hook: player camera struct base
// FOV ratio (currentFOV / 47) lives at [g_camBase - 0x774]
static void* volatile g_camBase = NULL;
// Menu screen object, captured by both screen-state hooks (one fires on
// entering the title screen, one on leaving it, so the pointer is known
// after the first transition in either direction).
// [g_menuObj + 0x574] reads 0 on the 'press any key' title screen and
// 1 in the main menu. The object gets recycled, so validate the field is
// 0 or 1 before trusting it.
static volatile void* g_menuObj = NULL;
// Return addresses for the naked trampolines (filled in at patch time)
static void* g_smoothRet = NULL;
static void* g_cineRet = NULL;
static void* g_pitchRet = NULL;
static void* g_yawcapRet = NULL;
static void* g_menuRet = NULL; // entering the title screen
static void* g_menuSetRet = NULL; // leaving it
// ----------------------------------------------------------------------------
// Logging
// ----------------------------------------------------------------------------
static bool g_logStarted = false;
static void Log(const char* fmt, ...)
{
if (!g_cfgLogging || !g_logPath[0]) return;
FILE* f = NULL;
// First write of each run truncates; the rest append
if (fopen_s(&f, g_logPath, g_logStarted ? "a" : "w") != 0 || !f) return;
g_logStarted = true;
va_list ap; va_start(ap, fmt);
vfprintf(f, fmt, ap);
va_end(ap);
fprintf(f, "\n");
fclose(f);
}
// ----------------------------------------------------------------------------
// Resolve mousefix.ini / mousefix.log next to this DLL, then read settings
// ----------------------------------------------------------------------------
static float IniFloat(const char* section, const char* key, float def)
{
char buf[64], defbuf[64];
sprintf_s(defbuf, "%g", def);
GetPrivateProfileStringA(section, key, defbuf, buf, sizeof(buf), g_iniPath);
float v = (float)atof(buf);
return (v == 0.0f && buf[0] != '0') ? def : v; // unparsable -> default
}
// Written verbatim if mousefix.ini is missing, so a deleted or mangled file
// can always be recovered by deleting it and relaunching
static const char* DEFAULT_INI =
"; ============================================================================\n"
"; Alien: Isolation Mouse Fix - configuration\n"
";\n"
"; Every setting here is optional. If you run into problems, delete this\n"
"; file and it will be recreated with defaults on the next launch.\n"
";\n"
"; Edit with any text editor. Changes take effect on the next game launch.\n"
"; Check mousefix.log (same folder) to confirm what was applied.\n"
"; ============================================================================\n"
"\n"
"; ============================================================================\n"
"; FIXES - repairs to the game's own mouse handling\n"
"; ============================================================================\n"
"\n"
"[Deadzone]\n"
"\n"
"; Low-speed deadzone fix (normal gameplay)\n"
"; Removes the logic that dampens slow mouse movement\n"
"; 1 = fixed (default), 0 = stock game behavior\n"
"DeadzoneFix = 1\n"
"\n"
"\n"
"[Smoothing]\n"
"\n"
"; Cinematic-camera smoothing fix (rewire panels, lockers, etc.)\n"
"; Makes the camera track the mouse directly instead of drifting after it\n"
"; 1 = fixed (default), 0 = stock game behavior\n"
"SmoothingFix = 1\n"
"\n"
"\n"
"[Sensitivity]\n"
"\n"
"; Cinematic-camera sensitivity fix (rewire panels, lockers, etc.)\n"
"; Matches sensitivity in those views to normal gameplay\n"
"; 1 = fixed (default), 0 = stock game behavior\n"
"SensFix = 1\n"
"\n"
"; FOV compensation for the cinematic camera\n"
"; Those views render at a wider FOV than normal gameplay, so identical\n"
"; rotation covers less of the screen. With this on, the fix compensates\n"
"; so both views FEEL the same, recalculated live from the in-game FOV\n"
"; setting\n"
"; 1 = compensate (default, recommended)\n"
"; 0 = match raw rotation per mouse count instead; the cinematic camera\n"
"; will feel slower than normal gameplay\n"
"FovCompensation = 1\n"
"\n"
"; Extra multiplier on cinematic-camera sensitivity, applied on top of the\n"
"; calibrated correction. 1.0 = matched to normal gameplay (default)\n"
"; Raise for faster panel/locker aiming, lower for slower\n"
"; Only used when SensFix = 1\n"
"CineSensMultiplier = 1.0\n"
"\n"
"; Per-axis trim for the cinematic camera (rewire panels, lockers, etc.),\n"
"; multiplied on top of CineSensMultiplier\n"
"; Leave at 1.0 unless the yaw/pitch balance feels off on an unusual\n"
"; aspect ratio or display setup\n"
"CineYawMultiplier = 1.0\n"
"CinePitchMultiplier = 1.0\n"
"\n"
"\n"
"; ============================================================================\n"
"; ADDITIONS - behavior the stock game does not have\n"
"; ============================================================================\n"
"\n"
"[ClickToInteract]\n"
"\n"
"; In cinematic interaction views (security access tuner, rewire panels,\n"
"; computer terminals and the terminal hack minigame) LEFT CLICK sends\n"
"; that view's confirm key (E, or Enter in the minigame) and RIGHT CLICK\n"
"; sends Q (back / exit).\n"
"; At computer terminals a short right-click TAP means back, while HOLD\n"
"; keeps the game's normal freelook.\n"
"; 1 = on (default), 0 = off\n"
"Enabled = 1\n"
"\n"
"\n"
"[ScrollFix]\n"
"\n"
"; Mouse-wheel notches scroll terminal logs much further than the\n"
"; stock wheel step (by sending PageDown / PageUp presses).\n"
"; Only active while a computer terminal is open.\n"
"; 1 = on (default), 0 = off\n"
"Enabled = 1\n"
"\n"
"; Scroll speed multiplier: page-taps sent per wheel notch\n"
"; 1.0 = one page per notch. 2.0 = two pages per notch. Default 2.5\n"
"; Fractions work too: 1.5 alternates one and two pages per notch\n"
"SpeedMultiplier = 2.5\n"
"\n"
"; How many extra pages can wait in line when you scroll faster than\n"
"; pages can be sent. The scroll that starts a payout always counts in\n"
"; full; this only limits the backlog behind it.\n"
"; The unit is PAGES, not wheel notches - with a 2.5 multiplier, a\n"
"; buffer of 25 is ten notches' worth.\n"
"; 0 = no buffer: extra scrolling during a payout is ignored\n"
"; 25 = default: far more than anyone rolls in one burst, so nothing\n"
"; is lost in practice, while still bounding the backlog\n"
"; -1 = unlimited: every notch is honored no matter what\n"
"; Scrolling the other way always cancels whatever is still queued\n"
"ScrollBuffer = 25\n"
"\n"
"; Milliseconds between queued page-taps. Lower = faster payout of big\n"
"; multipliers/buffers, but more pages become irreversible when you\n"
"; reverse direction mid-scroll (the game glides through pages already\n"
"; sent). Higher = every reversal feels instant, slower total payout.\n"
"TapSpacing = 6\n"
"\n"
"\n"
"[MenuClick]\n"
"\n"
"; LEFT CLICK advances the title screen (the 'press any key' screen,\n"
"; where the mouse natively does nothing), and RIGHT CLICK sends Escape\n"
"; (back) throughout the menus. Left click is left alone inside the\n"
"; menus, which already support the mouse natively.\n"
"; 1 = on (default), 0 = off\n"
"Enabled = 1\n"
"\n"
"\n"
"; ============================================================================\n"
"; CLAMP FIX - per-view look limits\n"
"; ============================================================================\n"
"\n"
"[Clamping]\n"
"\n"
"; The cinematic camera limits how far you can look in each direction.\n"
"; These limits are inconsistent and appear to be tuned for a controller.\n"
"; This adjusts them per view - widening the cramped ones, and locking\n"
"; those where a small amount of freedom feels unnatural.\n"
"; 1 = widened (default), 0 = stock game behavior\n"
"ClampFix = 1\n"
"\n"
"\n"
"[Clamping.Locker]\n"
"\n"
"; Look limits in degrees from center\n"
"; Negative yaw is left, negative pitch is down\n"
"; Stock game values: -18 / 18 / -25 / 10 (set ClampFix = 0 to use those)\n"
"YawMin = -46\n"
"YawMax = 46\n"
"PitchMin = -25\n"
"PitchMax = 32\n"
"\n"
"\n"
"[Clamping.SecurityAccessTuner]\n"
"\n"
"; Look limits in degrees from center\n"
"; Negative yaw is left, negative pitch is down\n"
"; Stock game values: -1 / 0 / -1 / 1 (set ClampFix = 0 to use those)\n"
"; Setting min equal to max locks that axis, which is what the defaults do\n"
"YawMin = -1\n"
"YawMax = -1\n"
"PitchMin = 1\n"
"PitchMax = 1\n"
"\n"
"\n"
"[Clamping.MaintenanceJack]\n"
"\n"
"; Look limits in degrees from center\n"
"; Negative yaw is left, negative pitch is down\n"
"; Stock game values: -5 / 5 / -5 / 0 (set ClampFix = 0 to use those)\n"
"; Defaults lock the view fully - the stock freedom is too small to be\n"
"; useful\n"
"YawMin = 0\n"
"YawMax = 0\n"
"PitchMin = 0\n"
"PitchMax = 0\n"
"\n"
"\n"
"[Clamping.SharedInteractions]\n"
"\n"
"; Door levers, small button presses (e.g. comms link) and the vent\n"
"; entrance animation all share one set of stock limits\n"
"; Stock game values: -15 / 15 / -15 / 15 (set ClampFix = 0 to use those)\n"
"; Defaults lock the view fully - the stock freedom is too small to be\n"
"; useful\n"
"YawMin = 0\n"
"YawMax = 0\n"
"PitchMin = 0\n"
"PitchMax = 0\n"
"\n"
"\n"
"[Clamping.Mantle]\n"
"\n"
"; The brief mantle (climb-up) animation\n"
"; Stock game values: -10 / 10 / -10 / 10 (set ClampFix = 0 to use those)\n"
"; Defaults lock the view fully - the stock freedom is too small to be\n"
"; useful\n"
"YawMin = 0\n"
"YawMax = 0\n"
"PitchMin = 0\n"
"PitchMax = 0\n"
"\n"
"\n"
"[Advanced]\n"
"\n"
"; Write a log of what was patched to mousefix.log\n"
"; 1 = on (default). Useful for troubleshooting; harmless to leave on\n"
"Logging = 1\n";
static bool g_iniWasCreated = false;
static void LoadConfig(HMODULE self)
{
char dir[MAX_PATH];
GetModuleFileNameA(self, dir, MAX_PATH);
char* slash = strrchr(dir, '\\');
if (slash) *(slash + 1) = '\0';
sprintf_s(g_iniPath, "%smousefix.ini", dir);
sprintf_s(g_logPath, "%smousefix.log", dir);
// Recreate the INI with documented defaults if it is missing
if (GetFileAttributesA(g_iniPath) == INVALID_FILE_ATTRIBUTES)
{
FILE* f = NULL;
if (fopen_s(&f, g_iniPath, "w") == 0 && f)
{
fputs(DEFAULT_INI, f);
fclose(f);
g_iniWasCreated = true;
}
}
g_cfgDeadzoneFix = GetPrivateProfileIntA("Deadzone", "DeadzoneFix", 1, g_iniPath);
g_cfgSmoothingFix = GetPrivateProfileIntA("Smoothing", "SmoothingFix", 1, g_iniPath);
g_cfgSensFix = GetPrivateProfileIntA("Sensitivity", "SensFix", 1, g_iniPath);
g_cfgFovComp = GetPrivateProfileIntA("Sensitivity", "FovCompensation", 1, g_iniPath);
g_cfgClampFix = GetPrivateProfileIntA("Clamping", "ClampFix", 1, g_iniPath);
g_cfgClickToInteract = GetPrivateProfileIntA("ClickToInteract", "Enabled", 1, g_iniPath);
g_cfgScrollFix = GetPrivateProfileIntA("ScrollFix", "Enabled", 1, g_iniPath);
g_cfgScrollMult = IniFloat("ScrollFix", "SpeedMultiplier", 2.5f);
if (g_cfgScrollMult < 0.1f || g_cfgScrollMult > 10.0f) g_cfgScrollMult = 2.5f;
g_cfgScrollBuffer = GetPrivateProfileIntA("ScrollFix", "ScrollBuffer", 25, g_iniPath);
if (g_cfgScrollBuffer < -1) g_cfgScrollBuffer = 25; // -1 = unlimited
if (g_cfgScrollBuffer > 1000) g_cfgScrollBuffer = 1000;
g_cfgTapSpacingMs = GetPrivateProfileIntA("ScrollFix", "TapSpacing", 6, g_iniPath);
g_cfgMenuClick = GetPrivateProfileIntA("MenuClick", "Enabled", 1, g_iniPath);
if (g_cfgTapSpacingMs < 0) g_cfgTapSpacingMs = 6;
if (g_cfgTapSpacingMs > 1000) g_cfgTapSpacingMs = 1000;
g_cfgLogging = GetPrivateProfileIntA("Advanced", "Logging", 1, g_iniPath);
g_cfgCineSensMul = IniFloat("Sensitivity", "CineSensMultiplier", 1.0f);
g_cfgCineYawMul = IniFloat("Sensitivity", "CineYawMultiplier", 1.0f);
g_cfgCinePitchMul = IniFloat("Sensitivity", "CinePitchMultiplier", 1.0f);
// Clamp profiles: read the fixed limits (degrees in the INI, radians here)
for (int i = 0; i < G_CLAMP_COUNT; ++i)
{
ClampProfile& p = g_clampProfiles[i];
const char* keys[4] = { "YawMin", "YawMax", "PitchMin", "PitchMax" };
for (int k = 0; k < 4; ++k)
{
float deg = p.fixed[k] * 180.0f / 3.14159265f; // current default
float v = IniFloat(p.section, keys[k], deg);
if (v < -180.0f || v > 180.0f) v = deg; // guard typos
p.fixed[k] = DEG(v);
}
}
// Guard against typos that would make the game unplayable
if (g_cfgCineSensMul <= 0.0f || g_cfgCineSensMul > 100.0f) g_cfgCineSensMul = 1.0f;
if (g_cfgCineYawMul <= 0.0f || g_cfgCineYawMul > 100.0f) g_cfgCineYawMul = 1.0f;
if (g_cfgCinePitchMul <= 0.0f || g_cfgCinePitchMul > 100.0f) g_cfgCinePitchMul = 1.0f;
}
// ----------------------------------------------------------------------------
// AOB scan over the main module
// ----------------------------------------------------------------------------
static BYTE* FindPattern(const BYTE* pat, size_t len)
{
BYTE* base = (BYTE*)GetModuleHandleA(NULL);
if (!base) return NULL;
IMAGE_DOS_HEADER* dos = (IMAGE_DOS_HEADER*)base;
IMAGE_NT_HEADERS* nt = (IMAGE_NT_HEADERS*)(base + dos->e_lfanew);
size_t size = nt->OptionalHeader.SizeOfImage;
for (size_t i = 0; i + len <= size; ++i)
{
if (memcmp(base + i, pat, len) == 0)
return base + i;
}
return NULL;
}
static void WriteBytes(void* dst, const void* src, size_t len)
{
DWORD old;
VirtualProtect(dst, len, PAGE_EXECUTE_READWRITE, &old);
memcpy(dst, src, len);
VirtualProtect(dst, len, old, &old);
FlushInstructionCache(GetCurrentProcess(), dst, len);
}
// Write E9 rel32 jump at 'site' to 'target', pad remainder of stolen bytes
// with NOPs
static void WriteJump(BYTE* site, void* target, size_t stolen)
{
BYTE buf[16];
buf[0] = 0xE9;
*(INT32*)(buf + 1) = (INT32)((BYTE*)target - (site + 5));
for (size_t i = 5; i < stolen; ++i) buf[i] = 0x90;
WriteBytes(site, buf, stolen);
}
// ----------------------------------------------------------------------------
// Trampolines (x86, __declspec(naked))
// ----------------------------------------------------------------------------
// SMOOTHING: hooked at "fstp dword ptr [esi+E8]" (6 bytes stolen)
// Re-execute the fstp (keeps x87 stack balanced), then current = target
// for both yaw and pitch, then return to original flow
static __declspec(naked) void SmoothTramp()
{
__asm {
fstp dword ptr [esi+0E8h]
movss xmm0, dword ptr [esi+0F0h]
movss dword ptr [esi+0E8h], xmm0 // current yaw = target yaw
movss xmm0, dword ptr [esi+0ECh]
movss dword ptr [esi+0E4h], xmm0 // current pitch = target pitch
jmp dword ptr [g_smoothRet]
}
}
// SENS (yaw) + cine camera capture: hooked at "movss xmm0,[ecx+AC]"
// (8 bytes stolen). Stores ecx (the cine camera) for the clamp fix,
// re-executes the load, scales by g_Ky, returns
// g_Ky is 1.0 when the sens fix is off, so the multiply is a no-op then
static __declspec(naked) void CineTramp()
{
__asm {
mov dword ptr [g_cineCam], ecx
movss xmm0, dword ptr [ecx+0ACh]
mulss xmm0, dword ptr [g_Ky]
jmp dword ptr [g_cineRet]
}
}
// SCREEN STATE capture (entering the title screen): hooked at
// "mov [esi+574],0" (10 bytes stolen). esi = screen object.
static __declspec(naked) void MenuTramp()
{
__asm {
mov dword ptr [g_menuObj], esi
mov dword ptr [esi+574h], 0
jmp dword ptr [g_menuRet]
}
}
// SCREEN STATE capture (leaving the title screen): hooked at
// "mov [esi+574],eax" (6 bytes stolen). Same object as above.
static __declspec(naked) void MenuSetTramp()
{
__asm {
mov dword ptr [g_menuObj], esi
mov dword ptr [esi+574h], eax
jmp dword ptr [g_menuSetRet]
}
}
// SENS (pitch): hooked at "mulss xmm1,[eax+20]" (5 bytes stolen)
static __declspec(naked) void PitchTramp()
{
__asm {
mulss xmm1, dword ptr [eax+20h]
mulss xmm1, dword ptr [g_Kp]
jmp dword ptr [g_pitchRet]
}
}
// CAMERA CAPTURE: hooked at "addss xmm1,[esi+204]" in the player camera's
// fast-path yaw writer (8 bytes stolen). Stores the struct base so the FOV
// thread can read the fov ratio at base-0x774
static __declspec(naked) void YawCapTramp()
{
__asm {
mov dword ptr [g_camBase], esi
addss xmm1, dword ptr [esi+204h]
jmp dword ptr [g_yawcapRet]
}
}
// ----------------------------------------------------------------------------
// FOV compensation thread
// M = tan(cineFOV/2) / tan(playerFOV/2), applied to both base factors
// ----------------------------------------------------------------------------
static DWORD WINAPI FovThread(LPVOID)
{
const float tanCine = tanf(CINE_FOV_DEG * 0.5f * 3.14159265f / 180.0f);
for (;;)
{
Sleep(FOV_POLL_MS);
BYTE* cam = (BYTE*)g_camBase;
if (!cam) continue;
float ratio = 0.0f;
__try {
ratio = *(float*)(cam - 0x774);
} __except (EXCEPTION_EXECUTE_HANDLER) {
continue; // stale pointer: skip this tick
}
// Sanity gate - on garbage, keep last known-good values
if (ratio < 0.5f || ratio > 3.0f) continue;
float fovDeg = ratio * FOV_REF;
float M = tanCine / tanf(fovDeg * 0.5f * 3.14159265f / 180.0f);
g_Ky = BASE_KY * M * g_cfgCineSensMul * g_cfgCineYawMul;
g_Kp = BASE_KP * M * g_cfgCineSensMul * g_cfgCinePitchMul;
}
return 0;
}
// ----------------------------------------------------------------------------
// Clamp fix thread
// Watches the cine camera's look limits. When they match a known stock set,
// they are replaced with the configured values. Because the match is against
// the STOCK values, this re-applies every time the game stamps them back
// (on entering a locker, etc.) and never compounds.
// ----------------------------------------------------------------------------
static bool NearRad(float a, float b) { return fabsf(a - b) < 0.0001f; }
static DWORD WINAPI ClampThread(LPVOID)
{
int lastLogged = -1;
for (;;)
{
Sleep(CLAMP_POLL_MS);
BYTE* cam = (BYTE*)g_cineCam;
if (!cam) continue;
float cur[4];
__try {
cur[0] = *(float*)(cam + OFF_YAW_MIN);
cur[1] = *(float*)(cam + OFF_YAW_MAX);
cur[2] = *(float*)(cam + OFF_PITCH_MIN);
cur[3] = *(float*)(cam + OFF_PITCH_MAX);
} __except (EXCEPTION_EXECUTE_HANDLER) {
continue; // stale pointer: skip this tick
}
for (int i = 0; i < G_CLAMP_COUNT; ++i)
{
const ClampProfile& p = g_clampProfiles[i];
if (NearRad(cur[0], p.stock[0]) && NearRad(cur[1], p.stock[1]) &&
NearRad(cur[2], p.stock[2]) && NearRad(cur[3], p.stock[3]))
{
__try {
*(float*)(cam + OFF_YAW_MIN) = p.fixed[0];
*(float*)(cam + OFF_YAW_MAX) = p.fixed[1];
*(float*)(cam + OFF_PITCH_MIN) = p.fixed[2];
*(float*)(cam + OFF_PITCH_MAX) = p.fixed[3];
} __except (EXCEPTION_EXECUTE_HANDLER) {
break;
}
if (lastLogged != i)
{
Log("clamp profile applied: %s", p.name);
lastLogged = i;
}
break;
}
}
}
return 0;
}
// ----------------------------------------------------------------------------
// Additions: click-to-interact, terminal scroll fix, menu click
// ----------------------------------------------------------------------------
// --- key sender (SendInput, VK + scan code; the game reads scan codes) ------
static void SendKeyTap(WORD vk, WORD scan, bool extended)
{
INPUT in[2];
ZeroMemory(in, sizeof(in));
DWORD flags = KEYEVENTF_SCANCODE | (extended ? KEYEVENTF_EXTENDEDKEY : 0);
in[0].type = INPUT_KEYBOARD;
in[0].ki.wVk = vk;
in[0].ki.wScan = scan;
in[0].ki.dwFlags = flags;
in[1] = in[0];
in[1].ki.dwFlags = flags | KEYEVENTF_KEYUP;
SendInput(1, &in[0], sizeof(INPUT));
Sleep(25);
SendInput(1, &in[1], sizeof(INPUT));
}
// --- shared view tests ------------------------------------------------------
// Two cine-camera fields used to identify and qualify a view:
//
// +0xAC fov. Reads 0 at a terminal and 1.3963 in every other cinematic
// view, so terminal identity is two-factor: all-zero clamps AND
// fov ~ 0.
// +0x78 seconds since the last interaction in the current view. Resets
// to ~0.402 on each interaction and counts up while the view is
// open, so it changes continuously whenever a view is live and
// stops once the view closes. Used as a liveness test - has it
// moved recently, not what does it read.
static const int OFF_CINE_FOV = 0xAC;
static const int OFF_CINE_ACT = 0x78;
static bool ReadClamps(BYTE* cam, float* out4, float* fov)
{
__try {
out4[0] = *(float*)(cam + OFF_YAW_MIN);
out4[1] = *(float*)(cam + OFF_YAW_MAX);
out4[2] = *(float*)(cam + OFF_PITCH_MIN);
out4[3] = *(float*)(cam + OFF_PITCH_MAX);
if (fov) *fov = *(float*)(cam + OFF_CINE_FOV);
return true;
} __except (EXCEPTION_EXECUTE_HANDLER) {
return false;
}
}
static bool SigMatch(const float* cur, const float* sig)
{
return NearRad(cur[0], sig[0]) && NearRad(cur[1], sig[1]) &&
NearRad(cur[2], sig[2]) && NearRad(cur[3], sig[3]);
}
// Used by the scroll fix. Signature (zero clamps + zero fov) AND liveness:
// clamp values persist after a view closes, so without the liveness test a
// terminal you have walked away from still looks open, and mouse wheel
// scrolling would fire page keys during ordinary gameplay.
// Keeps its OWN liveness state - this runs on the window thread while
// CineViewActive() runs on the interact thread, and they must not share.
static bool IsTerminalOpen()
{
BYTE* cam = (BYTE*)g_cineCam;
if (!cam) return false;
static int sLastAct = 0;
static bool sActInit = false;
static DWORD sLastChange = 0;
int act = 0;
__try { act = *(int*)(cam + OFF_CINE_ACT); }
__except (EXCEPTION_EXECUTE_HANDLER) { return false; }
DWORD now = GetTickCount();
if (!sActInit) { sLastAct = act; sActInit = true; sLastChange = 0; }
else if (act != sLastAct) { sLastAct = act; sLastChange = now; }
if (!sLastChange || (now - sLastChange) >= 300) return false;
float c[4], fov;
if (!ReadClamps(cam, c, &fov)) return false;
return NearRad(c[0], 0) && NearRad(c[1], 0) &&
NearRad(c[2], 0) && NearRad(c[3], 0) && NearRad(fov, 0);
}
// --- click-to-interact ------------------------------------------------------
// Views matched BY NAME on their clamp signatures; rows can be reordered
// freely. lmb: 'e' = send E, 'n' = send Enter. qmode: 'p' = Q on press,
// 't' = Q on release if held < TAP_MS (terminal: hold = native freelook).
static const int TAP_MS = 500; // higher = lazier taps count as "back",
// but an aborted freelook may fire Q
static const int DISGUISE_MS = 400;
static const int LIVENESS_MS = 150;
static const int INTERACT_POLL = 15;
static const float SIG_TUNER_STOCK[4] = { DEG(-1), DEG(0), DEG(-1), DEG(1) };
static const float SIG_PANEL[4] = { DEG(-57.5f),DEG(57.5f),DEG(-32.5f),DEG(32.5f)};
static const float SIG_MINIGAME[4] = { DEG(-80), DEG(80), DEG(-40), DEG(40) };
static const float SIG_TERMINAL[4] = { 0, 0, 0, 0 };
struct InteractView
{
const char* name;
const float* sig; // 4 clamp values (radians)
char lmb; // 'e' or 'n' (Enter)
char qmode; // 'p' press | 't' tap
bool needFovZero; // terminal second factor
};
// NOTE: "tuner (locked)" points at the tuner clamp profile's FIXED values,
// which are INI-configurable - resolved at thread start so a user-tuned
// lock is still recognised.
static float g_tunerLocked[4];
static InteractView g_views[] =
{
{ "tuner (stock)", SIG_TUNER_STOCK, 'e', 'p', false },
{ "tuner (locked)", g_tunerLocked, 'e', 'p', false },
{ "rewire panel", SIG_PANEL, 'e', 'p', false },
{ "terminal hack minigame", SIG_MINIGAME, 'n', 'p', false },
{ "terminal", SIG_TERMINAL, 'e', 't', true },
};
static const int G_VIEW_COUNT = sizeof(g_views) / sizeof(g_views[0]);
static bool GameHasFocus()
{
// The foreground window must belong to THIS process.
DWORD pid = 0;
GetWindowThreadProcessId(GetForegroundWindow(), &pid);
return pid == GetCurrentProcessId();
}
// --- menu detection (feeds menu click) --------------------------------------
// Two module-relative flags, both verified by census:
// [AI.exe+OFF_INGAME] : 1 during gameplay, 0 in the main menu AND the
// in-game pause menu (a gameplay/menu timer pair
// drives it - whichever context is inactive freezes)
// [g_menuObj+0x68] : 1 on the 'press any key' title screen, 0 past
// it (captured by the two menu hooks)
// No inference, no state machine.
static const size_t OFF_INGAME = 0x15A3158;
static bool g_inMenu = false;
static bool AtTitleScreen()
{
if (!g_menuObj) return false;
int t = -1;
__try { t = *(int*)((BYTE*)g_menuObj + 0x574); }
__except (EXCEPTION_EXECUTE_HANDLER) { t = -1; }
// The field is a 0/1 flag. Anything else means the object we captured
// has been recycled for something else - do not trust it.
if (t != 0 && t != 1) return false;
return t == 0; // 0 = title screen, 1 = main menu
}
// Entering menu state is debounced: the flag must read 0 continuously for
// MENU_SETTLE_MS before we believe it. Transient dips (e.g. while a
// cinematic view spins up) would otherwise turn a right-click meant for Q
// into an Escape. Leaving menu state is immediate - a stale "in menu"
// belief is the harmful direction. The title screen path does not consult
// this flag at all, so title -> main menu -> right-click stays instant.
static const DWORD MENU_SETTLE_MS = 250;
// A recognised cinematic view outranks the menu flag: if the cine camera
// is showing a tuner / panel / terminal / minigame, we are in gameplay no
// matter what a transient flag dip says. This keeps a right-click meant
// for Q from ever becoming an Escape.
static bool CineViewActive()
{
BYTE* cam = (BYTE*)g_cineCam;
if (!cam) return false;
// Clamp values PERSIST after a view ends - the camera keeps the last
// view's limits while you are back in gameplay or even in the menus.
// So a signature match alone is not enough: [cam+78] must also still
// be moving, which it does continuously while a view is open.
static int lastAct = 0;
static bool actInit = false;
static DWORD lastChange = 0;
int act = 0;
__try { act = *(int*)(cam + OFF_CINE_ACT); }
__except (EXCEPTION_EXECUTE_HANDLER) { return false; }
DWORD now = GetTickCount();
if (!actInit) { lastAct = act; actInit = true; lastChange = 0; }
else if (act != lastAct) { lastAct = act; lastChange = now; }
if (!lastChange || (now - lastChange) >= 300) return false;
float cur[4], fov;
if (!ReadClamps(cam, cur, &fov)) return false;
for (int i = 0; i < G_VIEW_COUNT; ++i)
{
if (SigMatch(cur, g_views[i].sig))
{
if (g_views[i].needFovZero && !NearRad(fov, 0)) continue;
return true;
}
}
return false;
}
// Menu state comes from the module flag alone: census shows it reads 0 in
// the main menu, the pause menu and the title screen, and 1 in gameplay and
// in cinematic views (terminal, tuner, panels). The screen object is not
// consulted here - it only tells the title screen apart from the other
// menus, for the G tap.
static void UpdateMenuState()
{
static DWORD zeroSince = 0;
// A recognised cinematic view is gameplay, whatever the flag says.
if (CineViewActive()) { g_inMenu = false; zeroSince = 0; return; }
BYTE* base = (BYTE*)GetModuleHandleA(NULL);
if (!base) { g_inMenu = false; zeroSince = 0; return; }
int v = 1;
__try { v = *(int*)(base + OFF_INGAME); }
__except (EXCEPTION_EXECUTE_HANDLER) { v = 1; }
if (v != 0) // gameplay: believe it immediately
{
g_inMenu = false;
zeroSince = 0;
return;
}
if (CineViewActive()) // cine view outranks the flag
{
g_inMenu = false;
zeroSince = 0;
return;
}
DWORD now = GetTickCount(); // menu: only after it settles
if (!zeroSince) zeroSince = now;
if (now - zeroSince >= MENU_SETTLE_MS) g_inMenu = true;
}
static DWORD WINAPI InteractThread(LPVOID)
{
// resolve the locked-tuner signature from the (possibly INI-tuned)
// tuner profile
for (int i = 0; i < G_CLAMP_COUNT; ++i)
if (strcmp(g_clampProfiles[i].name, "security access tuner") == 0)
for (int k = 0; k < 4; ++k)
g_tunerLocked[k] = g_clampProfiles[i].fixed[k];
bool prevL = true, prevR = true; // true: swallow a held button at start
DWORD rDownTime = 0; bool rTimerArmed = false;
int lastAct = 0; bool actInit = false;
DWORD lastActTime = 0, lastZeros = 0;
for (;;)
{
Sleep(INTERACT_POLL);
BYTE* cam = (BYTE*)g_cineCam;
// NOTE: cam may be NULL at the boot title screen; the menu-click
// branch below must still run, so the NULL check happens after it
if (!GameHasFocus()) // reset edges so a click made
{ // elsewhere can't fire on refocus
prevL = prevR = true;
rTimerArmed = false;
continue;
}
DWORD now = GetTickCount();
// menu click: at the title screen / main menu, LMB = Enter and
// RMB = Escape; cine logic below never runs while in the menu
if (g_cfgMenuClick)
{
UpdateMenuState();
if (g_inMenu)
{
// LMB sends 'G' on the "press any key" screen only -
// any key advances it, and an unbound letter is inert
// everywhere else, including the key-binding listener.
// Before either screen-state hook has fired nothing has
// been captured, and the only menu reachable in that
// window is the boot title screen, so assume it.
// Fire on RELEASE, not press: if the menu appears while
// the button is still physically held, the game ignores
// the next click - it never saw that button go down on
// this screen. Releasing first matches the keyboard case.
bool atTitle = g_menuObj ? AtTitleScreen() : true;
bool Lm = (GetAsyncKeyState(VK_LBUTTON) & 0x8000) != 0;
if (!Lm && prevL && atTitle) SendKeyTap('G', 0x22, false);
prevL = Lm;
// RMB sends Escape (back) anywhere in the menus
bool Rm = (GetAsyncKeyState(VK_RBUTTON) & 0x8000) != 0;
if (Rm && !prevR) SendKeyTap(VK_ESCAPE, 0x01, false);
prevR = Rm;
rTimerArmed = false;
continue;
}
}
if (!cam) continue; // no cine struct yet (and not menu)
// liveness: [cam+78] must have CHANGED recently (first sample is
// baseline only - nil-to-value is not an edge)
int act = 0;
__try { act = *(int*)(cam + OFF_CINE_ACT); }
__except (EXCEPTION_EXECUTE_HANDLER) { continue; }