Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fce32eab52 | ||
|
|
a70f868120 | ||
|
|
9bc7ef4177 | ||
|
|
6cb70e87af | ||
|
|
393588d8d0 | ||
|
|
f22040d8e3 | ||
|
|
937d775f63 | ||
|
|
709b17d729 | ||
|
|
9a18ea1576 | ||
|
|
96c4fd80a4 | ||
|
|
47b647784e | ||
|
|
3cf1d708a8 | ||
|
|
e8df86c250 | ||
|
|
40bb3e2cc2 | ||
|
|
7c5e64774c | ||
|
|
a02293a518 | ||
|
|
1f296c3880 | ||
|
|
054c86e2d2 | ||
|
|
eed72cdec9
|
||
|
|
749bf9a7a7 | ||
|
|
f1f71e7533 | ||
|
|
27eeb48044 | ||
|
|
60f1868334 | ||
|
|
44e5997847
|
||
|
|
0384eee42c | ||
|
|
89a320fd0f | ||
|
|
58fd6b7e9c | ||
|
|
53fd0112c2 | ||
|
|
23fd4b12f9 | ||
|
|
e0a5821431 | ||
|
|
50b5388f8a | ||
|
|
d185ab1a57 | ||
|
|
4205a332a7 | ||
|
|
ade95b5c65 | ||
|
|
54985d350a | ||
|
|
97e1446996 | ||
|
|
746b36dfc0
|
||
|
|
200b92cd48
|
||
|
|
fb7e1f7864
|
||
|
|
b0afa9de1a
|
||
|
|
890ae36590
|
||
|
|
89eec435ff
|
||
|
|
2611dbfd3e
|
||
|
|
eafc9e1d10
|
||
|
|
0e92c9908d
|
||
|
|
8ffc5efb8d
|
||
|
|
bd8f70ed5f
|
||
|
|
9227533303
|
||
|
|
4b522d9b3f
|
||
|
|
d393bbfc37
|
||
|
|
7190dc05fe
|
||
|
|
23b6c879ae
|
||
|
|
660a478fbb
|
||
|
|
28486174f2
|
||
|
|
0f65c76055
|
||
|
|
8943e61f3a
|
||
|
|
ed0a828fd4
|
||
|
|
96ef494443
|
||
|
|
9b5340a6c5
|
||
|
|
74132ec322
|
||
|
|
a58606c91d
|
||
|
|
47adf42a48
|
||
|
|
0fb587027c
|
||
|
|
0424b7b114
|
||
|
|
e89c3acc06
|
||
|
|
ab64c29935
|
||
|
|
e216445c7f
|
||
|
|
aeb99992bb
|
||
|
|
8ab6e14560
|
||
|
|
b9ebdfdb5c
|
||
|
|
2804812691
|
||
|
|
606fb7e191
|
||
|
|
e2a779b7f5
|
||
|
|
a8b04bb72b
|
||
|
|
c34cf187d5
|
||
|
|
f07421e75a
|
||
|
|
8b4a455dc1
|
||
|
|
a1300f8d76
|
||
|
|
9e623219a5
|
||
|
|
09e19b6e16
|
||
|
|
4498c45965
|
||
|
|
af8a5b1746
|
||
|
|
560d9659f4
|
||
|
|
bf086d0442
|
||
|
|
a6ea8ad8e2
|
||
|
|
4c00d1846b
|
||
|
|
e5e335f4f3
|
||
|
|
3ab5987b67
|
||
|
|
2030796337
|
||
|
|
2edb4f492a
|
||
|
|
114292d912
|
||
|
|
8adc1f49ad
|
||
|
|
bb27334f57
|
||
|
|
1e13905885
|
||
|
|
8fc73b2992
|
||
|
|
43b72995dd
|
||
|
|
d3473d46e3
|
||
|
|
1363f5b125
|
||
|
|
d15b8070ec | ||
|
|
efccd6ed50 | ||
|
|
5c73ee7bd5 | ||
|
|
445b63fa4c | ||
|
|
941868dcc8 | ||
|
|
762a3b9b26 | ||
|
|
0cb211c103 | ||
|
|
90b32c5c5a | ||
|
|
71c3c69ea2 | ||
|
|
b553e418f5 | ||
|
|
ab026c0bc6 | ||
|
|
32e21f58a1 | ||
|
|
b0c339cc5f | ||
|
|
c2584b5d01 | ||
|
|
3322f9582f | ||
|
|
5348a1d6ee | ||
|
|
05ce68cb47 | ||
|
|
f63d24ea43 | ||
|
|
356d5f17e5 | ||
|
|
bc5e80aaca | ||
|
|
e050231692 | ||
|
|
6e33536dfe | ||
|
|
c36a41f919 | ||
|
|
660e7a874f | ||
|
|
72abda7352 | ||
|
|
ebf704c6f4 | ||
|
|
5c5e6d60a1 | ||
|
|
98a6545b27 | ||
|
|
a9a965462a | ||
|
|
81b4b26f96 | ||
|
|
06df466d24 | ||
|
|
b8c444dc5b | ||
|
|
e848ca8d79 | ||
|
|
0a831580e2 | ||
|
|
10e69eaac3 | ||
|
|
9dbfbc4419 | ||
|
|
ef0b6de6ba | ||
|
|
e7470bd694 | ||
|
|
936f683385 | ||
|
|
b3158f71fb | ||
|
|
070e477fd4 | ||
|
|
7745bba10e | ||
|
|
8d7a0d12b9 | ||
|
|
b325ebb03c | ||
|
|
38c6402a12 | ||
|
|
2d42c0c3c4 | ||
|
|
a423721aee | ||
|
|
f3436c8ddc | ||
|
|
afcf0a87c0 | ||
|
|
73d610949f | ||
|
|
7f358d9adb | ||
|
|
7caa5421f7 | ||
|
|
ecac0d54aa | ||
|
|
a84401ca33 | ||
|
|
1bcd6b2c7d | ||
|
|
1dbdc0e4cb | ||
|
|
3610e44a08 | ||
|
|
f5fd22d35c | ||
|
|
022828b802 | ||
|
|
262d2d0720 | ||
|
|
2326edf97b | ||
|
|
301be82837 | ||
|
|
0d7dde929d | ||
|
|
3ab802fd56 | ||
|
|
53030cc5f7 | ||
|
|
383224c2fa | ||
|
|
575a6a4f1d | ||
|
|
17579de277 | ||
|
|
00727b9913 | ||
|
|
1f6fc22525 | ||
|
|
27b09be76e | ||
|
|
6f15e8c348 | ||
|
|
d294aa9861 | ||
|
|
f688f1c3c4 | ||
|
|
58ec9f6567 | ||
|
|
1c9ce2a4d3 | ||
|
|
8a5d6cf38c | ||
|
|
d789b0f62d | ||
|
|
6079a72f47 | ||
|
|
914ffe46d2 | ||
|
|
935703e1ab | ||
|
|
202554e03b | ||
|
|
da2b15a0b6 | ||
|
|
ccc27b1944 | ||
|
|
10168e84d1 | ||
|
|
e72964a9f8 | ||
|
|
18052d0c78 | ||
|
|
09f5a3895b | ||
|
|
89892ef592 | ||
|
|
61de3aeee3 | ||
|
|
4ed00e6ac3 | ||
|
|
332db0ea03 | ||
|
|
64d13e3964 | ||
|
|
feb3a793d9 | ||
|
|
585f454bc0 | ||
|
|
27524e855a | ||
|
|
0257834d04 | ||
|
|
64e94bf790 | ||
|
|
668a6eb38b | ||
|
|
0de79769d2 | ||
|
|
b79c5f62b3 |
@@ -1,2 +1,3 @@
|
||||
/target
|
||||
.vscode
|
||||
.idea
|
||||
Generated
+1151
-2399
File diff suppressed because it is too large
Load Diff
+27
-27
@@ -1,38 +1,38 @@
|
||||
[package]
|
||||
name = "gta-tools"
|
||||
version = "0.4.2"
|
||||
version = "0.12.0"
|
||||
edition = "2024"
|
||||
license = "Apache-2.0"
|
||||
authors = ["futile <git@futile.eu>"]
|
||||
description = "A toolset of convenient things for GTA V Online."
|
||||
|
||||
[dependencies]
|
||||
catppuccin-egui = { version = "5.5.0", default-features = false, features = [
|
||||
"egui31",
|
||||
anyhow = "1.0"
|
||||
eframe = { version = "0.35", default-features = false, features = [
|
||||
"glow",
|
||||
"default_fonts",
|
||||
] }
|
||||
chrono = "0.4.40"
|
||||
dirs = "6.0.0"
|
||||
eframe = "0.31.1"
|
||||
egui_extras = { version = "0.31.1", features = ["image"] }
|
||||
image = { version = "0.25.6", default-features = false, features = ["png"] }
|
||||
open = "5.3.2"
|
||||
semver = "1.0.26"
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_json = "1.0.140"
|
||||
strum = { version = "0.27.1", features = ["derive"] }
|
||||
sysinfo = { git = "https://github.com/GuillaumeGomez/sysinfo", rev = "bcbb7b2b389b8800ff457773f48b130e710458fc" }
|
||||
ureq = { version = "3.0.11", features = ["json"] }
|
||||
windows = { version = "0.61.1", features = [
|
||||
"Win32_UI_Input_KeyboardAndMouse",
|
||||
egui_extras = { version = "0.35", default-features = false, features = [
|
||||
"image",
|
||||
"svg",
|
||||
] }
|
||||
humantime = "2.3.0"
|
||||
image = { version = "0.25", default-features = false, features = ["png"] }
|
||||
log = "0.4"
|
||||
open = "5"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
strum = { version = "0.28", features = ["derive"] }
|
||||
windows = { version = "0.62", features = [
|
||||
"Win32_NetworkManagement_WindowsFirewall",
|
||||
"Win32_System_Com",
|
||||
"Win32_System_Diagnostics_ToolHelp",
|
||||
"Win32_System_Threading",
|
||||
"Win32_UI_Shell",
|
||||
"Win32_UI_Input_KeyboardAndMouse",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
"Win32_Security",
|
||||
] }
|
||||
winreg = "0.55.0"
|
||||
winreg = "0.56"
|
||||
|
||||
[build-dependencies]
|
||||
static_vcruntime = "2.0.0"
|
||||
winresource = "0.1.20"
|
||||
|
||||
[profile.release]
|
||||
strip = true
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
static_vcruntime = "3.0.0"
|
||||
tauri-winres = "0.3"
|
||||
|
||||
@@ -1,4 +1,81 @@
|
||||
# GTA Tools
|
||||
A toolset of convenient things for GTA 5 Online.
|
||||
|
||||

|
||||
A toolset of convenient things for GTA V Online.
|
||||
|
||||

|
||||

|
||||
|
||||
> [!NOTE]
|
||||
> I have been forced to move this project from [GitHub](https://github.com/elituf/gta-tools) to [here](https://gitlab.com/futile/gta-tools). Sorry for the inconvenience. From now on you may always find the project source code via the redirect at [futile.eu/gta-tools](https://futile.eu/gta-tools).
|
||||
|
||||
## Installing
|
||||
|
||||
**Option 1** — <ins>Download</ins>
|
||||
Download the latest release [here](https://gitlab.com/futile/gta-tools/-/releases/permalink/latest/downloads/gta-tools.exe) and place it somewhere convenient for you, such as Documents. You could then make a shortcut titled "GTA Tools", and pin it to taskbar or Start.
|
||||
|
||||
**Option 2** — <ins>Build from source</ins>
|
||||
You will need Rust, which you can get [here](https://rustup.rs). Then, clone this repo and navigate to it. At this point, you should probably `git checkout x.x.x`, where `x.x.x` is the latest tag. You can then run `cargo build --release`. Once you do that, you can use the binary located at `.\target\release\gta-tools.exe` in the same way as **Option 1**.
|
||||
|
||||
> [!NOTE]
|
||||
> It is recommended to always use an up-to-date version of GTA Tools from [releases](https://gitlab.com/futile/gta-tools/-/releases). You can also easily access this repository by going to the <kbd>About</kbd> page of GTA Tools and clicking the <kbd>git</kbd> button beside the version number.
|
||||
|
||||
## Guide
|
||||
|
||||
Every feature of GTA Tools works for both the Legacy and Enhanced versions of the game. GTA Tools requires administrator access.
|
||||
|
||||
#### Game
|
||||
|
||||
This section is quite simple.
|
||||
|
||||
<kbd>Launch</kbd> — Starts your game on the chosen launcher and game version.
|
||||
|
||||
<kbd>Force close game</kbd> — Kills all game processes. The button requires a second press after the first one for confirmation. This *does not* touch Rockstar Games Launcher or any other processes, only ones named `GTA5_Enhanced.exe` or `GTA5.exe`.
|
||||
|
||||
#### Session
|
||||
|
||||
This section also has two features.
|
||||
|
||||
<kbd>Empty current session</kbd> — Clears your session of players[^1] by blocking the game's network access for 10 seconds, then unblocking it. This can be useful in numerous ways:
|
||||
* You are stuck loading into a session
|
||||
* You are stuck loading into an interior
|
||||
* You are about to get griefed while doing a freemode mission
|
||||
* You want to quickly get a "new" session
|
||||
|
||||
<kbd>Anti AFK</kbd> — Prevents you from getting kicked for idling by pressing keys on your keyboard every 60 seconds. At present, those keys are `VK_NUMPAD4` & `VK_NUMPAD6`, due to being a relatively good balance of keys that the game actually registers but don't interrupt gameplay too much if they activate while you're playing. You *must* be tabbed into GTA V for <kbd>Anti AFK</kbd> to work. While toggled, <kbd>Anti AFK</kbd> will only activate when all of the following conditions are true:
|
||||
* The `Grand Theft Auto V` window is currently focused
|
||||
* None of the designated keys are currently being pressed[^2]
|
||||
* The mouse cursor is not currently visible[^3]
|
||||
|
||||
#### Network
|
||||
|
||||
<kbd>Game's network access</kbd> — Blocks/unblocks GTA V's entire internet access using the Windows Firewall. This feature can also be changed to <kbd>Rockstar save server access</kbd> via the **Network** dropdown of the settings. When in this mode, instead of blocking the entire game's network, it will only block the Rockstar save server at the IP `192.81.241.171`. The IP can also be changed in the same settings, if ever needed.
|
||||
|
||||
This feature is primarily useful for **replay glitching**, which is an exploit that allows you to complete any heist/mission finale, get the money for it, and then be able to do it again right away.
|
||||
|
||||
<details>
|
||||
<summary>How to do a replay glitch</summary>
|
||||
|
||||
1. Get to the finale of a heist/contract/mission series, such as **The Contract: Dr. Dre**
|
||||
2. Complete the finale as normal
|
||||
3. Right *after* the **HEIST/MISSION PASSED** screen you get when you complete a finale (basically, when your cut is being shown), <kbd>Block</kbd> the game's internet access
|
||||
4. After a few seconds, you will be shown an error screen and kicked to the main menu
|
||||
5. At this point, <kbd>Unblock</kbd> the game's internet access and load back into online
|
||||
6. Finally, spend any amount of money, and you should receive the finale cut, but also be able to go play the finale again immediately
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>How to do a replay glitch (save server method)</summary>
|
||||
|
||||
1. Get to the finale of a heist/contract/mission series, such as **The Contract: Dr. Dre**
|
||||
2. At any point before you complete the finale, <kbd>Block</kbd> the save server access (for example, at the beginning of the mission)
|
||||
3. Complete the finale as normal
|
||||
4. After loading into online, exit to story mode
|
||||
5. At this point, <kbd>Unblock</kbd> the save server access and load back into online
|
||||
6. Finally, force a save by changing your outfit, and you should receive the finale cut, but also be able to go play the finale again immediately
|
||||
</details>
|
||||
|
||||
|
||||
|
||||
[^1]: Technically, it just makes you lose connection to the session. But it looks like everyone left you instead.
|
||||
[^2]: Otherwise, if you are, for example, using those keys to fly a plane, <kbd>Anti AFK</kbd> activating could cancel the input.
|
||||
[^3]: Specifically, the Windows mouse cursor, not the in-game mouse cursor. This check is mostly used to prevent typing while you are in the Rockstar overlay.
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><circle style="fill:#fff" cx="256" cy="256" r="256"/></svg>
|
||||
|
After Width: | Height: | Size: 147 B |
@@ -1,7 +1,34 @@
|
||||
fn main() {
|
||||
static_vcruntime::metabuild();
|
||||
winresource::WindowsResource::new()
|
||||
tauri_winres::WindowsResource::new()
|
||||
.set("FileDescription", "GTA Tools")
|
||||
.set("ProductName", "GTA Tools")
|
||||
.set("LegalCopyright", "futile <git@futile.eu>")
|
||||
.set_language(0x0009)
|
||||
.set_icon("assets/icon.ico")
|
||||
.set_manifest(
|
||||
r#"<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
</assembly>"#,
|
||||
)
|
||||
.compile()
|
||||
.unwrap();
|
||||
embed_latest_git_hash();
|
||||
}
|
||||
|
||||
fn embed_latest_git_hash() {
|
||||
let git_rev_parse = std::process::Command::new("git")
|
||||
.args(["rev-parse", "--short=8", "HEAD"])
|
||||
.output()
|
||||
.unwrap();
|
||||
let git_hash = String::from_utf8(git_rev_parse.stdout).unwrap();
|
||||
println!("cargo::rustc-env=LATEST_GIT_COMMIT_HASH={git_hash}");
|
||||
println!("cargo::rerun-if-changed=.git/refs/heads/main");
|
||||
println!("cargo::rerun-if-changed=.git/HEAD");
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
set windows-shell := ["pwsh", "-NoLogo", "-NoProfileLoadTime", "-Command"]
|
||||
|
||||
install:
|
||||
cargo build --release
|
||||
cp .\target\release\gta-tools.exe ~\.cargo\bin
|
||||
cp .\target\release\gta-tools.exe ~\Documents
|
||||
+35
-29
@@ -1,13 +1,12 @@
|
||||
#![allow(clippy::cast_possible_truncation)]
|
||||
|
||||
use crate::util::{self, consts::GTA_WINDOW_TITLE};
|
||||
use crate::util::{self, consts::game::WINDOW_TITLE};
|
||||
use std::time::{Duration, Instant};
|
||||
use windows::Win32::UI::Input::KeyboardAndMouse::{
|
||||
KEYBD_EVENT_FLAGS, MAP_VIRTUAL_KEY_TYPE, MapVirtualKeyW, VK_NUMPAD4, VK_NUMPAD6, keybd_event,
|
||||
INPUT, INPUT_KEYBOARD, KEYBD_EVENT_FLAGS, KEYBDINPUT, KEYEVENTF_KEYUP, MAPVK_VK_TO_VSC,
|
||||
MapVirtualKeyW, SendInput, VIRTUAL_KEY, VK_NUMPAD4, VK_NUMPAD6,
|
||||
};
|
||||
|
||||
pub const INTERVAL: Duration = Duration::from_secs(60);
|
||||
const PRESS_KEYS: [u8; 2] = [VK_NUMPAD4.0 as u8, VK_NUMPAD6.0 as u8];
|
||||
const INTERVAL: Duration = Duration::from_secs(60);
|
||||
const PRESS_KEYS: [VIRTUAL_KEY; 2] = [VK_NUMPAD4, VK_NUMPAD6];
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AntiAfk {
|
||||
@@ -25,32 +24,39 @@ impl Default for AntiAfk {
|
||||
}
|
||||
|
||||
impl AntiAfk {
|
||||
pub fn can_activate(&self) -> bool {
|
||||
use util::win::{is_any_key_pressed, is_cursor_visible, is_window_focused};
|
||||
is_window_focused(WINDOW_TITLE) && !is_any_key_pressed(&PRESS_KEYS) && !is_cursor_visible()
|
||||
}
|
||||
|
||||
pub fn should_activate(&self) -> bool {
|
||||
self.enabled && self.interval.elapsed() >= INTERVAL
|
||||
}
|
||||
|
||||
pub fn activate(&mut self) {
|
||||
if can_activate() {
|
||||
send(&PRESS_KEYS);
|
||||
}
|
||||
send(&PRESS_KEYS);
|
||||
self.interval = Instant::now();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn can_activate() -> bool {
|
||||
use util::win::{is_any_key_pressed, is_cursor_visible, is_window_focused};
|
||||
is_window_focused(GTA_WINDOW_TITLE) && !is_any_key_pressed(&PRESS_KEYS) && !is_cursor_visible()
|
||||
}
|
||||
|
||||
pub fn send(vk_codes: &[u8]) {
|
||||
vk_codes.iter().for_each(|vk_code: &u8| unsafe {
|
||||
keybd_event(
|
||||
*vk_code,
|
||||
u8::try_from(MapVirtualKeyW(u32::from(*vk_code), MAP_VIRTUAL_KEY_TYPE(0))).unwrap(),
|
||||
KEYBD_EVENT_FLAGS(0),
|
||||
0,
|
||||
);
|
||||
keybd_event(
|
||||
*vk_code,
|
||||
u8::try_from(MapVirtualKeyW(u32::from(*vk_code), MAP_VIRTUAL_KEY_TYPE(0))).unwrap(),
|
||||
KEYBD_EVENT_FLAGS(2),
|
||||
0,
|
||||
);
|
||||
});
|
||||
fn send(vk_codes: &[VIRTUAL_KEY]) {
|
||||
let mut inputs = Vec::new();
|
||||
for &vk_code in vk_codes {
|
||||
let scan_code = unsafe { MapVirtualKeyW(u32::from(vk_code.0), MAPVK_VK_TO_VSC) } as u16;
|
||||
for event in [KEYBD_EVENT_FLAGS(0), KEYEVENTF_KEYUP] {
|
||||
let mut input = INPUT {
|
||||
r#type: INPUT_KEYBOARD,
|
||||
..Default::default()
|
||||
};
|
||||
input.Anonymous.ki = KEYBDINPUT {
|
||||
wVk: vk_code,
|
||||
wScan: scan_code,
|
||||
dwFlags: event,
|
||||
time: 0,
|
||||
dwExtraInfo: 0,
|
||||
};
|
||||
inputs.push(input);
|
||||
}
|
||||
}
|
||||
unsafe { SendInput(&inputs, size_of::<INPUT>() as i32) };
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
use crate::util::{
|
||||
consts::{ENHANCED, LEGACY},
|
||||
countdown::Countdown,
|
||||
firewall::{Firewall, RuleDirection, RuleMode, RuleProtocol},
|
||||
system_info::SystemInfo,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use std::time::{Duration, Instant};
|
||||
use sysinfo::System;
|
||||
use windows::Win32::{
|
||||
Foundation::{CloseHandle, HANDLE, NTSTATUS},
|
||||
System::Threading::{OpenProcess, PROCESS_SUSPEND_RESUME},
|
||||
};
|
||||
|
||||
pub const INTERVAL: Duration = Duration::from_secs(10);
|
||||
const FILTER_NAME_EMPTY_SESSION_IN: &str = "[GTA Tools] Block inbound UDP traffic for all of GTA V";
|
||||
const FILTER_NAME_EMPTY_SESSION_OUT: &str =
|
||||
"[GTA Tools] Block outbound UDP traffic for all of GTA V";
|
||||
|
||||
const INTERVAL: Duration = Duration::from_secs(10);
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct EmptySession {
|
||||
@@ -28,40 +29,43 @@ impl Default for EmptySession {
|
||||
}
|
||||
}
|
||||
|
||||
#[link(name = "ntdll")]
|
||||
unsafe extern "system" {
|
||||
pub unsafe fn NtSuspendProcess(ProcessHandle: HANDLE) -> NTSTATUS;
|
||||
pub unsafe fn NtResumeProcess(ProcessHandle: HANDLE) -> NTSTATUS;
|
||||
}
|
||||
|
||||
fn get_gta_pid(sysinfo: &mut System) -> u32 {
|
||||
sysinfo.refresh_all();
|
||||
if let Some((pid, _)) = sysinfo
|
||||
.processes()
|
||||
.iter()
|
||||
.find(|(_, p)| p.name() == ENHANCED || p.name() == LEGACY)
|
||||
{
|
||||
return pid.as_u32();
|
||||
}
|
||||
u32::MAX
|
||||
}
|
||||
|
||||
pub fn activate(handle: &mut HANDLE, sysinfo: &mut System) {
|
||||
let pid = get_gta_pid(sysinfo);
|
||||
if pid == u32::MAX {
|
||||
return;
|
||||
}
|
||||
unsafe {
|
||||
*handle = OpenProcess(PROCESS_SUSPEND_RESUME, false, pid).unwrap();
|
||||
let _ = NtSuspendProcess(*handle);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deactivate(handle: &mut HANDLE) {
|
||||
unsafe {
|
||||
if !handle.is_invalid() {
|
||||
let _ = NtResumeProcess(*handle);
|
||||
let _ = CloseHandle(*handle);
|
||||
impl EmptySession {
|
||||
pub fn run_timers(&mut self, firewall: &Firewall) -> Result<()> {
|
||||
if self.disabled {
|
||||
self.countdown.count();
|
||||
} else {
|
||||
self.countdown.reset();
|
||||
}
|
||||
if self.interval.elapsed() >= INTERVAL {
|
||||
deactivate(firewall)?;
|
||||
self.disabled = false;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn activate(system_info: &mut SystemInfo, firewall: &Firewall) -> Result<bool> {
|
||||
let Some(exe_path) = system_info.get_game_exe_path() else {
|
||||
log::info!("wasn't able to find game exe");
|
||||
return Ok(false);
|
||||
};
|
||||
firewall.add(
|
||||
FILTER_NAME_EMPTY_SESSION_IN,
|
||||
RuleMode::Executable(exe_path.to_path_buf()),
|
||||
RuleDirection::In,
|
||||
RuleProtocol::Udp,
|
||||
)?;
|
||||
firewall.add(
|
||||
FILTER_NAME_EMPTY_SESSION_OUT,
|
||||
RuleMode::Executable(exe_path.to_path_buf()),
|
||||
RuleDirection::Out,
|
||||
RuleProtocol::Udp,
|
||||
)?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub fn deactivate(firewall: &Firewall) -> Result<()> {
|
||||
firewall.remove(FILTER_NAME_EMPTY_SESSION_IN)?;
|
||||
firewall.remove(FILTER_NAME_EMPTY_SESSION_OUT)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+46
-18
@@ -1,40 +1,68 @@
|
||||
use crate::util::consts::{ENHANCED, LEGACY};
|
||||
use std::time::Instant;
|
||||
use sysinfo::System;
|
||||
use crate::util::{
|
||||
consts::game::{EXE_ENHANCED, EXE_LEGACY},
|
||||
system_info::SystemInfo,
|
||||
};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
const INTERVAL: Duration = Duration::from_secs(3);
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ForceClose {
|
||||
pub button_text: String,
|
||||
pub prompting: bool,
|
||||
pub interval: Instant,
|
||||
timer: Instant,
|
||||
counting: bool,
|
||||
current_frame: bool,
|
||||
}
|
||||
|
||||
impl Default for ForceClose {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
button_text: "Force close game".to_owned(),
|
||||
prompting: false,
|
||||
interval: Instant::now(),
|
||||
timer: Instant::now(),
|
||||
counting: false,
|
||||
current_frame: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ForceClose {
|
||||
pub fn prompting(&mut self) {
|
||||
self.button_text = "Are you sure?".to_owned();
|
||||
self.prompting = true;
|
||||
self.interval = Instant::now();
|
||||
pub fn prompt(&mut self, force_close_button_clicked: bool, system_info: &mut SystemInfo) {
|
||||
if force_close_button_clicked && !self.counting {
|
||||
self.button_text = "Are you sure?".to_owned();
|
||||
self.timer = Instant::now();
|
||||
self.counting = true;
|
||||
self.current_frame = true;
|
||||
}
|
||||
if self.counting && self.timer.elapsed() >= INTERVAL {
|
||||
self.reset();
|
||||
} else if force_close_button_clicked && !self.current_frame {
|
||||
activate(system_info);
|
||||
self.reset();
|
||||
}
|
||||
self.finish_current_frame();
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
*self = Self::default();
|
||||
}
|
||||
|
||||
const fn finish_current_frame(&mut self) {
|
||||
if self.current_frame {
|
||||
self.current_frame = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn activate(sysinfo: &mut System) {
|
||||
sysinfo.refresh_all();
|
||||
sysinfo
|
||||
fn activate(system_info: &mut SystemInfo) {
|
||||
system_info.refresh();
|
||||
system_info
|
||||
.processes()
|
||||
.iter()
|
||||
.filter(|(_, p)| p.name() == ENHANCED || p.name() == LEGACY)
|
||||
.for_each(|(_, p)| {
|
||||
p.kill();
|
||||
.filter(|p| p.name() == EXE_ENHANCED || p.name() == EXE_LEGACY)
|
||||
.for_each(|p| {
|
||||
if !p.kill() {
|
||||
log::error!("failed to force close game, probably due to access denied");
|
||||
}
|
||||
});
|
||||
sysinfo.refresh_all();
|
||||
system_info.refresh();
|
||||
}
|
||||
|
||||
@@ -1,72 +1,100 @@
|
||||
#![allow(clippy::zombie_processes)]
|
||||
use crate::{
|
||||
gui::settings::BlockMethod,
|
||||
util::{
|
||||
firewall::{Firewall, RuleDirection, RuleMode, RuleProtocol},
|
||||
system_info::SystemInfo,
|
||||
},
|
||||
};
|
||||
use anyhow::Result;
|
||||
use strum::{Display, EnumIter};
|
||||
|
||||
use crate::util::consts::{ENHANCED, LEGACY};
|
||||
use std::{os::windows::process::CommandExt, path::Path, process::Command};
|
||||
use sysinfo::System;
|
||||
use windows::Win32::System::Threading::CREATE_NO_WINDOW;
|
||||
const FILTER_NAME_EXE: &str = "[GTA Tools] Block outbound traffic for all of GTA V";
|
||||
const FILTER_NAME_SAVE_SERVER: &str = "[GTA Tools] Block outbound traffic to Rockstar save server";
|
||||
|
||||
const FILTER_NAME: &str = "[GTA Tools] Block all traffic for GTA V";
|
||||
#[derive(Clone, Copy, Debug, Default, Display, EnumIter, PartialEq)]
|
||||
pub enum BlockedStatus {
|
||||
#[default]
|
||||
Unblocked,
|
||||
Server,
|
||||
Executable,
|
||||
}
|
||||
|
||||
fn get_game_exe_path(sysinfo: &mut System) -> Option<&Path> {
|
||||
sysinfo.refresh_all();
|
||||
if let Some((_, process)) = sysinfo
|
||||
.processes()
|
||||
.iter()
|
||||
.find(|(_, p)| p.name() == ENHANCED || p.name() == LEGACY)
|
||||
{
|
||||
process.exe()
|
||||
} else {
|
||||
None
|
||||
#[derive(Debug)]
|
||||
pub struct GameNetworking {
|
||||
pub blocked: BlockedStatus,
|
||||
}
|
||||
|
||||
impl Default for GameNetworking {
|
||||
fn default() -> Self {
|
||||
let firewall = Firewall::default();
|
||||
Self {
|
||||
blocked: if firewall.is_blocked(FILTER_NAME_SAVE_SERVER).unwrap() {
|
||||
BlockedStatus::Server
|
||||
} else if firewall.is_blocked(FILTER_NAME_EXE).unwrap() {
|
||||
BlockedStatus::Executable
|
||||
} else {
|
||||
BlockedStatus::Unblocked
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn block_all(sysinfo: &mut System) {
|
||||
let Some(exe_path) = get_game_exe_path(sysinfo) else {
|
||||
return;
|
||||
};
|
||||
let exe_path = exe_path.display();
|
||||
Command::new("netsh")
|
||||
.args([
|
||||
"advfirewall",
|
||||
"firewall",
|
||||
"add",
|
||||
"rule",
|
||||
&format!("name={FILTER_NAME}"),
|
||||
"dir=out",
|
||||
"action=block",
|
||||
"protocol=ANY",
|
||||
&format!("program={exe_path}"),
|
||||
])
|
||||
.creation_flags(CREATE_NO_WINDOW.0)
|
||||
.spawn()
|
||||
.unwrap();
|
||||
Command::new("netsh")
|
||||
.args([
|
||||
"advfirewall",
|
||||
"firewall",
|
||||
"add",
|
||||
"rule",
|
||||
&format!("name={FILTER_NAME}"),
|
||||
"dir=in",
|
||||
"action=block",
|
||||
"protocol=ANY",
|
||||
&format!("program={exe_path}"),
|
||||
])
|
||||
.creation_flags(CREATE_NO_WINDOW.0)
|
||||
.spawn()
|
||||
.unwrap();
|
||||
}
|
||||
impl GameNetworking {
|
||||
pub fn block_exe(&mut self, system_info: &mut SystemInfo, firewall: &Firewall) -> Result<()> {
|
||||
let Some(exe_path) = system_info.get_game_exe_path() else {
|
||||
log::warn!("Unable to find game executable path.");
|
||||
return Ok(());
|
||||
};
|
||||
firewall
|
||||
.add(
|
||||
FILTER_NAME_EXE,
|
||||
RuleMode::Executable(exe_path.to_path_buf()),
|
||||
RuleDirection::Out,
|
||||
RuleProtocol::Any,
|
||||
)
|
||||
.inspect(|_| self.blocked = BlockedStatus::Executable)
|
||||
}
|
||||
|
||||
pub fn unblock_all() {
|
||||
Command::new("netsh")
|
||||
.args([
|
||||
"advfirewall",
|
||||
"firewall",
|
||||
"delete",
|
||||
"rule",
|
||||
&format!("name={FILTER_NAME}"),
|
||||
])
|
||||
.creation_flags(CREATE_NO_WINDOW.0)
|
||||
.spawn()
|
||||
.unwrap();
|
||||
pub fn unblock_exe(&mut self, firewall: &Firewall) -> Result<()> {
|
||||
firewall
|
||||
.remove(FILTER_NAME_EXE)
|
||||
.inspect(|_| self.blocked = BlockedStatus::Unblocked)
|
||||
}
|
||||
|
||||
pub fn block_save_server(&mut self, save_server_ip: &str, firewall: &Firewall) -> Result<()> {
|
||||
firewall
|
||||
.add(
|
||||
FILTER_NAME_SAVE_SERVER,
|
||||
RuleMode::Address(save_server_ip.to_owned()),
|
||||
RuleDirection::Out,
|
||||
RuleProtocol::Any,
|
||||
)
|
||||
.inspect(|_| self.blocked = BlockedStatus::Server)
|
||||
}
|
||||
|
||||
pub fn unblock_save_server(&mut self, firewall: &Firewall) -> Result<()> {
|
||||
firewall
|
||||
.remove(FILTER_NAME_SAVE_SERVER)
|
||||
.inspect(|_| self.blocked = BlockedStatus::Unblocked)
|
||||
}
|
||||
|
||||
pub fn ensure_block_exclusivity(
|
||||
&mut self,
|
||||
block_method: BlockMethod,
|
||||
firewall: &Firewall,
|
||||
) -> Result<()> {
|
||||
match block_method {
|
||||
BlockMethod::EntireGame => {
|
||||
if self.blocked == BlockedStatus::Server {
|
||||
self.unblock_save_server(firewall)?;
|
||||
}
|
||||
}
|
||||
BlockMethod::SaveServer => {
|
||||
if self.blocked == BlockedStatus::Executable {
|
||||
self.unblock_exe(firewall)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
+32
-26
@@ -1,57 +1,63 @@
|
||||
use crate::gui::settings::LaunchVersion;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{fmt::Display, path::PathBuf, process::Command};
|
||||
use strum::EnumIter;
|
||||
use std::{path::PathBuf, process::Command};
|
||||
use strum::{Display, EnumIter};
|
||||
use winreg::{RegKey, enums::HKEY_LOCAL_MACHINE};
|
||||
|
||||
#[derive(Clone, Copy, Default, Debug, PartialEq, Eq, Serialize, Deserialize, EnumIter)]
|
||||
#[derive(Clone, Copy, Default, Debug, Display, PartialEq, Eq, Serialize, Deserialize, EnumIter)]
|
||||
pub enum Platform {
|
||||
#[default]
|
||||
Steam,
|
||||
#[strum(to_string = "Rockstar Games")]
|
||||
Rockstar,
|
||||
#[strum(to_string = "Epic Games")]
|
||||
Epic,
|
||||
}
|
||||
|
||||
impl Display for Platform {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let x = match self {
|
||||
Self::Steam => "Steam",
|
||||
Self::Rockstar => "Rockstar Games",
|
||||
Self::Epic => "Epic Games",
|
||||
};
|
||||
write!(f, "{x}")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Launch {
|
||||
pub selected: Platform,
|
||||
}
|
||||
|
||||
pub fn launch(platform: &Platform) {
|
||||
pub fn launch(platform: &Platform, version: &LaunchVersion) {
|
||||
match platform {
|
||||
Platform::Steam => {
|
||||
let _ = open::that_detached("steam://run/3240220");
|
||||
let steam_url = match version {
|
||||
LaunchVersion::Enhanced => "steam://run/3240220",
|
||||
LaunchVersion::Legacy => "steam://run/271590",
|
||||
};
|
||||
open::that_detached(steam_url).unwrap();
|
||||
}
|
||||
Platform::Rockstar => {
|
||||
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
|
||||
let Ok(gta_v_enhanced) =
|
||||
hklm.open_subkey(r"SOFTWARE\WOW6432Node\Rockstar Games\GTAV Enhanced")
|
||||
else {
|
||||
let rockstar_url = match version {
|
||||
LaunchVersion::Enhanced => r"SOFTWARE\WOW6432Node\Rockstar Games\GTAV Enhanced",
|
||||
LaunchVersion::Legacy => r"SOFTWARE\WOW6432Node\Rockstar Games\Grand Theft Auto V",
|
||||
};
|
||||
let Ok(gta_key) = hklm.open_subkey(rockstar_url) else {
|
||||
return;
|
||||
};
|
||||
let Ok(install_folder): Result<String, std::io::Error> =
|
||||
gta_v_enhanced.get_value("InstallFolder")
|
||||
gta_key.get_value("InstallFolder")
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let mut play_gtav_path = PathBuf::from(install_folder);
|
||||
play_gtav_path.push("PlayGTAV.exe");
|
||||
let _ = Command::new(play_gtav_path).spawn();
|
||||
let mut exe_path = PathBuf::from(install_folder);
|
||||
exe_path.push("PlayGTAV.exe");
|
||||
if let Err(why) = Command::new(exe_path).spawn() {
|
||||
log::warn!("Couldn't launch GTA 5 via Rockstar Games Launcher: {why}");
|
||||
}
|
||||
}
|
||||
Platform::Epic => {
|
||||
let _ = open::that_detached(
|
||||
"com.epicgames.launcher://apps/331226ba7c944720baa99103cb1fe80c?action=launch&silent=true",
|
||||
);
|
||||
let epic_url = match version {
|
||||
LaunchVersion::Enhanced => {
|
||||
"com.epicgames.launcher://apps/8769e24080ea413b8ebca3f1b8c50951?action=launch&silent=true"
|
||||
}
|
||||
LaunchVersion::Legacy => {
|
||||
"com.epicgames.launcher://apps/9d2d0eb64d5c44529cece33fe2a46482?action=launch&silent=true"
|
||||
}
|
||||
};
|
||||
open::that_detached(epic_url).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
mod app;
|
||||
pub mod app;
|
||||
mod debug;
|
||||
pub mod run;
|
||||
pub mod settings;
|
||||
mod tools;
|
||||
mod ui_ext;
|
||||
+225
-236
@@ -1,197 +1,122 @@
|
||||
use crate::{
|
||||
features::{
|
||||
self, anti_afk::AntiAfk, empty_session::EmptySession, force_close::ForceClose,
|
||||
launch::Launch,
|
||||
},
|
||||
features::{self, game_networking::BlockedStatus},
|
||||
gui::{
|
||||
settings::{self, Settings},
|
||||
settings::{BlockMethod, ROCKSTAR_SAVE_SERVER, Settings},
|
||||
tools,
|
||||
ui_ext::UiExt,
|
||||
},
|
||||
util::{
|
||||
self,
|
||||
consts::{APP_STORAGE_PATH, ENHANCED, GTA_WINDOW_TITLE, LEGACY},
|
||||
meta::Meta,
|
||||
consts::{colours, game::WINDOW_TITLE, path},
|
||||
firewall::Firewall,
|
||||
persistent_state::PersistentState,
|
||||
system_info::SystemInfo,
|
||||
win,
|
||||
},
|
||||
};
|
||||
use eframe::egui;
|
||||
use std::time::{Duration, Instant};
|
||||
use strum::{Display, EnumIter};
|
||||
|
||||
pub const WINDOW_SIZE: [f32; 2] = [240.0, 240.0];
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq)]
|
||||
pub enum Stage {
|
||||
#[derive(Clone, Copy, Debug, Default, Display, PartialEq, Eq, EnumIter)]
|
||||
enum Stage {
|
||||
#[default]
|
||||
Main,
|
||||
Settings,
|
||||
About,
|
||||
}
|
||||
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Flags {
|
||||
pub elevated: bool,
|
||||
debug: bool,
|
||||
closing: bool,
|
||||
current_frame: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct App {
|
||||
meta: Meta,
|
||||
pub settings: Settings,
|
||||
stage: Stage,
|
||||
pub flags: Flags,
|
||||
pub sysinfo: sysinfo::System,
|
||||
pub game_handle: windows::Win32::Foundation::HANDLE,
|
||||
pub launch: Launch,
|
||||
force_close: ForceClose,
|
||||
empty_session: EmptySession,
|
||||
anti_afk: AntiAfk,
|
||||
pub struct Flags {
|
||||
pub debug: bool,
|
||||
closing: bool,
|
||||
}
|
||||
|
||||
impl Default for App {
|
||||
impl Default for Flags {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
meta: Meta::default(),
|
||||
settings: Settings::default(),
|
||||
stage: Stage::default(),
|
||||
flags: Flags::default(),
|
||||
sysinfo: sysinfo::System::new_all(),
|
||||
game_handle: windows::Win32::Foundation::HANDLE::default(),
|
||||
launch: Launch::default(),
|
||||
force_close: ForceClose::default(),
|
||||
empty_session: EmptySession::default(),
|
||||
anti_afk: AntiAfk::default(),
|
||||
debug: false,
|
||||
closing: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct App {
|
||||
pub settings: Settings,
|
||||
stage: Stage,
|
||||
pub flags: Flags,
|
||||
pub system_info: SystemInfo,
|
||||
firewall: Firewall,
|
||||
pub anti_afk: features::anti_afk::AntiAfk,
|
||||
empty_session: features::empty_session::EmptySession,
|
||||
force_close: features::force_close::ForceClose,
|
||||
pub game_networking: features::game_networking::GameNetworking,
|
||||
pub launch: features::launch::Launch,
|
||||
}
|
||||
|
||||
impl eframe::App for App {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
catppuccin_egui::set_theme(ctx, self.settings.theme.into());
|
||||
self.run_timers();
|
||||
egui::TopBottomPanel::bottom("bottom_panel")
|
||||
.exact_height(25.0)
|
||||
.show(ctx, |ui| {
|
||||
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
|
||||
ui.request_repaint_after(Duration::from_millis(100));
|
||||
self.empty_session.run_timers(&self.firewall).unwrap();
|
||||
egui::Panel::bottom("bottom_panel")
|
||||
.exact_size(25.0)
|
||||
.show(ui, |ui| {
|
||||
ui.with_layout(egui::Layout::left_to_right(egui::Align::Center), |ui| {
|
||||
ui.selectable_value(&mut self.stage, Stage::Main, "Main");
|
||||
ui.selectable_value(&mut self.stage, Stage::Settings, "Settings");
|
||||
ui.selectable_value(&mut self.stage, Stage::About, "About");
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
let button = ui
|
||||
.add_enabled(!self.flags.elevated, egui::Button::new("Elevate"))
|
||||
.on_hover_text("Relaunch ourselves as administrator.")
|
||||
.on_disabled_hover_text("We are already running elevated.");
|
||||
if button.clicked() {
|
||||
util::win::elevate(util::win::ElevationExitMethod::Gentle(
|
||||
&mut self.flags.closing,
|
||||
));
|
||||
}
|
||||
});
|
||||
ui.build_menu(&mut self.stage);
|
||||
});
|
||||
});
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
egui::CentralPanel::default().show(ui, |ui| {
|
||||
egui::ScrollArea::vertical()
|
||||
.auto_shrink([false, true])
|
||||
.show(ui, |ui| match self.stage {
|
||||
Stage::Main => {
|
||||
self.show_game(ctx, ui);
|
||||
self.show_session(ctx, ui);
|
||||
self.show_network(ctx, ui);
|
||||
}
|
||||
Stage::Settings => self.show_settings(ctx, ui),
|
||||
Stage::About => self.show_about(ctx, ui),
|
||||
Stage::Main => self.show_main_stage(ui),
|
||||
Stage::Settings => self.show_settings_stage(ui),
|
||||
Stage::About => self.show_about_stage(ui),
|
||||
});
|
||||
});
|
||||
if tools::check_debug_keycombo_pressed(ctx) {
|
||||
if tools::debug_keycombo_pressed(ui) || tools::debug_viewport_close_pressed(ui) {
|
||||
self.flags.debug = !self.flags.debug;
|
||||
}
|
||||
if tools::check_debug_viewport_close_button_pressed(ctx) {
|
||||
self.flags.debug = false;
|
||||
}
|
||||
if self.flags.debug {
|
||||
let main_rect = ctx.input(|i| {
|
||||
i.viewport()
|
||||
.clone()
|
||||
.outer_rect
|
||||
.unwrap_or(egui::Rect::EVERYTHING)
|
||||
});
|
||||
let position = [main_rect.right(), main_rect.min.y];
|
||||
ctx.show_viewport_immediate(
|
||||
egui::ViewportId::from_hash_of("debug_viewport"),
|
||||
egui::ViewportBuilder::default()
|
||||
.with_title("GTA Tools Debug")
|
||||
.with_minimize_button(false)
|
||||
.with_maximize_button(false)
|
||||
.with_inner_size(WINDOW_SIZE)
|
||||
.with_position(position)
|
||||
.with_icon(tools::load_icon()),
|
||||
|ctx, _class| {
|
||||
if tools::check_debug_keycombo_pressed(ctx) {
|
||||
self.flags.debug = !self.flags.debug;
|
||||
}
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
egui::ScrollArea::both()
|
||||
.auto_shrink([false, true])
|
||||
.show(ui, |ui| {
|
||||
self.show_debug(ctx, ui);
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
self.show_debug_viewport(ui);
|
||||
}
|
||||
ctx.request_repaint_after(Duration::from_millis(100));
|
||||
if self.flags.closing {
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
||||
ui.send_viewport_cmd(egui::ViewportCommand::Close);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn show_game(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) {
|
||||
tools::header(ui, "Game");
|
||||
fn show_game_section(&mut self, ui: &mut egui::Ui) {
|
||||
ui.header("Game");
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("Launch").clicked() {
|
||||
features::launch::launch(&self.launch.selected);
|
||||
features::launch::launch(&self.launch.selected, &self.settings.launch_version);
|
||||
}
|
||||
tools::build_combo_box::<features::launch::Platform>(
|
||||
ui,
|
||||
&mut self.launch.selected,
|
||||
"Launch",
|
||||
);
|
||||
egui::ComboBox::from_id_salt("Launch")
|
||||
.selected_text(self.launch.selected.to_string())
|
||||
.width(120.0)
|
||||
.show_ui(ui, |ui| {
|
||||
ui.build_menu(&mut self.launch.selected);
|
||||
});
|
||||
});
|
||||
let force_close_button = ui.add_sized(
|
||||
[104.0, 0.0],
|
||||
egui::Button::new(&self.force_close.button_text),
|
||||
);
|
||||
if force_close_button.clicked() && !self.force_close.prompting {
|
||||
self.force_close.prompting();
|
||||
self.flags.current_frame = true;
|
||||
}
|
||||
if self.force_close.prompting
|
||||
&& self.force_close.interval.elapsed() <= Duration::from_secs(3)
|
||||
{
|
||||
if force_close_button.clicked() && !self.flags.current_frame {
|
||||
features::force_close::activate(&mut self.sysinfo);
|
||||
self.force_close = ForceClose::default();
|
||||
}
|
||||
} else {
|
||||
self.force_close = ForceClose::default();
|
||||
}
|
||||
if self.flags.current_frame {
|
||||
self.flags.current_frame = false;
|
||||
}
|
||||
let force_close_button = ui.button(&self.force_close.button_text);
|
||||
self.force_close
|
||||
.prompt(force_close_button.clicked(), &mut self.system_info);
|
||||
}
|
||||
|
||||
fn show_session(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) {
|
||||
tools::header(ui, "Session");
|
||||
fn show_session_section(&mut self, ui: &mut egui::Ui) {
|
||||
ui.header("Session");
|
||||
ui.add_enabled_ui(!self.empty_session.disabled, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("Empty current session").clicked() {
|
||||
if ui.button("Empty current session").clicked()
|
||||
&& features::empty_session::activate(&mut self.system_info, &self.firewall)
|
||||
.unwrap()
|
||||
{
|
||||
self.empty_session.interval = Instant::now();
|
||||
self.empty_session.disabled = true;
|
||||
features::empty_session::activate(&mut self.game_handle, &mut self.sysinfo);
|
||||
}
|
||||
ui.label(&self.empty_session.countdown.i_string);
|
||||
});
|
||||
@@ -202,79 +127,192 @@ impl App {
|
||||
if self.anti_afk.enabled {
|
||||
ui.add_space(8.0);
|
||||
ui.add_enabled_ui(false, |ui| {
|
||||
ui.label(if util::win::is_window_focused(GTA_WINDOW_TITLE) {
|
||||
"GTA is focused."
|
||||
ui.label(if win::is_window_focused(WINDOW_TITLE) {
|
||||
"Game is focused."
|
||||
} else {
|
||||
"GTA is not focused!"
|
||||
"Game is not focused!"
|
||||
})
|
||||
});
|
||||
}
|
||||
});
|
||||
if self.anti_afk.enabled && self.anti_afk.interval.elapsed() >= features::anti_afk::INTERVAL
|
||||
{
|
||||
if self.anti_afk.can_activate() && self.anti_afk.should_activate() {
|
||||
self.anti_afk.activate();
|
||||
}
|
||||
}
|
||||
|
||||
fn show_network(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) {
|
||||
tools::header(ui, "Network");
|
||||
fn show_network_section(&mut self, ui: &mut egui::Ui) {
|
||||
ui.header("Network");
|
||||
egui::Frame::new()
|
||||
.outer_margin(egui::vec2(0.0, -2.0))
|
||||
.show(ui, |ui| {
|
||||
let response = ui.add_enabled_ui(self.flags.elevated, |ui| {
|
||||
let label = ui.label("Game's network access");
|
||||
ui.horizontal(|ui| {
|
||||
let available_width = label.rect.width();
|
||||
let spacing = ui.spacing().item_spacing.x;
|
||||
let button_width = (available_width - spacing) / 2.0;
|
||||
if ui
|
||||
.add_sized([button_width, 18.0], egui::Button::new("Block"))
|
||||
.clicked()
|
||||
let label = ui.horizontal(|ui| {
|
||||
let label = match self.settings.block_method {
|
||||
BlockMethod::EntireGame => ui.label("Game's network access"),
|
||||
BlockMethod::SaveServer => ui.label("Rockstar save server access"),
|
||||
};
|
||||
ui.add_space(1.0);
|
||||
ui.create_indicator_dot(match self.game_networking.blocked {
|
||||
BlockedStatus::Executable
|
||||
if self.settings.block_method == BlockMethod::EntireGame =>
|
||||
{
|
||||
features::game_networking::block_all(&mut self.sysinfo);
|
||||
colours::RED
|
||||
}
|
||||
if ui
|
||||
.add_sized([button_width, 18.0], egui::Button::new("Unblock"))
|
||||
.clicked()
|
||||
BlockedStatus::Server
|
||||
if self.settings.block_method == BlockMethod::SaveServer =>
|
||||
{
|
||||
features::game_networking::unblock_all();
|
||||
colours::RED
|
||||
}
|
||||
_ => colours::GREEN,
|
||||
});
|
||||
label
|
||||
});
|
||||
response.response.on_disabled_hover_text(
|
||||
"This requires administrator.\nUse the Elevate button.",
|
||||
ui.allocate_ui_with_layout(
|
||||
egui::vec2(label.inner.rect.width(), 0.0),
|
||||
egui::Layout::top_down(egui::Align::Min),
|
||||
|ui| {
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered_justified(|ui| {
|
||||
if ui.button("Block").clicked() {
|
||||
match self.settings.block_method {
|
||||
BlockMethod::EntireGame => {
|
||||
self.game_networking
|
||||
.block_exe(&mut self.system_info, &self.firewall)
|
||||
.unwrap();
|
||||
}
|
||||
BlockMethod::SaveServer => {
|
||||
self.game_networking
|
||||
.block_save_server(
|
||||
&self.settings.save_server_ip,
|
||||
&self.firewall,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
if ui.button("Unblock").clicked() {
|
||||
match self.settings.block_method {
|
||||
BlockMethod::EntireGame => {
|
||||
self.game_networking
|
||||
.unblock_exe(&self.firewall)
|
||||
.unwrap();
|
||||
}
|
||||
BlockMethod::SaveServer => {
|
||||
self.game_networking
|
||||
.unblock_save_server(&self.firewall)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn show_about(&self, _ctx: &egui::Context, ui: &mut egui::Ui) {
|
||||
fn show_main_stage(&mut self, ui: &mut egui::Ui) {
|
||||
self.show_game_section(ui);
|
||||
self.show_session_section(ui);
|
||||
self.show_network_section(ui);
|
||||
}
|
||||
|
||||
fn show_settings_stage(&mut self, ui: &mut egui::Ui) {
|
||||
ui.collapsing("General", |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
let selection = self.settings.theme;
|
||||
egui::ComboBox::from_id_salt("Theme")
|
||||
.selected_text(self.settings.theme.to_string())
|
||||
.show_ui(ui, |ui| {
|
||||
ui.build_menu(&mut self.settings.theme);
|
||||
});
|
||||
if selection != self.settings.theme {
|
||||
ui.set_theme(egui::Theme::from(self.settings.theme));
|
||||
}
|
||||
ui.label("Theme");
|
||||
});
|
||||
});
|
||||
ui.collapsing("Game", |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
egui::ComboBox::from_id_salt("Launch version")
|
||||
.selected_text(self.settings.launch_version.to_string())
|
||||
.show_ui(ui, |ui| {
|
||||
ui.build_menu(&mut self.settings.launch_version);
|
||||
});
|
||||
ui.label("Launch version");
|
||||
});
|
||||
});
|
||||
ui.collapsing("Network", |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
egui::ComboBox::from_id_salt("Block method")
|
||||
.selected_text(self.settings.block_method.to_string())
|
||||
.show_ui(ui, |ui| {
|
||||
ui.build_menu(&mut self.settings.block_method);
|
||||
});
|
||||
ui.label("Block method");
|
||||
if let Err(why) = self
|
||||
.game_networking
|
||||
.ensure_block_exclusivity(self.settings.block_method, &self.firewall)
|
||||
{
|
||||
log::warn!("Couldn't ensure block exclusivity: {why}");
|
||||
}
|
||||
});
|
||||
if self.settings.block_method == BlockMethod::SaveServer {
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(
|
||||
egui::TextEdit::singleline(&mut self.settings.save_server_ip)
|
||||
.char_limit(15)
|
||||
.desired_width(100.0),
|
||||
);
|
||||
ui.label("Save server IP");
|
||||
if self.settings.save_server_ip != Settings::default().save_server_ip {
|
||||
if ui.button("↺").clicked() {
|
||||
self.settings.save_server_ip = String::from(ROCKSTAR_SAVE_SERVER);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
ui.collapsing("Miscellaneous", |ui| {
|
||||
if ui.button("Open storage path").clicked() {
|
||||
open::that_detached(path::APP_STORAGE.as_path()).unwrap();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn show_about_stage(&self, ui: &mut egui::Ui) {
|
||||
ui.with_layout(egui::Layout::bottom_up(egui::Align::Center), |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.label("with ");
|
||||
ui.hyperlink_to("❤", "https://codeberg.org/futile/gta-tools");
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().visuals.override_text_color = Some(colours::RED);
|
||||
ui.label("❤");
|
||||
});
|
||||
ui.label(" from ");
|
||||
ui.hyperlink_to("futile", "http://futile.eu");
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().visuals.hyperlink_color =
|
||||
egui::Theme::from(self.settings.theme)
|
||||
.default_style()
|
||||
.visuals
|
||||
.text_color();
|
||||
ui.hyperlink_to("futile", "https://futile.eu");
|
||||
});
|
||||
});
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
ui.label(format!(
|
||||
"v{} {}",
|
||||
self.meta.current_version,
|
||||
if cfg!(debug_assertions) { "(dev)" } else { "" }
|
||||
));
|
||||
let button = ui.add_enabled_ui(self.meta.newer_version_available, |ui| {
|
||||
ui.style_mut().spacing.button_padding = egui::Vec2::new(3.0, 0.0);
|
||||
ui.button("⬇")
|
||||
.on_disabled_hover_text("Already up to date.")
|
||||
.on_hover_text(format!(
|
||||
"New version available! ({})",
|
||||
self.meta.latest_release.version
|
||||
))
|
||||
});
|
||||
if button.inner.clicked() {
|
||||
open::that(&self.meta.latest_release.download_url).unwrap();
|
||||
ui.style_mut().spacing.button_padding = egui::Vec2::new(4.0, 0.0);
|
||||
if ui.button("").on_hover_text("View source code").clicked() {
|
||||
open::that("https://futile.eu/gta-tools").unwrap();
|
||||
}
|
||||
if cfg!(debug_assertions) {
|
||||
ui.label("(dev)");
|
||||
}
|
||||
ui.label(format!("v{}", env!("CARGO_PKG_VERSION")))
|
||||
.on_hover_text(
|
||||
egui::RichText::new(env!("LATEST_GIT_COMMIT_HASH")).monospace(),
|
||||
);
|
||||
});
|
||||
});
|
||||
ui.add(egui::Image::new(egui::include_image!(
|
||||
@@ -282,69 +320,20 @@ impl App {
|
||||
)));
|
||||
});
|
||||
}
|
||||
|
||||
fn show_settings(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Theme");
|
||||
tools::build_combo_box::<settings::Theme>(ui, &mut self.settings.theme, "Theme");
|
||||
});
|
||||
ui.checkbox(&mut self.settings.start_elevated, "Always start elevated");
|
||||
}
|
||||
|
||||
fn show_debug(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) {
|
||||
if ui.button("open storage path").clicked() {
|
||||
open::that_detached(APP_STORAGE_PATH.as_path()).unwrap();
|
||||
}
|
||||
ui.collapsing("anti afk", |ui| {
|
||||
ui.label(format!(
|
||||
"timer: {}",
|
||||
self.anti_afk.interval.elapsed().as_secs()
|
||||
));
|
||||
ui.label(format!(
|
||||
"can activate: {}",
|
||||
features::anti_afk::can_activate()
|
||||
));
|
||||
});
|
||||
ui.collapsing("sysinfo", |ui| {
|
||||
if ui.button("refresh all").clicked() {
|
||||
self.sysinfo.refresh_all();
|
||||
}
|
||||
let pid = self
|
||||
.sysinfo
|
||||
.processes()
|
||||
.iter()
|
||||
.find(|(_, p)| p.name() == ENHANCED || p.name() == LEGACY)
|
||||
.map_or_else(
|
||||
|| "no pid found!".to_owned(),
|
||||
|(pid, _)| pid.as_u32().to_string(),
|
||||
);
|
||||
ui.label(format!("gta pid: {pid}"));
|
||||
});
|
||||
ui.collapsing("app state", |ui| ui.label(format!("{self:#?}")));
|
||||
}
|
||||
|
||||
fn run_timers(&mut self) {
|
||||
if self.empty_session.disabled {
|
||||
self.empty_session.countdown.count();
|
||||
} else {
|
||||
self.empty_session.countdown.reset();
|
||||
}
|
||||
if self.empty_session.interval.elapsed() >= features::empty_session::INTERVAL {
|
||||
features::empty_session::deactivate(&mut self.game_handle);
|
||||
self.empty_session.disabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for App {
|
||||
fn drop(&mut self) {
|
||||
// save any persistent state to config file
|
||||
let persistent_state = PersistentState {
|
||||
PersistentState {
|
||||
launcher: self.launch.selected,
|
||||
anti_afk_enabled: self.anti_afk.enabled,
|
||||
settings: self.settings.clone(),
|
||||
};
|
||||
persistent_state.set();
|
||||
// make sure we are not suspending game
|
||||
features::empty_session::deactivate(&mut self.game_handle);
|
||||
}
|
||||
.set();
|
||||
// make sure we are not network blocking game
|
||||
if let Err(why) = features::empty_session::deactivate(&self.firewall) {
|
||||
log::error!("couldn't deactivate empty session: {why}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
use crate::{
|
||||
gui::{
|
||||
app::{App, WINDOW_SIZE},
|
||||
tools,
|
||||
ui_ext::UiExt,
|
||||
},
|
||||
util::consts::game::{EXE_ENHANCED, EXE_LEGACY},
|
||||
};
|
||||
use eframe::egui;
|
||||
|
||||
impl App {
|
||||
fn add_debug_viewport_contents(&mut self, ui: &mut egui::Ui) {
|
||||
ui.collapsing("misc", |ui| {
|
||||
ui.scope(|ui| {
|
||||
use windows::Win32::UI::WindowsAndMessaging::{
|
||||
GetForegroundWindow, GetWindowTextW,
|
||||
};
|
||||
let current_title = {
|
||||
let mut buffer = [0; 512];
|
||||
let hwnd = unsafe { GetForegroundWindow() };
|
||||
let length = unsafe { GetWindowTextW(hwnd, &mut buffer) };
|
||||
String::from_utf16_lossy(&buffer[..length as usize])
|
||||
};
|
||||
ui.label(format!("focused: \"{current_title}\""));
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("blocked");
|
||||
egui::ComboBox::from_id_salt("blocked")
|
||||
.selected_text(self.game_networking.blocked.to_string())
|
||||
.show_ui(ui, |ui| ui.build_menu(&mut self.game_networking.blocked));
|
||||
});
|
||||
if ui.add(egui::Button::new("force refresh theme")).clicked() {
|
||||
ui.set_theme(egui::Theme::from(self.settings.theme));
|
||||
}
|
||||
if ui.button("panic button").clicked() {
|
||||
panic!("this is the panic button");
|
||||
}
|
||||
});
|
||||
ui.collapsing("anti afk", |ui| {
|
||||
ui.label(format!(
|
||||
"timer: {}",
|
||||
self.anti_afk.interval.elapsed().as_secs()
|
||||
));
|
||||
ui.label(format!("can activate: {}", self.anti_afk.can_activate()));
|
||||
});
|
||||
ui.collapsing("system info", |ui| {
|
||||
if ui.button("refresh").clicked() {
|
||||
self.system_info.refresh();
|
||||
}
|
||||
let pid = self
|
||||
.system_info
|
||||
.processes()
|
||||
.iter()
|
||||
.find(|p| p.name() == EXE_ENHANCED || p.name() == EXE_LEGACY)
|
||||
.map_or_else(|| "no pid found!".to_owned(), |p| p.pid().to_string());
|
||||
ui.label(format!("gta pid: {pid}"));
|
||||
});
|
||||
ui.collapsing("app state", |ui| ui.label(format!("{self:#?}")));
|
||||
}
|
||||
|
||||
pub fn show_debug_viewport(&mut self, ctx: &egui::Context) {
|
||||
let main = ctx.input(|i| i.viewport().outer_rect.unwrap_or(egui::Rect::EVERYTHING));
|
||||
let builder = egui::ViewportBuilder::default()
|
||||
.with_title("GTA Tools Debug")
|
||||
.with_minimize_button(false)
|
||||
.with_maximize_button(false)
|
||||
.with_inner_size(WINDOW_SIZE)
|
||||
.with_position([main.right() - 12.0, main.min.y])
|
||||
.with_icon(tools::load_icon());
|
||||
ctx.show_viewport_immediate(
|
||||
egui::ViewportId::from_hash_of("debug_viewport"),
|
||||
builder,
|
||||
|ctx, _class| {
|
||||
if tools::debug_keycombo_pressed(ctx) {
|
||||
self.flags.debug = !self.flags.debug;
|
||||
}
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
egui::ScrollArea::both()
|
||||
.auto_shrink([false, true])
|
||||
.show(ui, |ui| self.add_debug_viewport_contents(ui));
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
+34
-30
@@ -3,54 +3,58 @@ use crate::{
|
||||
app::{App, WINDOW_SIZE},
|
||||
tools,
|
||||
},
|
||||
util::{self, consts::APP_STORAGE_PATH, persistent_state::PersistentState},
|
||||
util::persistent_state::PersistentState,
|
||||
};
|
||||
use eframe::egui;
|
||||
use std::{fs::File, io::Write};
|
||||
use windows::Win32::System::Com::{COINIT_APARTMENTTHREADED, CoInitializeEx};
|
||||
|
||||
fn panic_hook(panic_info: &std::panic::PanicHookInfo<'_>) {
|
||||
let log_path = APP_STORAGE_PATH.join("panic.log");
|
||||
let mut file = File::options()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(log_path)
|
||||
.unwrap();
|
||||
let timestamp = chrono::Local::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, false);
|
||||
let backtrace = std::backtrace::Backtrace::force_capture();
|
||||
let message = format!("[{timestamp}]\n{panic_info}\nstack backtrace:\n{backtrace}\n");
|
||||
file.write_all(message.as_bytes()).unwrap();
|
||||
}
|
||||
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
fn app_creator(
|
||||
cc: &eframe::CreationContext<'_>,
|
||||
) -> Result<Box<dyn eframe::App>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
std::panic::set_hook(Box::new(panic_hook));
|
||||
let mut app = Box::<App>::default();
|
||||
// initialize COM just in case
|
||||
if let Err(e) = unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED) }.ok() {
|
||||
log::error!("couldn't initialize COM: {e}");
|
||||
}
|
||||
// initialize App early to modify some things before returning it
|
||||
let mut app = Box::new(App::default());
|
||||
// load previously selected launch platform & settings from persistent state
|
||||
if let Some(persistent_state) = PersistentState::get() {
|
||||
app.launch.selected = persistent_state.launcher;
|
||||
app.settings = persistent_state.settings;
|
||||
persistent_state.apply_to(&mut app);
|
||||
}
|
||||
let elevated = util::win::is_elevated();
|
||||
if app.settings.start_elevated && !elevated {
|
||||
util::win::elevate(util::win::ElevationExitMethod::Forced);
|
||||
}
|
||||
app.flags.elevated = elevated;
|
||||
// refresh system info because it initializes with nothing
|
||||
app.system_info.refresh();
|
||||
// enable image loading support in egui
|
||||
egui_extras::install_image_loaders(&cc.egui_ctx);
|
||||
cc.egui_ctx.style_mut(|style| {
|
||||
// set our initial theme, from earlier loaded settings. we set the egui theme here to work
|
||||
// around system theme based switching of the egui style
|
||||
cc.egui_ctx.set_theme(egui::Theme::from(app.settings.theme));
|
||||
// apply some global styling that we like
|
||||
cc.egui_ctx.all_styles_mut(|style| {
|
||||
style.spacing.item_spacing = egui::vec2(4.0, 4.0);
|
||||
style.interaction.selectable_labels = false;
|
||||
// style.visuals.hyperlink_color = catppuccin_egui::Theme::from(app.settings.theme).text;
|
||||
});
|
||||
// load any extra fonts that we need
|
||||
let mut fonts = egui::FontDefinitions::default();
|
||||
fonts.font_data.insert(
|
||||
"Ubuntu-Regular".to_owned(),
|
||||
egui::FontData::from_static(include_bytes!("../../assets/Ubuntu-Regular.ttf")).into(),
|
||||
"Inter 18pt Light".to_owned(),
|
||||
egui::FontData::from_static(include_bytes!("../../assets/Inter_18pt-Light.ttf")).into(),
|
||||
);
|
||||
fonts
|
||||
.families
|
||||
.get_mut(&egui::FontFamily::Proportional)
|
||||
.unwrap()
|
||||
.insert(0, "Inter 18pt Light".to_owned());
|
||||
fonts.font_data.insert(
|
||||
"Inter 18pt Regular".to_owned(),
|
||||
egui::FontData::from_static(include_bytes!("../../assets/Inter_18pt-Regular.ttf")).into(),
|
||||
);
|
||||
fonts.families.insert(
|
||||
egui::FontFamily::Name("Ubuntu-Regular".into()),
|
||||
vec!["Ubuntu-Regular".to_owned()],
|
||||
egui::FontFamily::Name("Inter 18pt Regular".into()),
|
||||
vec!["Inter 18pt Regular".to_owned()],
|
||||
);
|
||||
cc.egui_ctx.set_fonts(fonts);
|
||||
// finally return the App
|
||||
Ok(app)
|
||||
}
|
||||
|
||||
|
||||
+42
-30
@@ -1,56 +1,68 @@
|
||||
use crate::util::win;
|
||||
use eframe::egui;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Display;
|
||||
use strum::EnumIter;
|
||||
use strum::{Display, EnumIter};
|
||||
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, EnumIter)]
|
||||
pub const ROCKSTAR_SAVE_SERVER: &str = "192.81.241.171";
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Display, PartialEq, Eq, Serialize, Deserialize, EnumIter)]
|
||||
pub enum Theme {
|
||||
CatppuccinLatte,
|
||||
CatppuccinFrappe,
|
||||
CatppuccinMacchiato,
|
||||
CatppuccinMocha,
|
||||
#[default]
|
||||
#[strum(to_string = "Auto")]
|
||||
Auto,
|
||||
#[strum(to_string = "Light")]
|
||||
Light,
|
||||
#[strum(to_string = "Dark")]
|
||||
Dark,
|
||||
}
|
||||
|
||||
impl From<Theme> for catppuccin_egui::Theme {
|
||||
impl From<Theme> for egui::Theme {
|
||||
fn from(val: Theme) -> Self {
|
||||
match val {
|
||||
Theme::CatppuccinLatte => catppuccin_egui::LATTE,
|
||||
Theme::CatppuccinFrappe => catppuccin_egui::FRAPPE,
|
||||
Theme::CatppuccinMacchiato => catppuccin_egui::MACCHIATO,
|
||||
Theme::CatppuccinMocha => catppuccin_egui::MOCHA,
|
||||
Theme::Auto => {
|
||||
if win::is_system_theme_light() {
|
||||
egui::Theme::Light
|
||||
} else {
|
||||
egui::Theme::Dark
|
||||
}
|
||||
}
|
||||
Theme::Light => egui::Theme::Light,
|
||||
Theme::Dark => egui::Theme::Dark,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Theme {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let x = match self {
|
||||
Self::CatppuccinLatte => "Catppuccin Latte",
|
||||
Self::CatppuccinFrappe => "Catppuccin Frappe",
|
||||
Self::CatppuccinMacchiato => "Catppuccin Macchiato",
|
||||
Self::CatppuccinMocha => "Catppuccin Mocha",
|
||||
};
|
||||
write!(f, "{x}")
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, Default, Display, PartialEq, Eq, Serialize, Deserialize, EnumIter)]
|
||||
pub enum LaunchVersion {
|
||||
#[default]
|
||||
Enhanced,
|
||||
Legacy,
|
||||
}
|
||||
|
||||
impl Theme {
|
||||
pub fn to_catppuccin(self) -> catppuccin_egui::Theme {
|
||||
self.into()
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, Default, Display, PartialEq, Eq, Serialize, Deserialize, EnumIter)]
|
||||
pub enum BlockMethod {
|
||||
#[default]
|
||||
#[strum(to_string = "Entire game")]
|
||||
EntireGame,
|
||||
#[strum(to_string = "Save server")]
|
||||
SaveServer,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Settings {
|
||||
pub theme: Theme,
|
||||
pub start_elevated: bool,
|
||||
pub launch_version: LaunchVersion,
|
||||
pub save_server_ip: String,
|
||||
pub block_method: BlockMethod,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
theme: Theme::CatppuccinMocha,
|
||||
start_elevated: false,
|
||||
theme: Theme::default(),
|
||||
launch_version: LaunchVersion::default(),
|
||||
block_method: BlockMethod::default(),
|
||||
save_server_ip: String::from(ROCKSTAR_SAVE_SERVER),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-27
@@ -11,35 +11,12 @@ pub fn load_icon() -> egui::IconData {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn header(ui: &mut egui::Ui, text: &str) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(egui::RichText::new(text).font(egui::FontId::new(
|
||||
12.5,
|
||||
egui::FontFamily::Name("Ubuntu-Regular".into()),
|
||||
)));
|
||||
ui.add(egui::Separator::default().horizontal());
|
||||
});
|
||||
pub fn debug_keycombo_pressed(ui: &egui::Ui) -> bool {
|
||||
ui.input(|i| i.modifiers.all() && i.key_pressed(egui::Key::D))
|
||||
}
|
||||
|
||||
pub fn build_combo_box<E>(ui: &mut egui::Ui, current_value: &mut E, label: impl std::hash::Hash)
|
||||
where
|
||||
E: strum::IntoEnumIterator + std::fmt::Display + std::cmp::PartialEq + Copy,
|
||||
{
|
||||
egui::ComboBox::from_id_salt(label)
|
||||
.selected_text(current_value.to_string())
|
||||
.show_ui(ui, |ui| {
|
||||
E::iter().for_each(|v| {
|
||||
ui.selectable_value(current_value, v, v.to_string());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn check_debug_keycombo_pressed(ctx: &egui::Context) -> bool {
|
||||
ctx.input(|i| i.modifiers.all() && i.key_pressed(egui::Key::D))
|
||||
}
|
||||
|
||||
pub fn check_debug_viewport_close_button_pressed(ctx: &egui::Context) -> bool {
|
||||
ctx.input(|i| {
|
||||
pub fn debug_viewport_close_pressed(ui: &egui::Ui) -> bool {
|
||||
ui.input(|i| {
|
||||
i.raw
|
||||
.viewports
|
||||
.get(&egui::ViewportId::from_hash_of("debug_viewport"))
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
use eframe::egui;
|
||||
|
||||
pub trait UiExt {
|
||||
fn header(&mut self, text: &str);
|
||||
fn build_menu<E>(&mut self, current_value: &mut E)
|
||||
where
|
||||
E: strum::IntoEnumIterator + std::fmt::Display + std::cmp::PartialEq + Copy;
|
||||
fn create_indicator_dot(&mut self, colour: impl Into<egui::Color32>) -> egui::Response;
|
||||
}
|
||||
|
||||
impl UiExt for egui::Ui {
|
||||
fn header(&mut self, text: &str) {
|
||||
self.horizontal(|ui| {
|
||||
ui.label(egui::RichText::new(text).font(egui::FontId::new(
|
||||
12.5,
|
||||
egui::FontFamily::Name("Inter 18pt Regular".into()),
|
||||
)));
|
||||
ui.add(egui::Separator::default().horizontal());
|
||||
});
|
||||
}
|
||||
|
||||
fn build_menu<E>(&mut self, current_value: &mut E)
|
||||
where
|
||||
E: strum::IntoEnumIterator + std::fmt::Display + std::cmp::PartialEq + Copy,
|
||||
{
|
||||
E::iter().for_each(|variant| {
|
||||
self.selectable_value(current_value, variant, variant.to_string());
|
||||
});
|
||||
}
|
||||
|
||||
fn create_indicator_dot(&mut self, colour: impl Into<egui::Color32>) -> egui::Response {
|
||||
self.add(
|
||||
egui::Image::new(egui::include_image!("../../assets/circle.svg"))
|
||||
.max_size([4.0, 4.0].into())
|
||||
.tint(colour),
|
||||
)
|
||||
}
|
||||
}
|
||||
+20
@@ -4,6 +4,26 @@ mod features;
|
||||
mod gui;
|
||||
mod util;
|
||||
|
||||
use std::fmt::Write;
|
||||
|
||||
fn init_storage() {
|
||||
if !util::consts::path::APP_STORAGE.exists() {
|
||||
std::fs::create_dir_all(util::consts::path::APP_STORAGE.as_path()).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn panic_hook(panic_info: &std::panic::PanicHookInfo<'_>) {
|
||||
let backtrace = std::backtrace::Backtrace::capture();
|
||||
let mut message = format!("{panic_info}");
|
||||
if backtrace.status() == std::backtrace::BacktraceStatus::Captured {
|
||||
write!(message, "\nstack backtrace:\n{backtrace}").unwrap();
|
||||
}
|
||||
log::error!("{message}");
|
||||
}
|
||||
|
||||
fn main() {
|
||||
init_storage();
|
||||
util::logging::Logger::init(log::LevelFilter::Info);
|
||||
std::panic::set_hook(Box::new(panic_hook));
|
||||
gui::run::run();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
pub mod codeberg;
|
||||
pub mod consts;
|
||||
pub mod countdown;
|
||||
pub mod meta;
|
||||
pub mod firewall;
|
||||
pub mod logging;
|
||||
pub mod persistent_state;
|
||||
pub mod system_info;
|
||||
pub mod win;
|
||||
@@ -1,35 +0,0 @@
|
||||
use semver::Version;
|
||||
|
||||
const APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
|
||||
const CODEBERG_ENDPOINT_ROOT: &str = "https://codeberg.org/api/v1";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Release {
|
||||
pub version: Version,
|
||||
pub download_url: String,
|
||||
}
|
||||
|
||||
impl Default for Release {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
version: Version::parse(env!("CARGO_PKG_VERSION")).unwrap(),
|
||||
download_url: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_latest_release() -> Option<Release> {
|
||||
let request_url = format!("{CODEBERG_ENDPOINT_ROOT}/repos/futile/gta-tools/releases/latest");
|
||||
let ureq: ureq::Agent = ureq::Agent::config_builder()
|
||||
.user_agent(APP_USER_AGENT)
|
||||
.build()
|
||||
.into();
|
||||
let mut response = ureq.get(request_url).call().ok()?;
|
||||
let json = response.body_mut().read_json::<serde_json::Value>().ok()?;
|
||||
let tag_name = json["tag_name"].as_str()?;
|
||||
let browser_download_url = json["assets"][0]["browser_download_url"].as_str()?;
|
||||
Some(Release {
|
||||
version: Version::parse(tag_name).expect("expected a valid semver pattern"),
|
||||
download_url: String::from(browser_download_url),
|
||||
})
|
||||
}
|
||||
+19
-6
@@ -1,9 +1,22 @@
|
||||
use std::{path::PathBuf, sync::LazyLock};
|
||||
pub mod path {
|
||||
use std::{env, path::PathBuf, sync::LazyLock};
|
||||
pub static APP_STORAGE: LazyLock<PathBuf> = LazyLock::new(|| {
|
||||
PathBuf::from(env::var("LOCALAPPDATA").unwrap_or_else(|_| String::from(".")))
|
||||
.join("GTA Tools")
|
||||
});
|
||||
pub static APP_CONFIG: LazyLock<PathBuf> = LazyLock::new(|| APP_STORAGE.join("config.json"));
|
||||
pub static APP_LOG: LazyLock<PathBuf> = LazyLock::new(|| APP_STORAGE.join("gta-tools.log"));
|
||||
}
|
||||
|
||||
pub const ENHANCED: &str = "GTA5_Enhanced.exe";
|
||||
pub const LEGACY: &str = "GTA5.exe";
|
||||
pub mod game {
|
||||
pub const EXE_ENHANCED: &str = "GTA5_Enhanced.exe";
|
||||
pub const EXE_LEGACY: &str = "GTA5.exe";
|
||||
pub const WINDOW_TITLE: &str = "Grand Theft Auto V";
|
||||
}
|
||||
|
||||
pub const GTA_WINDOW_TITLE: &str = "Grand Theft Auto V";
|
||||
pub mod colours {
|
||||
use eframe::egui;
|
||||
|
||||
pub static APP_STORAGE_PATH: LazyLock<PathBuf> =
|
||||
LazyLock::new(|| dirs::config_local_dir().unwrap().join("GTA Tools"));
|
||||
pub const RED: egui::Color32 = egui::Color32::from_rgb(255, 96, 96);
|
||||
pub const GREEN: egui::Color32 = egui::Color32::from_rgb(96, 255, 96);
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@ use std::time::{Duration, Instant};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Countdown {
|
||||
pub i: u64,
|
||||
pub i_original: u64,
|
||||
i: u64,
|
||||
i_original: u64,
|
||||
pub i_string: String,
|
||||
pub interval: Instant,
|
||||
pub first_count: bool,
|
||||
interval: Instant,
|
||||
first_count: bool,
|
||||
}
|
||||
|
||||
impl Countdown {
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
use anyhow::Result;
|
||||
use std::path::PathBuf;
|
||||
use windows::{
|
||||
Win32::{
|
||||
NetworkManagement::WindowsFirewall::{
|
||||
INetFwPolicy2, INetFwRule, NET_FW_ACTION_BLOCK, NET_FW_IP_PROTOCOL_ANY,
|
||||
NET_FW_IP_PROTOCOL_TCP, NET_FW_IP_PROTOCOL_UDP, NET_FW_RULE_DIR_IN,
|
||||
NET_FW_RULE_DIR_OUT, NetFwPolicy2, NetFwRule,
|
||||
},
|
||||
System::Com::{CLSCTX_INPROC_SERVER, CoCreateInstance},
|
||||
},
|
||||
core::BSTR,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Firewall {
|
||||
policy: INetFwPolicy2,
|
||||
}
|
||||
|
||||
impl Default for Firewall {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
policy: unsafe { CoCreateInstance(&NetFwPolicy2, None, CLSCTX_INPROC_SERVER) }.unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Firewall {
|
||||
pub fn add(
|
||||
&self,
|
||||
name: &str,
|
||||
mode: RuleMode,
|
||||
direction: RuleDirection,
|
||||
protocol: RuleProtocol,
|
||||
) -> Result<()> {
|
||||
let add_rule = || {
|
||||
let rules = unsafe { self.policy.Rules() }?;
|
||||
unsafe { rules.Remove(&BSTR::from(name)) }?;
|
||||
let rule: INetFwRule =
|
||||
unsafe { CoCreateInstance(&NetFwRule, None, CLSCTX_INPROC_SERVER) }?;
|
||||
unsafe { rule.SetName(&BSTR::from(name)) }?;
|
||||
match mode {
|
||||
RuleMode::Executable(exe) => {
|
||||
unsafe { rule.SetApplicationName(&BSTR::from(exe.to_string_lossy().as_ref())) }?
|
||||
}
|
||||
RuleMode::Address(ip) => unsafe { rule.SetRemoteAddresses(&BSTR::from(ip)) }?,
|
||||
}
|
||||
match direction {
|
||||
RuleDirection::In => unsafe { rule.SetDirection(NET_FW_RULE_DIR_IN) }?,
|
||||
RuleDirection::Out => unsafe { rule.SetDirection(NET_FW_RULE_DIR_OUT) }?,
|
||||
}
|
||||
unsafe { rule.SetEnabled(true.into()) }?;
|
||||
unsafe { rule.SetAction(NET_FW_ACTION_BLOCK) }?;
|
||||
match protocol {
|
||||
RuleProtocol::Any => unsafe { rule.SetProtocol(NET_FW_IP_PROTOCOL_ANY.0) }?,
|
||||
RuleProtocol::Tcp => unsafe { rule.SetProtocol(NET_FW_IP_PROTOCOL_TCP.0) }?,
|
||||
RuleProtocol::Udp => unsafe { rule.SetProtocol(NET_FW_IP_PROTOCOL_UDP.0) }?,
|
||||
}
|
||||
unsafe { rules.Add(&rule) }?;
|
||||
Ok(())
|
||||
};
|
||||
add_rule().inspect_err(|e| log::warn!("Failed to add rule '{name}': {e}"))
|
||||
}
|
||||
|
||||
pub fn remove(&self, name: &str) -> Result<()> {
|
||||
let remove_rule = || {
|
||||
let rules = unsafe { self.policy.Rules() }?;
|
||||
unsafe { rules.Remove(&BSTR::from(name)) }?;
|
||||
Ok(())
|
||||
};
|
||||
remove_rule().inspect_err(|e| log::warn!("Failed to remove rule '{name}': {e}"))
|
||||
}
|
||||
|
||||
pub fn is_blocked(&self, name: &str) -> Result<bool> {
|
||||
let rules = unsafe { self.policy.Rules() }?;
|
||||
let rule_exists = unsafe { rules.Item(&BSTR::from(name)) }.is_ok();
|
||||
Ok(rule_exists)
|
||||
}
|
||||
}
|
||||
|
||||
pub enum RuleMode {
|
||||
Executable(PathBuf),
|
||||
Address(String),
|
||||
}
|
||||
|
||||
pub enum RuleDirection {
|
||||
In,
|
||||
Out,
|
||||
}
|
||||
|
||||
pub enum RuleProtocol {
|
||||
Any,
|
||||
#[allow(unused)]
|
||||
Tcp,
|
||||
Udp,
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
use std::{
|
||||
fs::File,
|
||||
io::Write,
|
||||
sync::{LazyLock, Mutex},
|
||||
time::SystemTime,
|
||||
};
|
||||
|
||||
pub struct Logger {
|
||||
file: Mutex<File>,
|
||||
}
|
||||
|
||||
static LOGGER: LazyLock<Logger> = LazyLock::new(|| Logger {
|
||||
file: File::options()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(crate::util::consts::path::APP_LOG.as_path())
|
||||
.unwrap()
|
||||
.into(),
|
||||
});
|
||||
|
||||
impl log::Log for Logger {
|
||||
fn enabled(&self, metadata: &log::Metadata) -> bool {
|
||||
metadata.level() <= log::Level::Info
|
||||
}
|
||||
|
||||
fn flush(&self) {}
|
||||
|
||||
fn log(&self, record: &log::Record) {
|
||||
if self.enabled(record.metadata()) {
|
||||
Self::log_to_stderr(record);
|
||||
self.log_to_file(record);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Logger {
|
||||
pub fn init(level: log::LevelFilter) {
|
||||
log::set_logger(&*LOGGER)
|
||||
.map(|()| log::set_max_level(level))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn log_to_file(&self, record: &log::Record) {
|
||||
let mut file = self.file.lock().unwrap();
|
||||
writeln!(
|
||||
file,
|
||||
"[{}][{}] {}",
|
||||
humantime::format_rfc3339_seconds(SystemTime::now()),
|
||||
record.level(),
|
||||
record.args()
|
||||
)
|
||||
.unwrap();
|
||||
file.flush().unwrap();
|
||||
}
|
||||
|
||||
fn log_to_stderr(record: &log::Record) {
|
||||
eprintln!(
|
||||
"[{}][{}] {}",
|
||||
humantime::format_rfc3339_seconds(SystemTime::now()),
|
||||
record.level(),
|
||||
record.args()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
use crate::util::codeberg;
|
||||
use semver::Version;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Meta {
|
||||
pub current_version: Version,
|
||||
pub latest_release: codeberg::Release,
|
||||
pub newer_version_available: bool,
|
||||
}
|
||||
|
||||
impl Default for Meta {
|
||||
fn default() -> Self {
|
||||
let current_version = Version::parse(env!("CARGO_PKG_VERSION")).unwrap();
|
||||
let latest_release = codeberg::get_latest_release().unwrap_or_default();
|
||||
let newer_version_available = matches!(
|
||||
¤t_version.cmp_precedence(&latest_release.version),
|
||||
std::cmp::Ordering::Less
|
||||
);
|
||||
Self {
|
||||
current_version,
|
||||
latest_release,
|
||||
newer_version_available,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,35 +1,42 @@
|
||||
use crate::{features::launch::Platform, gui::settings::Settings, util::consts::APP_STORAGE_PATH};
|
||||
use crate::{
|
||||
features::launch::Platform,
|
||||
gui::{app, settings::Settings},
|
||||
util::consts::path,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
io::Write,
|
||||
path::PathBuf,
|
||||
sync::LazyLock,
|
||||
};
|
||||
|
||||
static CONFIG_PATH: LazyLock<PathBuf> = LazyLock::new(|| APP_STORAGE_PATH.join("config.json"));
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct PersistentState {
|
||||
pub launcher: Platform,
|
||||
pub anti_afk_enabled: bool,
|
||||
pub settings: Settings,
|
||||
}
|
||||
|
||||
impl PersistentState {
|
||||
pub fn get() -> Option<Self> {
|
||||
fs::read_to_string(CONFIG_PATH.as_path())
|
||||
fs::read_to_string(path::APP_CONFIG.as_path())
|
||||
.ok()
|
||||
.and_then(|config| serde_json::from_str::<Self>(&config).ok())
|
||||
}
|
||||
|
||||
pub fn set(&self) {
|
||||
let config_path = CONFIG_PATH.as_path();
|
||||
let config_path_parent = config_path.parent().unwrap();
|
||||
if !config_path_parent.exists() {
|
||||
fs::create_dir(config_path_parent).unwrap();
|
||||
}
|
||||
let mut config_file = File::create(config_path).unwrap();
|
||||
let mut config_file = File::create(path::APP_CONFIG.as_path()).unwrap();
|
||||
let json = serde_json::to_string_pretty(&self).unwrap();
|
||||
config_file.write_all(json.as_bytes()).unwrap();
|
||||
}
|
||||
|
||||
pub fn apply_to(self, app: &mut app::App) {
|
||||
let Self {
|
||||
launcher,
|
||||
anti_afk_enabled,
|
||||
settings,
|
||||
} = self;
|
||||
app.launch.selected = launcher;
|
||||
app.anti_afk.enabled = anti_afk_enabled;
|
||||
app.settings = settings;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
use std::{
|
||||
ffi::{OsStr, OsString},
|
||||
os::windows::{ffi::OsStringExt, process::CommandExt},
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
use windows::{
|
||||
Win32::System::{
|
||||
Diagnostics::ToolHelp::{
|
||||
CreateToolhelp32Snapshot, PROCESSENTRY32W, Process32FirstW, Process32NextW,
|
||||
TH32CS_SNAPPROCESS,
|
||||
},
|
||||
Threading::{
|
||||
CREATE_NO_WINDOW, OpenProcess, PROCESS_NAME_WIN32, PROCESS_QUERY_LIMITED_INFORMATION,
|
||||
QueryFullProcessImageNameW,
|
||||
},
|
||||
},
|
||||
core::PWSTR,
|
||||
};
|
||||
|
||||
use crate::util::consts::game::{EXE_ENHANCED, EXE_LEGACY};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Process {
|
||||
pid: u32,
|
||||
name: OsString,
|
||||
exe: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl Process {
|
||||
pub const fn pid(&self) -> u32 {
|
||||
self.pid
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &OsStr {
|
||||
&self.name
|
||||
}
|
||||
|
||||
pub fn exe(&self) -> Option<&Path> {
|
||||
self.exe.as_deref()
|
||||
}
|
||||
|
||||
pub fn kill(&self) -> bool {
|
||||
let mut taskkill = Command::new("taskkill.exe");
|
||||
taskkill.creation_flags(CREATE_NO_WINDOW.0);
|
||||
taskkill.arg("/F").arg("/PID").arg(self.pid.to_string());
|
||||
match taskkill.output() {
|
||||
Ok(output) => output.status.success(),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct SystemInfo {
|
||||
processes: Vec<Process>,
|
||||
}
|
||||
|
||||
impl SystemInfo {
|
||||
pub fn refresh(&mut self) {
|
||||
let mut processes = Vec::new();
|
||||
let snapshot_handle = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) }.unwrap();
|
||||
let mut process_entry = PROCESSENTRY32W {
|
||||
dwSize: size_of::<PROCESSENTRY32W>() as u32,
|
||||
..Default::default()
|
||||
};
|
||||
unsafe { Process32FirstW(snapshot_handle, &raw mut process_entry) }.unwrap();
|
||||
let exe_full_path = get_exe_full_path(&process_entry);
|
||||
processes.push(Process {
|
||||
pid: process_entry.th32ProcessID,
|
||||
name: wide_array_to_os_string(&process_entry.szExeFile),
|
||||
exe: exe_full_path,
|
||||
});
|
||||
while unsafe { Process32NextW(snapshot_handle, &raw mut process_entry) }.is_ok() {
|
||||
let exe_full_path = get_exe_full_path(&process_entry);
|
||||
processes.push(Process {
|
||||
pid: process_entry.th32ProcessID,
|
||||
name: wide_array_to_os_string(&process_entry.szExeFile),
|
||||
exe: exe_full_path,
|
||||
});
|
||||
}
|
||||
self.processes = processes;
|
||||
}
|
||||
|
||||
pub fn get_game_exe_path(&mut self) -> Option<&Path> {
|
||||
self.refresh();
|
||||
self.processes()
|
||||
.iter()
|
||||
.find(|p| p.name() == EXE_ENHANCED || p.name() == EXE_LEGACY)
|
||||
.and_then(|p| p.exe())
|
||||
}
|
||||
|
||||
pub fn processes(&self) -> &[Process] {
|
||||
&self.processes
|
||||
}
|
||||
}
|
||||
|
||||
fn get_exe_full_path(process_entry: &PROCESSENTRY32W) -> Option<PathBuf> {
|
||||
let process_handle_result = unsafe {
|
||||
OpenProcess(
|
||||
PROCESS_QUERY_LIMITED_INFORMATION,
|
||||
false,
|
||||
process_entry.th32ProcessID,
|
||||
)
|
||||
};
|
||||
process_handle_result.map_or(None, |process_handle| {
|
||||
let mut exe_name = [0u16; 260];
|
||||
let mut dw_size = exe_name.len() as u32;
|
||||
let image_name_result = unsafe {
|
||||
QueryFullProcessImageNameW(
|
||||
process_handle,
|
||||
PROCESS_NAME_WIN32,
|
||||
PWSTR(exe_name.as_mut_ptr()),
|
||||
&raw mut dw_size,
|
||||
)
|
||||
};
|
||||
match image_name_result {
|
||||
Ok(()) => Some(PathBuf::from(wide_array_to_os_string(&exe_name))),
|
||||
Err(_) => None,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn wide_array_to_os_string(wide: &[u16]) -> OsString {
|
||||
let null_pos = wide.iter().position(|&x| x == 0).unwrap_or(wide.len());
|
||||
OsString::from_wide(&wide[..null_pos])
|
||||
}
|
||||
+25
-67
@@ -1,86 +1,44 @@
|
||||
use windows::{
|
||||
Win32::{
|
||||
Foundation::{CloseHandle, HANDLE},
|
||||
Security::{GetTokenInformation, TOKEN_ELEVATION, TOKEN_QUERY, TokenElevation},
|
||||
System::Threading::{GetCurrentProcess, OpenProcessToken},
|
||||
UI::{
|
||||
Input::KeyboardAndMouse::GetAsyncKeyState,
|
||||
Shell::ShellExecuteW,
|
||||
WindowsAndMessaging::{
|
||||
CURSOR_SHOWING, CURSORINFO, GetCursorInfo, GetForegroundWindow, GetWindowTextW,
|
||||
SW_NORMAL,
|
||||
},
|
||||
},
|
||||
use windows::Win32::UI::{
|
||||
Input::KeyboardAndMouse::{GetAsyncKeyState, VIRTUAL_KEY},
|
||||
WindowsAndMessaging::{
|
||||
CURSOR_SHOWING, CURSORINFO, GetCursorInfo, GetForegroundWindow, GetWindowTextW,
|
||||
},
|
||||
core::{HSTRING, PCWSTR},
|
||||
};
|
||||
|
||||
pub enum ElevationExitMethod<'a> {
|
||||
Gentle(&'a mut bool),
|
||||
Forced,
|
||||
}
|
||||
|
||||
pub fn is_cursor_visible() -> bool {
|
||||
let mut ci = CURSORINFO {
|
||||
cbSize: u32::try_from(std::mem::size_of::<CURSORINFO>()).unwrap(),
|
||||
..Default::default()
|
||||
};
|
||||
unsafe {
|
||||
GetCursorInfo(&mut ci).unwrap();
|
||||
}
|
||||
unsafe { GetCursorInfo(&raw mut ci) }.unwrap();
|
||||
ci.flags == CURSOR_SHOWING
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_sign_loss)]
|
||||
pub fn is_window_focused(target_title: &str) -> bool {
|
||||
let mut buffer = [0; 512];
|
||||
unsafe {
|
||||
let hwnd = GetForegroundWindow();
|
||||
let length = GetWindowTextW(hwnd, &mut buffer);
|
||||
let current_title = String::from_utf16_lossy(&buffer[..length as usize]);
|
||||
current_title == target_title
|
||||
}
|
||||
let hwnd = unsafe { GetForegroundWindow() };
|
||||
let length = unsafe { GetWindowTextW(hwnd, &mut buffer) };
|
||||
let current_title = String::from_utf16_lossy(&buffer[..length as usize]);
|
||||
current_title == target_title
|
||||
}
|
||||
|
||||
pub fn is_any_key_pressed(keys: &[u8]) -> bool {
|
||||
pub fn is_any_key_pressed(keys: &[VIRTUAL_KEY]) -> bool {
|
||||
keys.iter()
|
||||
.any(|&key| unsafe { (GetAsyncKeyState(i32::from(key)) & i16::MIN) != 0 })
|
||||
.any(|&key| unsafe { GetAsyncKeyState(i32::from(key.0)) } & i16::MIN != 0)
|
||||
}
|
||||
|
||||
pub fn elevate(closing: ElevationExitMethod) {
|
||||
let exe = std::env::current_exe().unwrap();
|
||||
unsafe {
|
||||
ShellExecuteW(
|
||||
None,
|
||||
&HSTRING::from("runas"),
|
||||
&HSTRING::from(exe.as_path()),
|
||||
PCWSTR::null(),
|
||||
PCWSTR::null(),
|
||||
SW_NORMAL,
|
||||
);
|
||||
}
|
||||
match closing {
|
||||
ElevationExitMethod::Gentle(closing) => *closing = true,
|
||||
ElevationExitMethod::Forced => std::process::exit(0),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_elevated() -> bool {
|
||||
let mut token: HANDLE = HANDLE::default();
|
||||
unsafe {
|
||||
if OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token).is_err() {
|
||||
return false;
|
||||
}
|
||||
let mut elevation = TOKEN_ELEVATION::default();
|
||||
let mut size = u32::try_from(std::mem::size_of::<TOKEN_ELEVATION>()).unwrap();
|
||||
let result = GetTokenInformation(
|
||||
token,
|
||||
TokenElevation,
|
||||
Some((&raw mut elevation).cast()),
|
||||
size,
|
||||
&mut size,
|
||||
);
|
||||
CloseHandle(token).unwrap();
|
||||
result.is_ok() && elevation.TokenIsElevated != 0
|
||||
}
|
||||
pub fn is_system_theme_light() -> bool {
|
||||
use winreg::RegKey;
|
||||
let hkcu = RegKey::predef(winreg::enums::HKEY_CURRENT_USER);
|
||||
let Ok(subkey) =
|
||||
hkcu.open_subkey("Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize")
|
||||
else {
|
||||
return true;
|
||||
};
|
||||
let Ok(apps_use_light_theme): Result<u32, std::io::Error> =
|
||||
subkey.get_value("AppsUseLightTheme")
|
||||
else {
|
||||
return true;
|
||||
};
|
||||
apps_use_light_theme == 1
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user