Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
axstin
GitHub Repository: axstin/rbxfpsunlocker
Path: blob/master/Source/main.cpp
259 views
1
#include <Windows.h>
2
#include <iostream>
3
#include <sstream>
4
#include <vector>
5
#include <codecvt>
6
#include <unordered_map>
7
#include <unordered_set>
8
#include <chrono>
9
#include <fstream>
10
#include <mutex>
11
#include <TlHelp32.h>
12
#include <winternl.h>
13
#include <ShlObj.h>
14
15
#pragma comment(lib, "Shlwapi.lib")
16
#include <Shlwapi.h>
17
18
#include "ui.h"
19
#include "settings.h"
20
#include "rfu.h"
21
#include "procutil.h"
22
#include "sigscan.h"
23
#include "fflags.hpp"
24
25
#define ROBLOX_BASIC_ACCESS (PROCESS_QUERY_INFORMATION | PROCESS_VM_READ)
26
#define ROBLOX_WRITE_ACCESS (PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_READ | PROCESS_VM_WRITE)
27
28
HANDLE SingletonMutex;
29
30
enum class RobloxHandleType
31
{
32
None,
33
Client,
34
UWP,
35
Studio
36
};
37
38
struct RobloxProcessHandle
39
{
40
DWORD id;
41
RobloxHandleType type;
42
43
std::shared_ptr<ProcUtil::ScopedHandle> shared_handle;
44
bool can_write;
45
46
RobloxProcessHandle(DWORD process_id = 0, RobloxHandleType type = RobloxHandleType::None, bool open = false) : id(process_id), shared_handle(nullptr), type(type), can_write(false)
47
{
48
if (open) Open();
49
};
50
51
bool ShouldAttach() const
52
{
53
switch (type)
54
{
55
case RobloxHandleType::Client:
56
case RobloxHandleType::UWP:
57
return Settings::UnlockClient;
58
case RobloxHandleType::Studio:
59
return Settings::UnlockStudio;
60
default:
61
return false;
62
}
63
}
64
65
bool IsValid() const
66
{
67
return id != 0;
68
}
69
70
HANDLE Handle() const
71
{
72
return shared_handle ? shared_handle->value : NULL;
73
}
74
75
bool IsOpen() const
76
{
77
return Handle() != NULL;
78
}
79
80
bool Open()
81
{
82
can_write = type == RobloxHandleType::Studio;
83
if (HANDLE handle = OpenProcess(can_write ? ROBLOX_WRITE_ACCESS : ROBLOX_BASIC_ACCESS, FALSE, id))
84
{
85
shared_handle = std::make_shared<ProcUtil::ScopedHandle>(handle);
86
return true;
87
}
88
return false;
89
}
90
91
std::filesystem::path FetchPath()
92
{
93
assert(IsValid());
94
auto modules = ProcUtil::GetProcessModules(id, 1);
95
if (!modules.empty()) return modules[0].path;
96
return {};
97
}
98
99
HANDLE CreateWriteHandle() const
100
{
101
assert(IsOpen());
102
HANDLE new_handle = NULL;
103
DuplicateHandle(GetCurrentProcess(), Handle(), GetCurrentProcess(), &new_handle, ROBLOX_WRITE_ACCESS, FALSE, NULL);
104
return new_handle;
105
}
106
107
template <typename T>
108
void Write(const void *location, const T &value) const
109
{
110
assert(IsOpen());
111
if (can_write)
112
{
113
printf("[%u] Writing to %p\n", id, location);
114
ProcUtil::Write<T>(Handle(), location, value);
115
}
116
else
117
{
118
auto write_handle = CreateWriteHandle();
119
if (!write_handle) throw ProcUtil::WindowsException("failed to create write handle");
120
printf("[%u] Writing to %p with handle %p\n", id, location, write_handle);
121
ProcUtil::Write<T>(write_handle, location, value);
122
CloseHandle(write_handle);
123
}
124
}
125
};
126
127
std::vector<RobloxProcessHandle> GetRobloxProcesses(bool open_all, bool include_client, bool include_studio)
128
{
129
std::vector<RobloxProcessHandle> result;
130
if (include_client)
131
{
132
for (auto pid : ProcUtil::GetProcessIdsByImageName("RobloxPlayerBeta.exe")) result.emplace_back(pid, RobloxHandleType::Client, open_all);
133
for (auto pid : ProcUtil::GetProcessIdsByImageName("Windows10Universal.exe")) result.emplace_back(pid, RobloxHandleType::UWP, open_all);
134
}
135
if (include_studio)
136
{
137
for (auto pid : ProcUtil::GetProcessIdsByImageName("RobloxStudioBeta.exe")) result.emplace_back(pid, RobloxHandleType::Studio, open_all);
138
}
139
return result;
140
}
141
142
RobloxProcessHandle GetRobloxProcess()
143
{
144
auto processes = GetRobloxProcesses(true, true, true);
145
146
if (processes.empty())
147
return {};
148
149
if (processes.size() == 1)
150
return processes[0];
151
152
printf("Multiple processes found! Select a process to inject into (%u - %zu):\n", 1, processes.size());
153
for (int i = 0; i < processes.size(); i++)
154
{
155
try
156
{
157
ProcUtil::ProcessInfo info(processes[i].Handle(), true);
158
printf("[%d] [%s] %s\n", i + 1, info.name.c_str(), info.window_title.c_str());
159
}
160
catch (ProcUtil::WindowsException& e)
161
{
162
printf("[%d] Invalid process %p (%s, %X)\n", i + 1, processes[i].Handle(), e.what(), e.GetLastError());
163
}
164
}
165
166
int selection;
167
168
while (true)
169
{
170
printf("\n>");
171
std::cin >> selection;
172
173
if (std::cin.fail())
174
{
175
std::cin.clear();
176
std::cin.ignore(std::cin.rdbuf()->in_avail());
177
printf("Invalid input, try again\n");
178
continue;
179
}
180
181
if (selection < 1 || selection > processes.size())
182
{
183
printf("Please enter a number between %u and %zu\n", 1, processes.size());
184
continue;
185
}
186
187
break;
188
}
189
190
return processes[selection - 1];
191
}
192
193
void NotifyError(const char* title, const char* error)
194
{
195
if (Settings::SilentErrors || Settings::NonBlockingErrors)
196
{
197
// lol
198
HANDLE console = GetStdHandle(STD_OUTPUT_HANDLE);
199
CONSOLE_SCREEN_BUFFER_INFO info{};
200
GetConsoleScreenBufferInfo(console, &info);
201
202
WORD color = (info.wAttributes & 0xFF00) | FOREGROUND_RED | FOREGROUND_INTENSITY;
203
SetConsoleTextAttribute(console, color);
204
205
printf("[ERROR] %s\n", error);
206
207
SetConsoleTextAttribute(console, info.wAttributes);
208
209
if (!Settings::SilentErrors)
210
{
211
UI::SetConsoleVisible(true);
212
}
213
}
214
else
215
{
216
UI::Message(error, title, MB_OK);
217
}
218
}
219
220
bool RobloxFFlags::apply(bool prompt)
221
{
222
if (target_fps_mod || alt_enter_mod)
223
{
224
if (!write_disk())
225
{
226
NotifyError("rbxfpsunlocker Error", "Failed to write ClientAppSettings.json! Try running Roblox FPS Unlocker as administrator or using a different unlock method.");
227
return false;
228
}
229
230
auto target_fps_value = target_fps();
231
auto alt_enter_value = alt_enter();
232
233
printf("Wrote flags to %ls (target_fps=", settings_file_path.c_str());
234
if (target_fps_value) printf("%llu", *target_fps_value); else printf("null");
235
printf(", alt_enter=");
236
if (alt_enter_value) printf("%u", *alt_enter_value); else printf("null");
237
printf(")\n");
238
239
240
if (prompt)
241
{
242
std::stringstream stream{};
243
if (target_fps_mod)
244
{
245
stream << "Set DFIntTaskSchedulerTargetFps to ";
246
auto value = target_fps();
247
if (value) stream << *value; else stream << "<null>";
248
stream << "\n";
249
}
250
if (alt_enter_mod)
251
{
252
stream << "Set FFlagHandleAltEnterFullscreenManually to ";
253
auto value = alt_enter();
254
if (value) stream << *value; else stream << "<null>";
255
}
256
stream << "\n\nin " << settings_file_path << "\n\nRestarting Roblox may be required for changes to take effect.";
257
258
UI::Message(stream.str());
259
}
260
261
target_fps_mod = false;
262
alt_enter_mod = false;
263
264
return true;
265
}
266
267
return false;
268
}
269
270
bool CheckExecutableFile64Bit(const std::filesystem::path &path)
271
{
272
std::ifstream file(path, std::ios::binary);
273
if (!file.is_open()) return false;
274
275
IMAGE_DOS_HEADER dos_header{};
276
file.read((char *)&dos_header, sizeof(dos_header));
277
if (!file || dos_header.e_magic != 0x5A4D) return false;
278
279
IMAGE_NT_HEADERS32 headers{};
280
file.seekg(dos_header.e_lfanew);
281
file.read((char *)&headers, sizeof(headers));
282
if (!file || headers.Signature != 0x4550) return false;
283
284
return headers.OptionalHeader.Magic == 0x020B;
285
}
286
287
std::filesystem::path GetLocalAppDataPath()
288
{
289
wchar_t *path = nullptr;
290
if (SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, NULL, &path) == S_OK)
291
{
292
assert(path);
293
return path;
294
}
295
return {};
296
}
297
298
class RobloxInstance
299
{
300
std::filesystem::path bin_path{};
301
std::filesystem::path version_folder{};
302
bool is_client = false;
303
304
RobloxProcessHandle process{};
305
306
ProcUtil::ModuleInfo main_module{};
307
std::vector<const void *> ts_ptr_candidates; // task scheduler pointer candidates
308
const void *fd_ptr = nullptr; // frame delay pointer
309
bool use_flags_file = false;
310
int retries_left = 0;
311
312
bool BlockingLoadModuleInfo()
313
{
314
int tries = 6;
315
int wait_time = 100;
316
317
printf("[%u] Finding process base...\n", process.id);
318
319
while (true)
320
{
321
main_module = ProcUtil::GetMainModuleInfo(process.Handle());
322
323
if (main_module.base != nullptr)
324
{
325
MEMORY_BASIC_INFORMATION mbi{};
326
if (VirtualQueryEx(process.Handle(), main_module.base, &mbi, sizeof(mbi)) && mbi.Type == MEM_PRIVATE)
327
{
328
// We need the Hyperion-mapped Roblox base, not this
329
// This VirtualQueryEx check will fail eventually due to Hyperion stripping our handle
330
printf("[%u] WARNING: GetMainModuleInfo returned invalid base address\n", process.id);
331
}
332
else
333
{
334
return true;
335
}
336
}
337
338
if (tries--)
339
{
340
printf("[%u] Retrying in %dms...\n", process.id, wait_time);
341
Sleep(wait_time);
342
wait_time *= 2;
343
} else
344
{
345
return false;
346
}
347
}
348
}
349
350
bool IsLikelyAntiCheatProtected() const
351
{
352
if (process.IsOpen())
353
{
354
return is_client && ProcUtil::IsProcess64Bit(process.Handle());
355
}
356
else
357
{
358
return is_client && CheckExecutableFile64Bit(bin_path);
359
}
360
}
361
362
void SetFPSCapInMemory(double cap)
363
{
364
if (fd_ptr)
365
{
366
try
367
{
368
static const double min_frame_delay = 1.0 / 10000.0;
369
double frame_delay = cap <= 0.0 ? min_frame_delay : 1.0 / cap;
370
371
process.Write(fd_ptr, frame_delay);
372
} catch (ProcUtil::WindowsException &e)
373
{
374
printf("[%u] RobloxProcess::SetFPSCapInMemory failed: %s (%d)\n", process.id, e.what(), e.GetLastError());
375
}
376
}
377
}
378
379
bool FindTaskScheduler()
380
{
381
try
382
{
383
const auto handle = process.Handle();
384
const auto start = (const uint8_t *)main_module.base;
385
const auto end = start + main_module.size;
386
387
if (ProcUtil::IsProcess64Bit(handle))
388
{
389
// Assume Studio
390
// Hyperion clients are no longer supported: the pointer to TaskScheduler is now obfuscated
391
392
std::unordered_set<const void *> candidates{};
393
auto i = start;
394
auto stop = (std::min)(end, start + 70 * 1024 * 1024); // optim: keep search roughly within .text
395
396
while (i < stop)
397
{
398
// 48 8B 05 ?? ?? ?? ?? 8B 00
399
auto result = (const uint8_t *)ProcUtil::ScanProcess(handle, "\x48\x8B\x05\x00\x00\x00\x00\x8B\x00", "xxx????xx", i, stop); // mov rax, <Rel32>; mov eax, [rax]
400
if (!result) break;
401
candidates.insert(result + 7 + ProcUtil::Read<int32_t>(handle, result + 3));
402
i = result + 1;
403
}
404
405
printf("[%u] GetTaskScheduler (sig 64-bit generic): found %zu candidates\n", process.id, candidates.size());
406
407
if (candidates.empty())
408
return false; // keep looking
409
410
ts_ptr_candidates = std::vector<const void *>(candidates.begin(), candidates.end());
411
return true;
412
}
413
else
414
{
415
// 32-bit clients aren't supported
416
}
417
}
418
catch (ProcUtil::WindowsException &e)
419
{
420
}
421
422
return false;
423
}
424
425
size_t FindTaskSchedulerFrameDelayOffset(const void *scheduler) const
426
{
427
const size_t search_offset = 0x100; // ProcUtil::IsProcess64Bit(process) ? 0x200 : 0x100;
428
429
uint8_t buffer[0x100];
430
if (!ProcUtil::Read(process.Handle(), (const uint8_t *)scheduler + search_offset, buffer, sizeof(buffer)))
431
return -1;
432
433
/* Find the frame delay variable inside TaskScheduler (ugly, but it should survive updates unless the variable is removed or shifted)
434
(variable was at +0x150 (32-bit) and +0x180 (studio 64-bit) as of 2/13/2020) */
435
for (int i = 0; i < sizeof(buffer) - sizeof(double); i += 4)
436
{
437
static const double frame_delay = 1.0 / 60.0;
438
double difference = *(double *)(buffer + i) - frame_delay;
439
difference = difference < 0 ? -difference : difference;
440
if (difference < std::numeric_limits<double>::epsilon()) return search_offset + i;
441
}
442
443
return -1;
444
}
445
446
bool ShouldUseFlagsFile() const
447
{
448
return Settings::UnlockMethod == Settings::UnlockMethodType::FlagsFile || (Settings::UnlockMethod == Settings::UnlockMethodType::Hybrid && IsLikelyAntiCheatProtected());
449
}
450
451
public:
452
RobloxInstance() {}
453
RobloxInstance(std::filesystem::path binary_path, bool is_client) : bin_path(std::move(binary_path)), is_client(is_client)
454
{
455
version_folder = bin_path.parent_path();
456
use_flags_file = ShouldUseFlagsFile();
457
}
458
459
const RobloxProcessHandle &GetHandle() const
460
{
461
return process;
462
}
463
464
bool IsClient() const
465
{
466
return is_client;
467
}
468
469
bool IsStudio() const
470
{
471
return !is_client;
472
}
473
474
bool IsRegistryInstance() const
475
{
476
return !process.IsValid();
477
}
478
479
bool AttachProcess(const RobloxProcessHandle &handle, int retry_count)
480
{
481
process = handle;
482
bin_path = process.FetchPath(); // note: CreateToolhelp32Snapshot opens a handle momentarily here
483
is_client = handle.type != RobloxHandleType::Studio;
484
retries_left = retry_count;
485
486
printf("[%u] Attached to PID %u, bin path %ls\n", process.id, process.id, bin_path.c_str());
487
488
if (handle.type == RobloxHandleType::UWP)
489
{
490
// Roblox's package name (ROBLOXCorporation.ROBLOX) and publisher ID (55nm5eh3cm0pr) shouldn't ever change
491
version_folder = GetLocalAppDataPath() / "Packages" / "ROBLOXCorporation.ROBLOX_55nm5eh3cm0pr" / "LocalState";
492
}
493
else
494
{
495
version_folder = bin_path.parent_path();
496
}
497
498
printf("[%u] Using version folder %ls\n", process.id, version_folder.c_str());
499
500
OnEvent(RFU::Event::SETTINGS_MASK);
501
MemoryWriteTick();
502
503
return !ts_ptr_candidates.empty() && fd_ptr != nullptr;
504
}
505
506
void MemoryWriteTick()
507
{
508
if (use_flags_file)
509
return;
510
511
if (retries_left < 0)
512
return; // we tried
513
514
if (!process.IsOpen())
515
{
516
process.Open();
517
printf("[%u] Opened handle %p (can_write: %u)\n", process.id, process.Handle(), process.can_write);
518
}
519
520
if (!main_module.base)
521
{
522
if (!BlockingLoadModuleInfo())
523
{
524
NotifyError("rbxfpsunlocker Error", "Failed to get process base! Restart Roblox FPS Unlocker or, if you are on a 64-bit operating system, make sure you are using the 64-bit version of Roblox FPS Unlocker.");
525
retries_left = -1;
526
return;
527
}
528
529
printf("[%u] Process base: %p (size %zu)\n", process.id, main_module.base, main_module.size);
530
531
// Small windows exist where we can attach to Roblox's security daemon while it isn't being debugged (see GetRobloxProcesses)
532
// As a secondary measure, check module size (daemon is about 1MB, client is about 80MB)
533
if (main_module.size < 1024 * 1024 * 10)
534
{
535
printf("[%u] Ignoring security daemon process\n", process.id);
536
retries_left = -1;
537
return;
538
}
539
}
540
541
if (ts_ptr_candidates.empty())
542
{
543
const auto start_time = std::chrono::steady_clock::now();
544
FindTaskScheduler();
545
546
if (ts_ptr_candidates.empty())
547
{
548
if (retries_left-- <= 0)
549
NotifyError("rbxfpsunlocker Error", "Unable to find TaskScheduler! This is probably due to a Roblox update-- watch the github for any patches or a fix.");
550
return;
551
}
552
else
553
{
554
const auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - start_time).count();
555
printf("[%u] Found TaskScheduler candidates in %lldms\n", process.id, elapsed);
556
}
557
}
558
559
if (!ts_ptr_candidates.empty() && !fd_ptr)
560
{
561
try
562
{
563
size_t fail_count = 0;
564
565
for (const void *ts_ptr : ts_ptr_candidates)
566
{
567
if (auto scheduler = (const uint8_t *)(ProcUtil::ReadPointer(process.Handle(), ts_ptr)))
568
{
569
printf("[%u] Potential task scheduler: %p\n", process.id, scheduler);
570
571
size_t delay_offset = FindTaskSchedulerFrameDelayOffset(scheduler);
572
if (delay_offset == -1)
573
{
574
fail_count++;
575
continue; // try next
576
}
577
578
// winner
579
printf("[%u] Frame delay offset: %zu (0x%zx)\n", process.id, delay_offset, delay_offset);
580
fd_ptr = scheduler + delay_offset;
581
582
// first write
583
SetFPSCapInMemory(Settings::FPSCap);
584
return;
585
}
586
else
587
{
588
printf("[%u] *ts_ptr (%p) == nullptr\n", process.id, ts_ptr);
589
}
590
}
591
592
if (fail_count > 0)
593
{
594
// one or more candidates had valid pointers with no frame delay variable
595
if (retries_left-- <= 0)
596
NotifyError("rbxfpsunlocker Error", "Variable scan failed! Make sure your framerate is at ~60.0 FPS (press Shift+F5 in-game) before using Roblox FPS Unlocker.");
597
}
598
}
599
catch (ProcUtil::WindowsException& e)
600
{
601
printf("[%u] RobloxProcess::Tick failed: %s (%d)\n", process.id, e.what(), e.GetLastError());
602
if (retries_left-- <= 0)
603
NotifyError("rbxfpsunlocker Error", "An exception occurred while performing the variable scan.");
604
}
605
}
606
}
607
608
void OnEvent(uint32_t ev_flags)
609
{
610
// todo: redesign the event/flag writing/blah code (again) (maybe)
611
// dealing with initiation, one or multiple setting changes, unlocks, and exits on both attached processes and registry instances whilst
612
// making sure it prompts the user correctly is a fun task
613
// 1. any user input that triggers a flag write (loading settings from disk, setting change in UI, app init) should notify the user if the version being written to has a process running AND the flag values have changed
614
// 2. ideally, a flag file is not read or written to more than once per version per event
615
// 3. flags should only be reverted if "Unlock Client/Studio" is toggled off or the user exits the app
616
// 4. note UnlockMethod setting, which can change whether a process needs to be attached, detached, or reattached as well as trigger flag changes
617
618
// a separate todo:
619
// there are obvious problems with attaching to Hyperion processes
620
// right now RFU has an okay success rate but it has a lot to do with attach timing and changes with OS (win 10 vs win 11)
621
// reattaches via settings changes or relaunches of RFU will lead to crashes as well
622
// consider removing the ability to attach to Hyperion clients, or hide it from the UI
623
// or... double down and implement more special code to circumvent Hyperion (fun, but not a good idea)
624
// or... convince Roblox to add fps cap and vsync settings to the graphics menu :-D
625
626
if (ev_flags & RFU::Event::CLOSE_MASK)
627
{
628
printf("[%u] Closing instance (IsRegistryInstance=%u, RevertFlagsOnClose=%u)\n", process.id, IsRegistryInstance(), Settings::RevertFlagsOnClose);
629
630
if (ev_flags != RFU::Event::PROCESS_DIED)
631
{
632
// revert flags if process isn't dead
633
if (Settings::RevertFlagsOnClose)
634
{
635
RobloxFFlags(version_folder).set_target_fps(std::nullopt).set_alt_enter_flag(std::nullopt).apply(ev_flags != RFU::Event::APP_EXIT && !IsRegistryInstance());
636
}
637
SetFPSCapInMemory(60.0);
638
}
639
}
640
else if (ev_flags & RFU::Event::SETTINGS_MASK)
641
{
642
RobloxFFlags fflags(version_folder);
643
644
if (ev_flags & RFU::Event::UNLOCK_METHOD)
645
{
646
if (ShouldUseFlagsFile())
647
{
648
printf("[%u] Using FlagsFile mode\n", process.id);
649
use_flags_file = true;
650
fflags.set_target_fps(Settings::FPSCap);
651
}
652
else
653
{
654
printf("[%u] Using MemoryWrite mode\n", process.id);
655
use_flags_file = false;
656
fflags.set_target_fps(std::nullopt);
657
}
658
}
659
660
if (ev_flags & RFU::Event::FPS_CAP)
661
{
662
if (use_flags_file) fflags.set_target_fps(Settings::FPSCap);
663
else SetFPSCapInMemory(Settings::FPSCap);
664
}
665
666
if (ev_flags & RFU::Event::ALT_ENTER)
667
{
668
fflags.set_alt_enter_flag(Settings::AltEnterFix ? alt_enter_t{false} : std::nullopt);
669
}
670
671
// write flags to disk. only prompt for live instances
672
fflags.apply(!IsRegistryInstance());
673
}
674
}
675
};
676
677
std::filesystem::path GetCurrentClientVersionPath()
678
{
679
HKEY key;
680
if (RegOpenKeyEx(HKEY_CURRENT_USER, "SOFTWARE\\ROBLOX Corporation\\Environments\\roblox-player", NULL, KEY_READ, &key) == ERROR_SUCCESS)
681
{
682
char version[64]{};
683
DWORD length = sizeof(version) - 1;
684
if (RegQueryValueEx(key, "version", NULL, NULL, (LPBYTE)version, &length) == ERROR_SUCCESS)
685
{
686
return GetLocalAppDataPath() / "Roblox" / "Versions" / version / "RobloxPlayerBeta.exe";
687
}
688
}
689
return {};
690
}
691
692
std::filesystem::path GetCurrentStudioVersionPath()
693
{
694
HKEY key;
695
if (RegOpenKeyEx(HKEY_CURRENT_USER, "SOFTWARE\\ROBLOX Corporation\\Environments\\roblox-studio", NULL, KEY_READ, &key) == ERROR_SUCCESS)
696
{
697
char version[64]{};
698
DWORD length = sizeof(version) - 1;
699
if (RegQueryValueEx(key, "version", NULL, NULL, (LPBYTE)version, &length) == ERROR_SUCCESS)
700
{
701
return GetLocalAppDataPath() / "Roblox" / "Versions" / version / "RobloxStudioBeta.exe";
702
}
703
}
704
return {};
705
}
706
707
using attached_instances_map_t = std::unordered_map<DWORD, RobloxInstance>;
708
709
struct RFUContext
710
{
711
attached_instances_map_t attached_instances{};
712
bool unlocking_client = false;
713
bool unlocking_studio = false;
714
};
715
716
std::tuple<std::unique_lock<std::mutex>, RFUContext *> AcquireRFUContext()
717
{
718
static std::mutex mutex;
719
static RFUContext context;
720
721
std::unique_lock lock(mutex);
722
return { std::move(lock), &context };
723
}
724
725
void RFU::OnEvent(uint32_t ev)
726
{
727
auto [lock, context] = AcquireRFUContext();
728
729
// update live processes
730
for (auto &it : context->attached_instances)
731
{
732
it.second.OnEvent(ev);
733
}
734
735
// update registry
736
if (context->unlocking_client)
737
{
738
auto client_path = GetCurrentClientVersionPath();
739
if (!client_path.empty())
740
{
741
RobloxInstance(client_path, true).OnEvent(ev);
742
}
743
}
744
745
if (context->unlocking_studio)
746
{
747
auto studio_path = GetCurrentStudioVersionPath();
748
if (!studio_path.empty())
749
{
750
RobloxInstance(studio_path, false).OnEvent(ev);
751
}
752
}
753
}
754
755
void pause()
756
{
757
printf("Press enter to continue . . .");
758
getchar();
759
}
760
761
DWORD WINAPI WatchThread(LPVOID)
762
{
763
printf("Watch thread started\n");
764
765
while (1)
766
{
767
{
768
auto [lock, context] = AcquireRFUContext();
769
auto processes = GetRobloxProcesses(false, true, true);
770
771
for (auto &process : processes)
772
{
773
auto id = process.id;
774
if (context->attached_instances.find(id) == context->attached_instances.end())
775
{
776
// we found a process that isn't in our attached list
777
if (process.ShouldAttach())
778
{
779
assert(!process.IsOpen());
780
781
RobloxInstance roblox_process;
782
roblox_process.AttachProcess(process, 5);
783
context->attached_instances[id] = std::move(roblox_process);
784
785
printf("New size: %zu\n", context->attached_instances.size());
786
}
787
}
788
}
789
790
for (auto it = context->attached_instances.begin(); it != context->attached_instances.end();)
791
{
792
auto &handle = it->second.GetHandle();
793
794
if (std::find_if(processes.begin(), processes.end(), [&it](const RobloxProcessHandle &x) { return x.id == it->first; }) == processes.end())
795
{
796
// it's gone
797
if (handle.IsOpen())
798
{
799
DWORD code = 0;
800
GetExitCodeProcess(handle.Handle(), &code);
801
printf("Purging dead process %p (pid %d, code %X)\n", handle.Handle(), handle.id, code);
802
}
803
else
804
{
805
printf("Purging dead process (pid %d)\n", handle.id);
806
}
807
808
// close
809
it->second.OnEvent(RFU::Event::PROCESS_DIED);
810
811
it = context->attached_instances.erase(it);
812
printf("New size: %zu\n", context->attached_instances.size());
813
}
814
else if (!handle.ShouldAttach())
815
{
816
// settings changed
817
printf("Closing process (pid %d)\n", handle.id);
818
it->second.OnEvent(RFU::Event::CLOSE);
819
it = context->attached_instances.erase(it);
820
printf("New size: %zu\n", context->attached_instances.size());
821
}
822
else
823
{
824
it->second.MemoryWriteTick();
825
it++;
826
}
827
}
828
829
if (context->unlocking_client != Settings::UnlockClient)
830
{
831
auto path = GetCurrentClientVersionPath();
832
if (!path.empty())
833
{
834
RobloxInstance(path, true).OnEvent(Settings::UnlockClient ? RFU::Event::SETTINGS_MASK : RFU::Event::CLOSE);
835
}
836
837
context->unlocking_client = Settings::UnlockClient;
838
}
839
840
if (context->unlocking_studio != Settings::UnlockStudio)
841
{
842
auto path = GetCurrentStudioVersionPath();
843
if (!path.empty())
844
{
845
RobloxInstance(path, false).OnEvent(Settings::UnlockStudio ? RFU::Event::SETTINGS_MASK : RFU::Event::CLOSE);
846
}
847
848
context->unlocking_studio = Settings::UnlockStudio;
849
}
850
851
UI::AttachedProcessesCount = context->attached_instances.size();
852
}
853
854
Sleep(2000);
855
}
856
857
return 0;
858
}
859
860
bool CheckRunning()
861
{
862
SingletonMutex = CreateMutexA(NULL, FALSE, "RFUMutex");
863
864
if (!SingletonMutex)
865
{
866
MessageBoxA(NULL, "Unable to create mutex", "Error", MB_OK);
867
return false;
868
}
869
870
return GetLastError() == ERROR_ALREADY_EXISTS;
871
}
872
873
int CALLBACK WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
874
{
875
if (!Settings::Init())
876
{
877
char buffer[64];
878
sprintf_s(buffer, "Unable to initiate settings\nGetLastError() = %X", GetLastError());
879
MessageBoxA(NULL, buffer, "Error", MB_OK);
880
return 0;
881
}
882
883
UI::IsConsoleOnly = strstr(lpCmdLine, "--console") != nullptr;
884
885
if (UI::IsConsoleOnly)
886
{
887
UI::ToggleConsole();
888
889
printf("Waiting for Roblox...\n");
890
891
RobloxProcessHandle process;
892
RobloxInstance attacher{};
893
894
do
895
{
896
Sleep(100);
897
process = GetRobloxProcess();
898
}
899
while (!process.IsValid());
900
901
printf("Found Roblox...\n");
902
printf("Attaching...\n");
903
904
if (!attacher.AttachProcess(process, 0))
905
{
906
printf("\nERROR: unable to attach to process\n");
907
pause();
908
return 0;
909
}
910
911
printf("\nSuccess! The injector will close in 3 seconds...\n");
912
913
Sleep(3000);
914
915
return 0;
916
}
917
else
918
{
919
if (CheckRunning())
920
{
921
MessageBoxA(NULL, "Roblox FPS Unlocker is already running", "Error", MB_OK);
922
}
923
else
924
{
925
if (!Settings::QuickStart)
926
UI::ToggleConsole();
927
else
928
UI::CreateHiddenConsole();
929
930
if (Settings::CheckForUpdates)
931
{
932
printf("Checking for updates...\n");
933
if (RFU::CheckForUpdates()) return 0;
934
}
935
936
if (!Settings::QuickStart)
937
{
938
printf("Minimizing to system tray in 2 seconds...\n");
939
Sleep(2000);
940
#ifdef NDEBUG
941
UI::ToggleConsole();
942
#endif
943
}
944
945
return UI::Start(hInstance, WatchThread);
946
}
947
}
948
}
949
950