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
using System.Diagnostics;
using System.IO;
using System.Management;
using System.Text.Json;
using Microsoft.Win32;
namespace AdamsToolkit.Core;
/// <summary>Resultado de uma verificação do diagnóstico. Level: ok | warn | bad.</summary>
public record DiagItem(string Level, string Title, string Detail)
{
public string Icon => Level switch { "ok" => "✅", "warn" => "⚠️", _ => "⛔" };
}
/// <summary>
/// Boost de FPS "check-safe" para o FiveM.
///
/// REGRA DESTE MÓDULO: só altera definições de desempenho (energia, prioridades,
/// preferências por-executável) — todas reversíveis e todas visíveis no registo.
/// NUNCA apaga rasto do sistema (Prefetch, event logs, USN journal, histórico),
/// nunca desliga o Defender nem serviços de diagnóstico: um PC com essas coisas
/// apagadas é exatamente o que uma verificação de PC procura. Tweaks novos que
/// apaguem provas NÃO entram aqui.
///
/// Mesmo modelo do <see cref="NetworkOptimizer"/>: backup JSON do valor original,
/// boost por sessão de Windows (reinicia o PC → reverte sozinho).
/// </summary>
public static class GameBoost
{
private static readonly string StateFile = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AdamsToolkit", "gameboost.json");
private const string Absent = "<ausente>";
private const string NoPerm = "sem permissão — reinicia a app como administrador";
private class OptState
{
public Dictionary<string, Dictionary<string, string>> Backup { get; set; } = new();
public HashSet<string> Applied { get; set; } = new();
public long BootStamp { get; set; }
}
private static OptState _state = Load();
private static OptState Load()
{
try
{
if (File.Exists(StateFile))
return JsonSerializer.Deserialize<OptState>(File.ReadAllText(StateFile)) ?? new OptState();
}
catch { }
return new OptState();
}
private static void Save()
{
try
{
Directory.CreateDirectory(Path.GetDirectoryName(StateFile)!);
File.WriteAllText(StateFile, JsonSerializer.Serialize(_state));
}
catch { }
}
public static bool IsApplied(string id) => _state.Applied.Contains(id);
public static bool AnyApplied => _state.Applied.Count > 0;
private static long CurrentBootStamp()
{
var boot = DateTimeOffset.UtcNow - TimeSpan.FromMilliseconds(Environment.TickCount64);
return boot.ToUnixTimeSeconds() / 60;
}
/// <summary>Igual ao NetworkOptimizer: boost de outro boot do Windows → reverte sozinho.</summary>
public static Task<bool> StartupCheck { get; private set; } = Task.FromResult(false);
public static void BeginStartupCheck() => StartupCheck = Task.Run(async () =>
{
if (!AnyApplied) return false;
if (_state.BootStamp == CurrentBootStamp()) return false;
await RevertAllAsync();
return true;
});
private static void BackupValue(string tweakId, string key, string value)
{
if (!_state.Backup.TryGetValue(tweakId, out var d))
_state.Backup[tweakId] = d = new();
d.TryAdd(key, value);
}
private static Dictionary<string, string> GetBackup(string tweakId) =>
_state.Backup.TryGetValue(tweakId, out var d) ? d : new();
// ---------- registry (backup genérico "HIVE|caminho|valor") ----------
private static RegistryKey RootOf(string hive) =>
hive == "HKLM" ? Registry.LocalMachine : Registry.CurrentUser;
private static string? WriteValue(string tweakId, string hive, string path, string name,
object value, RegistryValueKind kind)
{
try
{
using var k = RootOf(hive).CreateSubKey(path);
if (k == null) return $"não consegui abrir {hive}\\{path}";
var cur = k.GetValue(name);
BackupValue(tweakId, $"{hive}|{path}|{name}",
cur == null ? Absent : (kind == RegistryValueKind.DWord ? $"d:{cur}" : $"s:{cur}"));
k.SetValue(name, value, kind);
return null;
}
catch (UnauthorizedAccessException) { return NoPerm; }
catch (Exception ex) { return ex.Message; }
}
private static void RestoreValues(string tweakId)
{
foreach (var (key, val) in GetBackup(tweakId))
{
var parts = key.Split('|');
if (parts.Length != 3) continue;
try
{
using var k = RootOf(parts[0]).OpenSubKey(parts[1], writable: true);
if (k == null) continue;
if (val == Absent) k.DeleteValue(parts[2], throwOnMissingValue: false);
else if (val.StartsWith("d:") && int.TryParse(val[2..], out var iv))
k.SetValue(parts[2], iv, RegistryValueKind.DWord);
else if (val.StartsWith("s:"))
k.SetValue(parts[2], val[2..], RegistryValueKind.String);
}
catch { }
}
}
// ---------- executáveis do FiveM ----------
/// <summary>Launcher + processo(s) do jogo (FiveM_bXXXX_GTAProcess.exe) desta instalação.</summary>
private static List<string> FiveMExecutables()
{
var list = new List<string>();
try
{
var exe = FiveMLocator.ExePath;
if (exe != null) list.Add(exe);
var app = FiveMLocator.AppDataDir;
if (app != null && Directory.Exists(app))
{
foreach (var f in Directory.EnumerateFiles(app, "FiveM*GTAProcess.exe", SearchOption.TopDirectoryOnly))
list.Add(f);
}
}
catch { }
return list.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
}
private const string NoFiveM = "não encontrei o FiveM neste PC — abre-o uma vez e tenta de novo";
// ---------- catálogo ----------
public static readonly IReadOnlyList<NetTweak> Tweaks = new List<NetTweak>
{
// ===== SEGUROS =====
new()
{
Id = "accesskeys", Name = "Atalhos de acessibilidade do teclado desligados",
Description = "Desliga os atalhos das Teclas Fixas (Shift 5x), Teclas de Filtro (Shift 8s) e Teclas de Alternância (Num Lock 5s). No meio de um strafe/spam de tecla o Windows abre a janela deles e rouba o foco ao jogo — clássico. Não precisa de admin.",
Apply = () =>
{
// valores REG_SZ do Windows: default 510/126/62 → tirar o bit 4 (hotkey ativa) = 506/122/58
var err = WriteValue("accesskeys", "HKCU", @"Control Panel\Accessibility\StickyKeys", "Flags", "506", RegistryValueKind.String);
if (err != null) return Task.FromResult<string?>(err);
WriteValue("accesskeys", "HKCU", @"Control Panel\Accessibility\Keyboard Response", "Flags", "122", RegistryValueKind.String);
WriteValue("accesskeys", "HKCU", @"Control Panel\Accessibility\ToggleKeys", "Flags", "58", RegistryValueKind.String);
return Task.FromResult<string?>(null);
},
Revert = () => { RestoreValues("accesskeys"); return Task.FromResult<string?>(null); },
},
new()
{
Id = "gamedvr", Name = "Gravação em segundo plano desligada",
Description = "Desliga o Game DVR/Xbox Game Bar, que grava o jogo em fundo mesmo sem tu saberes — é a causa clássica de engasgos de meio segundo. Podes continuar a gravar com o que quiseres (ShadowPlay, OBS).",
Apply = () =>
{
var err = WriteValue("gamedvr", "HKCU", @"System\GameConfigStore", "GameDVR_Enabled", 0, RegistryValueKind.DWord);
if (err != null) return Task.FromResult<string?>(err);
WriteValue("gamedvr", "HKCU", @"Software\Microsoft\Windows\CurrentVersion\GameDVR", "AppCaptureEnabled", 0, RegistryValueKind.DWord);
// política de máquina: bónus, só entra com admin — falhar aqui não estraga o tweak
WriteValue("gamedvr", "HKLM", @"SOFTWARE\Policies\Microsoft\Windows\GameDVR", "AllowGameDVR", 0, RegistryValueKind.DWord);
return Task.FromResult<string?>(null);
},
Revert = () => { RestoreValues("gamedvr"); return Task.FromResult<string?>(null); },
},
new()
{
Id = "mmcss", Name = "Jogos com prioridade no CPU e na GPU",
Description = "Diz ao Windows para tratar jogos como tarefa prioritária (perfil MMCSS \"Games\"). Ajuda quando tens Discord, browser e mais coisas abertas ao mesmo tempo. Precisa de admin.",
Apply = () =>
{
const string path = @"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Multimedia\SystemProfile\Tasks\Games";
var err = WriteValue("mmcss", "HKLM", path, "GPU Priority", 8, RegistryValueKind.DWord);
if (err != null) return Task.FromResult<string?>(err);
WriteValue("mmcss", "HKLM", path, "Priority", 6, RegistryValueKind.DWord);
WriteValue("mmcss", "HKLM", path, "Scheduling Category", "High", RegistryValueKind.String);
WriteValue("mmcss", "HKLM", path, "SFIO Priority", "High", RegistryValueKind.String);
return Task.FromResult<string?>(null);
},
Revert = () => { RestoreValues("mmcss"); return Task.FromResult<string?>(null); },
},
new()
{
Id = "gpupref", Name = "FiveM sempre na placa gráfica dedicada",
Description = "Marca o FiveM como \"Alto desempenho\" nas definições gráficas do Windows. Em portáteis e PCs com gráficos integrados evita que o jogo corra na placa fraca. Não precisa de admin.",
Apply = () =>
{
var exes = FiveMExecutables();
if (exes.Count == 0) return Task.FromResult<string?>(NoFiveM);
foreach (var exe in exes)
{
var err = WriteValue("gpupref", "HKCU",
@"Software\Microsoft\DirectX\UserGpuPreferences", exe, "GpuPreference=2;", RegistryValueKind.String);
if (err != null) return Task.FromResult<string?>(err);
}
return Task.FromResult<string?>(null);
},
Revert = () => { RestoreValues("gpupref"); return Task.FromResult<string?>(null); },
},
new()
{
Id = "fso", Name = "Ecrã inteiro sem a camada do Windows",
Description = "Desativa as \"otimizações de ecrã inteiro\" no FiveM — em muitos PCs tira meio frame de atraso e estabiliza o tempo de frame. Em troca, o alt-tab fica um pouco mais lento. Não precisa de admin.",
Apply = () =>
{
var exes = FiveMExecutables();
if (exes.Count == 0) return Task.FromResult<string?>(NoFiveM);
foreach (var exe in exes)
{
var err = WriteValue("fso", "HKCU",
@"Software\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Layers", exe,
"~ DISABLEDXMAXIMIZEDWINDOWEDMODE", RegistryValueKind.String);
if (err != null) return Task.FromResult<string?>(err);
}
return Task.FromResult<string?>(null);
},
Revert = () => { RestoreValues("fso"); return Task.FromResult<string?>(null); },
},
new()
{
Id = "prio", Name = "FiveM com prioridade alta (enquanto a app está aberta)", SessionOnly = true,
Description = "Põe o processo do FiveM em prioridade alta assim que o abres, e vai reaplicando. Dura só enquanto o Adams Toolkit estiver aberto (fechar para a bandeja mantém).",
Apply = () => Task.FromResult(StartPriorityWatch()),
Revert = () => Task.FromResult(StopPriorityWatch()),
},
// Nota: a Integridade de Memória / Core Isolation NÃO se mexe aqui (decisão
// do dono da app) — é proteção do Windows e fica como o utilizador a tem.
};
// ---------- apply/revert ----------
public static async Task<string?> ApplyAsync(NetTweak t)
{
var err = await t.Apply();
if (err == null && !t.SessionOnly)
{
_state.Applied.Add(t.Id);
_state.BootStamp = CurrentBootStamp();
Save();
}
return err;
}
public static async Task<string?> RevertAsync(NetTweak t)
{
var err = await t.Revert();
if (err == null) { _state.Applied.Remove(t.Id); _state.Backup.Remove(t.Id); Save(); }
return err;
}
public static async Task<List<(string name, string error)>> RevertAllAsync()
{
var errors = new List<(string, string)>();
foreach (var t in Tweaks.Reverse())
{
if (!t.SessionOnly && !IsApplied(t.Id)) continue;
var err = await RevertAsync(t);
if (err != null) errors.Add((t.Name, err));
}
return errors;
}
// ---------- prioridade do processo (sessão) ----------
private static CancellationTokenSource? _prioCts;
private static IEnumerable<Process> GameProcesses()
{
Process[] all;
try { all = Process.GetProcesses(); }
catch { yield break; }
foreach (var p in all)
{
var match = false;
try { match = p.ProcessName.StartsWith("FiveM", StringComparison.OrdinalIgnoreCase); }
catch { }
if (match) yield return p;
else p.Dispose();
}
}
private static string? StartPriorityWatch()
{
StopPriorityWatch();
var cts = new CancellationTokenSource();
_prioCts = cts;
// ciclo leve: só corre enquanto o tweak está ligado (regra "featherweight")
_ = Task.Run(async () =>
{
while (!cts.IsCancellationRequested)
{
foreach (var p in GameProcesses())
{
try
{
if (p.PriorityClass != ProcessPriorityClass.High)
p.PriorityClass = ProcessPriorityClass.High;
}
catch { }
finally { p.Dispose(); }
}
try { await Task.Delay(4000, cts.Token); }
catch (OperationCanceledException) { return; }
}
}, cts.Token);
return null;
}
private static string? StopPriorityWatch()
{
_prioCts?.Cancel();
_prioCts = null;
foreach (var p in GameProcesses())
{
try
{
if (p.PriorityClass == ProcessPriorityClass.High)
p.PriorityClass = ProcessPriorityClass.Normal;
}
catch { }
finally { p.Dispose(); }
}
return null;
}
// ---------- diagnóstico ----------
private static async Task<string> RunPsAsync(string script)
{
try
{
var psi = new ProcessStartInfo("powershell.exe",
$"-NoProfile -ExecutionPolicy Bypass -Command \"{script.Replace("\"", "\\\"")}\"")
{
CreateNoWindow = true, UseShellExecute = false,
RedirectStandardOutput = true, RedirectStandardError = true,
};
using var p = Process.Start(psi);
if (p == null) return "";
var output = await p.StandardOutput.ReadToEndAsync();
await p.WaitForExitAsync();
return output.Trim();
}
catch { return ""; }
}
/// <summary>
/// Procura as causas reais de quebras de FPS que não se resolvem com tweaks:
/// pouca RAM/ficheiro de paginação, FiveM em disco lento, cache do FiveM
/// gigante, overlays a comer frames.
/// Só lê — não altera nada.
/// </summary>
public static async Task<List<DiagItem>> DiagnoseAsync()
{
var items = new List<DiagItem>();
// ----- RAM -----
double ramGb = 0;
try
{
using var s = new ManagementObjectSearcher("SELECT TotalPhysicalMemory FROM Win32_ComputerSystem");
foreach (ManagementObject mo in s.Get())
ramGb = Convert.ToDouble(mo["TotalPhysicalMemory"]) / 1073741824.0;
}
catch { }
if (ramGb > 0)
{
items.Add(ramGb < 12
? new DiagItem("bad", $"Memória RAM: {ramGb:0} GB",
"Servidores pesados de FiveM pedem 16 GB. Com esta RAM as quebras vêm de o Windows andar a despejar memória para o disco — é aqui que ganhas mais.")
: ramGb < 16
? new DiagItem("warn", $"Memória RAM: {ramGb:0} GB",
"Chega para servidores normais, mas nos mais carregados vais notar engasgos. Garante que o ficheiro de paginação está grande.")
: new DiagItem("ok", $"Memória RAM: {ramGb:0} GB", "Suficiente para qualquer servidor."));
}
// ----- ficheiro de paginação -----
try
{
var managed = false;
using (var k = Registry.LocalMachine.OpenSubKey(
@"SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management"))
{
if (k?.GetValue("PagingFiles") is string[] pf)
managed = pf.Any(l => l.TrimEnd().EndsWith("0 0"));
}
double pageMb = 0;
using (var s = new ManagementObjectSearcher("SELECT AllocatedBaseSize FROM Win32_PageFileUsage"))
foreach (ManagementObject mo in s.Get())
pageMb += Convert.ToDouble(mo["AllocatedBaseSize"]);
if (managed)
items.Add(new DiagItem("ok", "Ficheiro de paginação: gerido pelo Windows",
$"Atualmente {pageMb / 1024.0:0.#} GB e cresce sozinho — é o que queres para o FiveM."));
else if (pageMb < 8192)
items.Add(new DiagItem("bad", $"Ficheiro de paginação: {pageMb / 1024.0:0.#} GB — pequeno",
"O FiveM chega a pedir 20–30 GB de memória em servidores grandes. Fixo e pequeno = crashes e quebras. Põe em \"gerido pelo sistema\" ou fixa 16384–32768 MB no SSD."));
else
items.Add(new DiagItem("ok", $"Ficheiro de paginação: {pageMb / 1024.0:0.#} GB", "Tamanho suficiente."));
}
catch { }
// ----- disco onde o FiveM está -----
try
{
var root = FiveMLocator.RootDir;
if (root == null)
{
items.Add(new DiagItem("warn", "FiveM não encontrado",
"Não localizei a instalação do FiveM — abre-o uma vez e volta a analisar."));
}
else
{
var letter = Path.GetPathRoot(root)?.TrimEnd('\\', ':').Replace(":", "") ?? "C";
var media = await RunPsAsync(
$"$p=Get-Partition -DriveLetter {letter} -ErrorAction SilentlyContinue; " +
"if($p){ (Get-PhysicalDisk -ErrorAction SilentlyContinue | " +
"Where-Object DeviceId -eq $p.DiskNumber).MediaType }");
media = media.Split('\n').FirstOrDefault()?.Trim() ?? "";
if (media.Equals("HDD", StringComparison.OrdinalIgnoreCase))
items.Add(new DiagItem("bad", $"FiveM está num disco mecânico ({letter}:)",
"É a maior causa de quebras ao entrar em zonas novas: o jogo carrega modelos enquanto jogas. Mudar o FiveM para SSD resolve mais do que qualquer tweak."));
else if (media.Length > 0 && !media.Equals("Unspecified", StringComparison.OrdinalIgnoreCase))
items.Add(new DiagItem("ok", $"FiveM em {media} ({letter}:)", "Disco rápido — o streaming do jogo não é o problema."));
// espaço livre
try
{
var di = new DriveInfo(letter);
var freeGb = di.AvailableFreeSpace / 1073741824.0;
var pct = di.TotalSize > 0 ? di.AvailableFreeSpace * 100.0 / di.TotalSize : 100;
if (freeGb < 20 || pct < 10)
items.Add(new DiagItem("warn", $"Só {freeGb:0} GB livres em {letter}:",
"Disco quase cheio trava a cache do jogo e o ficheiro de paginação. Liberta espaço."));
}
catch { }
// cache do FiveM
try
{
var app = FiveMLocator.AppDataDir;
var data = app == null ? null : Path.Combine(app, "data");
if (data != null && Directory.Exists(data))
{
long size = 0;
foreach (var d in Directory.GetDirectories(data))
{
if (Path.GetFileName(d).Equals("game-storage", StringComparison.OrdinalIgnoreCase)) continue;
try
{
foreach (var f in Directory.EnumerateFiles(d, "*", SearchOption.AllDirectories))
try { size += new FileInfo(f).Length; } catch { }
}
catch { }
}
var gb = size / 1073741824.0;
if (gb > 8)
items.Add(new DiagItem("warn", $"Cache do FiveM: {gb:0.#} GB",
"Cache muito grande e antiga dá texturas partidas e engasgos. Limpa na aba FiveM (Manutenção) — os teus dados de jogo ficam."));
else
items.Add(new DiagItem("ok", $"Cache do FiveM: {gb:0.#} GB", "Tamanho normal."));
}
}
catch { }
}
}
catch { }
// ----- overlays / programas que roubam frames -----
try
{
var known = new (string proc, string label)[]
{
("RTSS", "RivaTuner (RTSS)"),
("MSIAfterburner", "MSI Afterburner"),
("wallpaper32", "Wallpaper Engine"),
("wallpaper64", "Wallpaper Engine"),
("Rainmeter", "Rainmeter"),
("obs64", "OBS a gravar/transmitir"),
("GameBar", "Xbox Game Bar"),
("NVIDIA Overlay", "Overlay da NVIDIA"),
("GameOverlayUI", "Overlay do Steam"),
};
var running = new List<string>();
foreach (var p in Process.GetProcesses())
{
try
{
var hit = known.FirstOrDefault(k => p.ProcessName.Equals(k.proc, StringComparison.OrdinalIgnoreCase));
if (hit.label != null && !running.Contains(hit.label)) running.Add(hit.label);
}
catch { }
finally { p.Dispose(); }
}
items.Add(running.Count > 0
? new DiagItem("warn", "Programas a roubar frames: " + string.Join(", ", running),
"Cada overlay a mais são frames a menos e picos de tempo de frame. Deixa só o que usas mesmo enquanto jogas.")
: new DiagItem("ok", "Sem overlays pesados a correr", "Nada a competir com o jogo."));
}
catch { }
return items;
}
}