62 Commits
Author SHA1 Message Date
futile b3158f71fb 0.6.0 2025-05-10 07:04:59 +01:00
futile 070e477fd4 derive default on some shit instead of manually 2025-05-10 06:57:26 +01:00
futile 7745bba10e improve styling in relation to windows theme 2025-05-10 06:44:58 +01:00
futile 8d7a0d12b9 don't force capture backtrace in panic hook & otherwise improve gui::run 2025-05-04 18:36:32 +01:00
futile b325ebb03c clippy stuff 2025-05-04 18:21:55 +01:00
futile 38c6402a12 split ui of App::show_debug_viewport into App::add_debug_viewport_contents 2025-05-04 18:01:50 +01:00
futile 2d42c0c3c4 improve util::codeberg 2025-05-04 17:52:50 +01:00
futile a423721aee bump nyquest and stop using .json() workaround 2025-05-04 17:52:06 +01:00
futile f3436c8ddc 0.5.4 2025-05-03 13:12:47 +01:00
futile afcf0a87c0 get sysinfo from crates again, instead of git repo 2025-05-03 13:12:14 +01:00
futile 73d610949f use nyquest instead of ureq for http client 2025-05-03 13:10:40 +01:00
futile 7f358d9adb change how module roots are organized 2025-05-01 11:23:27 +01:00
futile 7caa5421f7 eliminate chrono dependency by using unix timestamp in panic logs 2025-04-30 01:36:17 +01:00
futile ecac0d54aa eliminate dirs dependency by checking LOCALAPPDATA envvar instead 2025-04-30 00:53:27 +01:00
futile a84401ca33 fail compilation if compiling outside of windows 2025-04-30 00:48:44 +01:00
futile 1bcd6b2c7d fix duplicate inputs bug in features::anti_afk::send 2025-04-26 05:47:10 +01:00
futile 1dbdc0e4cb remove redundant method to_color32 2025-04-26 04:42:29 +01:00
futile 3610e44a08 create trait UiExt 2025-04-26 04:37:10 +01:00
futile f5fd22d35c redesign if_failed_return_to_unblocked to if_failed_return_to_boolean 2025-04-26 04:23:37 +01:00
futile 022828b802 update readme 2025-04-26 04:07:39 +01:00
futile 262d2d0720 update readme 2025-04-25 17:18:38 +00:00
futile 2326edf97b update readme with a guide 2025-04-24 23:43:52 +01:00
futile 301be82837 update screenshot in readme 2025-04-24 13:54:39 +01:00
futile 0d7dde929d 0.5.3 2025-04-24 13:24:35 +01:00
futile 3ab802fd56 make RED, YELLOW, and GREEN slightly brighter 2025-04-23 14:33:08 +01:00
futile 53030cc5f7 add debug menu feature to change blocked_status of GameNetworking 2025-04-23 14:32:29 +01:00
futile 383224c2fa update readme 2025-04-23 14:31:50 +01:00
futile 575a6a4f1d update readme 2025-04-22 20:03:09 +01:00
futile 17579de277 update readme 2025-04-22 19:57:09 +01:00
futile 00727b9913 add compilation instructions to the repo 2025-04-22 14:34:37 +01:00
futile 1f6fc22525 check for app elevation in Default of Flags 2025-04-21 12:38:20 +01:00
futile 27b09be76e update screenshot in readme 2025-04-20 12:06:57 +01:00
futile 6f15e8c348 0.5.2 2025-04-20 10:52:21 +01:00
futile d294aa9861 reduce amount of underlying dependencies 2025-04-20 10:48:32 +01:00
futile f688f1c3c4 strip = true doesn't do anything extra anymore 2025-04-20 10:15:33 +01:00
futile 58ec9f6567 clippy 2025-04-20 08:01:41 +01:00
futile 1c9ce2a4d3 remember to place to_color32 for BlockedStatus in the correct place 2025-04-20 07:57:41 +01:00
futile 8a5d6cf38c simplify features::game_networking 2025-04-20 07:56:54 +01:00
futile d789b0f62d rework networking indicator dot 2025-04-20 07:33:34 +01:00
futile 6079a72f47 use SendInput instead of keybd_event for anti afk 2025-04-20 07:07:51 +01:00
futile 914ffe46d2 add tooltip to game networking indicator 2025-04-20 06:55:43 +01:00
futile 935703e1ab listen to a few clippy lints 2025-04-20 06:19:29 +01:00
futile 202554e03b 0.5.1 2025-04-20 01:47:16 +01:00
futile da2b15a0b6 redesign features::force_close internals a bit 2025-04-20 01:42:06 +01:00
futile ccc27b1944 slightly tweak features::game_networking design & add visual indicator for blocking failing due to game being closed 2025-04-20 01:25:25 +01:00
futile 10168e84d1 update readme 2025-04-19 10:52:20 +01:00
futile e72964a9f8 0.5.0 2025-04-19 10:37:06 +01:00
futile 18052d0c78 add red/green circle indicator beside Block/Unblock buttons 2025-04-19 10:35:47 +01:00
futile 09f5a3895b use windows api instead of netsh command to manage game's network access 2025-04-19 10:34:10 +01:00
futile 89892ef592 change a couple of imports in gui::app 2025-04-19 07:20:49 +01:00
futile 61de3aeee3 move debug viewport code to own module 2025-04-19 06:48:56 +01:00
futile 4ed00e6ac3 don't assign PersistentState to variable when setting 2025-04-19 05:23:11 +01:00
futile 332db0ea03 document the process of gui::run::app_creator with comments 2025-04-19 05:18:04 +01:00
futile 64d13e3964 move EmptySession::run_timers back into main update loop so it actually runs 2025-04-19 05:02:24 +01:00
futile feb3a793d9 redesign how anti afk activates 2025-04-19 04:54:02 +01:00
futile 585f454bc0 hunt down some unused pubs 2025-04-19 04:45:47 +01:00
futile 27524e855a numerous improvements in gui and feature code 2025-04-19 04:28:24 +01:00
futile 0257834d04 add checkbox to debug to spoof new version of the app beign available 2025-04-18 20:32:39 +01:00
futile 64e94bf790 change how vks are stored 2025-04-18 05:26:28 +01:00
futile 668a6eb38b get rid of clippy-related allows 2025-04-18 04:44:11 +01:00
futile 0de79769d2 reorganize some consts 2025-04-18 03:17:40 +01:00
futile b79c5f62b3 update screenshot in readme 2025-04-18 03:09:14 +01:00
24 changed files with 928 additions and 2212 deletions
Generated
+239 -1801
View File
File diff suppressed because it is too large Load Diff
+13 -8
View File
@@ -1,30 +1,36 @@
[package] [package]
name = "gta-tools" name = "gta-tools"
version = "0.4.2" version = "0.6.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
catppuccin-egui = { version = "5.5.0", default-features = false, features = [ catppuccin-egui = { version = "5.5.0", default-features = false, features = [
"egui31", "egui31",
] } ] }
chrono = "0.4.40" eframe = { version = "0.31.1", default-features = false, features = [
dirs = "6.0.0" "glow",
eframe = "0.31.1" "default_fonts",
egui_extras = { version = "0.31.1", features = ["image"] } ] }
egui_extras = { version = "0.31.1", default-features = false, features = [
"image",
"svg",
] }
image = { version = "0.25.6", default-features = false, features = ["png"] } image = { version = "0.25.6", default-features = false, features = ["png"] }
nyquest = { version = "0.1.1", features = ["blocking", "json"] }
nyquest-backend-winrt = { version = "0.1.0", features = ["blocking"] }
open = "5.3.2" open = "5.3.2"
semver = "1.0.26" semver = "1.0.26"
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140" serde_json = "1.0.140"
strum = { version = "0.27.1", features = ["derive"] } strum = { version = "0.27.1", features = ["derive"] }
sysinfo = { git = "https://github.com/GuillaumeGomez/sysinfo", rev = "bcbb7b2b389b8800ff457773f48b130e710458fc" } sysinfo = "0.35.0"
ureq = { version = "3.0.11", features = ["json"] }
windows = { version = "0.61.1", features = [ windows = { version = "0.61.1", features = [
"Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Input_KeyboardAndMouse",
"Win32_System_Threading", "Win32_System_Threading",
"Win32_UI_Shell", "Win32_UI_Shell",
"Win32_UI_WindowsAndMessaging", "Win32_UI_WindowsAndMessaging",
"Win32_Security", "Win32_Security",
"Win32_NetworkManagement_WindowsFirewall",
] } ] }
winreg = "0.55.0" winreg = "0.55.0"
@@ -33,6 +39,5 @@ static_vcruntime = "2.0.0"
winresource = "0.1.20" winresource = "0.1.20"
[profile.release] [profile.release]
strip = true
lto = true lto = true
codegen-units = 1 codegen-units = 1
+59 -2
View File
@@ -1,4 +1,61 @@
# GTA Tools # GTA Tools
A toolset of convenient things for GTA 5 Online.
![](https://i.vgy.me/n6CrZi.png) A toolset of convenient things for GTA V Online.
![](https://i.vgy.me/aQ1qgw.png)
## Installing
**Option 1** — <ins>Download</ins>
Download the latest release [here](https://codeberg.org/futile/gta-tools/releases/download/latest/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 the Rust toolchain, which can be obtained [here](https://rustup.rs). Follow the instructions of its installer. Once you have Rust installed, 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**.
## Guide
Almost every[^1] feature of GTA Tools is Legacy/Enhanced-agnostic. Some functionality of GTA Tools requires administrator access. If necessary, GTA Tools can either be started as admin manually, or, the user can simply use the <kbd>Elevate</kbd> button to relaunch GTA Tools as admin.
It is recommended to use an up-to-date version of GTA Tools from [releases](https://codeberg.org/futile/gta-tools/releases). You can check for updates to your existing installation by going to the <kbd>About</kbd> page of GTA Tools and hovering the <kbd>↓</kbd> button near the version number.
#### Game
This section is quite simple.
There is a <kbd>Launch</kbd>[^1] feature, which will start your game on the chosen launcher (I am unsure if launching Epic Games works, let me know).
There is also a <kbd>Force close game</kbd> feature, which simply kills all game processes. 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.
The first being <kbd>Empty current session</kbd>, which "removes everyone" from your session by suspending your game for 10 seconds, and then resuming it. This method is exceedingly similar to the Resource Monitor method, but made convenient in one button. 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
The other feature is <kbd>Anti AFK</kbd>, which, when toggled, 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
The last section has a single feature, <kbd>Game's network access</kbd>, with two buttons, <kbd>Block</kbd> and <kbd>Unblock</kbd>, and a coloured indicator dot. This feature requires administrator, and blocks/unblocks GTA V's entire internet access using the Windows Firewall. The coloured indicator dot has these meanings:
* **Green** - the game is connected to the internet
* **Yellow** - the game could not be found running, and thus was not blocked
* **Red** - the game is blocked from the internet
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. Here's how you can do a replay glitch:
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
[^1]: <kbd>Launch</kbd> only launches Enhanced installations of the game.
[^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.
+23
View File
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="512"
height="512"
viewBox="0 0 512 512"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="layer1">
<circle
style="fill:#ffffff"
id="path1"
cx="256"
cy="256"
r="256" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 458 B

+34 -28
View File
@@ -1,13 +1,12 @@
#![allow(clippy::cast_possible_truncation)] use crate::util::{self, consts::game::WINDOW_TITLE};
use crate::util::{self, consts::GTA_WINDOW_TITLE};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use windows::Win32::UI::Input::KeyboardAndMouse::{ 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 INTERVAL: Duration = Duration::from_secs(60);
const PRESS_KEYS: [u8; 2] = [VK_NUMPAD4.0 as u8, VK_NUMPAD6.0 as u8]; const PRESS_KEYS: [VIRTUAL_KEY; 2] = [VK_NUMPAD4, VK_NUMPAD6];
#[derive(Debug)] #[derive(Debug)]
pub struct AntiAfk { pub struct AntiAfk {
@@ -25,32 +24,39 @@ impl Default for AntiAfk {
} }
impl AntiAfk { impl AntiAfk {
pub fn activate(&mut self) { pub fn can_activate(&self) -> bool {
if can_activate() { use util::win::{is_any_key_pressed, is_cursor_visible, is_window_focused};
send(&PRESS_KEYS); 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) {
send(&PRESS_KEYS);
self.interval = Instant::now(); self.interval = Instant::now();
} }
} }
pub fn can_activate() -> bool { fn send(vk_codes: &[VIRTUAL_KEY]) {
use util::win::{is_any_key_pressed, is_cursor_visible, is_window_focused}; let mut inputs = Vec::new();
is_window_focused(GTA_WINDOW_TITLE) && !is_any_key_pressed(&PRESS_KEYS) && !is_cursor_visible() 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);
} }
}
pub fn send(vk_codes: &[u8]) { unsafe { SendInput(&inputs, size_of::<INPUT>() as i32) };
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,
);
});
} }
+26 -12
View File
@@ -1,5 +1,5 @@
use crate::util::{ use crate::util::{
consts::{ENHANCED, LEGACY}, consts::game::{EXE_ENHANCED, EXE_LEGACY},
countdown::Countdown, countdown::Countdown,
}; };
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
@@ -9,7 +9,7 @@ use windows::Win32::{
System::Threading::{OpenProcess, PROCESS_SUSPEND_RESUME}, System::Threading::{OpenProcess, PROCESS_SUSPEND_RESUME},
}; };
pub const INTERVAL: Duration = Duration::from_secs(10); const INTERVAL: Duration = Duration::from_secs(10);
#[derive(Debug)] #[derive(Debug)]
pub struct EmptySession { pub struct EmptySession {
@@ -28,10 +28,24 @@ impl Default for EmptySession {
} }
} }
impl EmptySession {
pub fn run_timers(&mut self, game_handle: &mut HANDLE) {
if self.disabled {
self.countdown.count();
} else {
self.countdown.reset();
}
if self.interval.elapsed() >= INTERVAL {
deactivate(game_handle);
self.disabled = false;
}
}
}
#[link(name = "ntdll")] #[link(name = "ntdll")]
unsafe extern "system" { unsafe extern "system" {
pub unsafe fn NtSuspendProcess(ProcessHandle: HANDLE) -> NTSTATUS; unsafe fn NtSuspendProcess(ProcessHandle: HANDLE) -> NTSTATUS;
pub unsafe fn NtResumeProcess(ProcessHandle: HANDLE) -> NTSTATUS; unsafe fn NtResumeProcess(ProcessHandle: HANDLE) -> NTSTATUS;
} }
fn get_gta_pid(sysinfo: &mut System) -> u32 { fn get_gta_pid(sysinfo: &mut System) -> u32 {
@@ -39,29 +53,29 @@ fn get_gta_pid(sysinfo: &mut System) -> u32 {
if let Some((pid, _)) = sysinfo if let Some((pid, _)) = sysinfo
.processes() .processes()
.iter() .iter()
.find(|(_, p)| p.name() == ENHANCED || p.name() == LEGACY) .find(|(_, p)| p.name() == EXE_ENHANCED || p.name() == EXE_LEGACY)
{ {
return pid.as_u32(); return pid.as_u32();
} }
u32::MAX u32::MAX
} }
pub fn activate(handle: &mut HANDLE, sysinfo: &mut System) { pub fn activate(game_handle: &mut HANDLE, sysinfo: &mut System) {
let pid = get_gta_pid(sysinfo); let pid = get_gta_pid(sysinfo);
if pid == u32::MAX { if pid == u32::MAX {
return; return;
} }
unsafe { unsafe {
*handle = OpenProcess(PROCESS_SUSPEND_RESUME, false, pid).unwrap(); *game_handle = OpenProcess(PROCESS_SUSPEND_RESUME, false, pid).unwrap();
let _ = NtSuspendProcess(*handle); let _ = NtSuspendProcess(*game_handle);
} }
} }
pub fn deactivate(handle: &mut HANDLE) { pub fn deactivate(game_handle: &mut HANDLE) {
unsafe { unsafe {
if !handle.is_invalid() { if !game_handle.is_invalid() {
let _ = NtResumeProcess(*handle); let _ = NtResumeProcess(*game_handle);
let _ = CloseHandle(*handle); let _ = CloseHandle(*game_handle);
} }
} }
} }
+35 -11
View File
@@ -1,38 +1,62 @@
use crate::util::consts::{ENHANCED, LEGACY}; use crate::util::consts::game::{EXE_ENHANCED, EXE_LEGACY};
use std::time::Instant; use std::time::{Duration, Instant};
use sysinfo::System; use sysinfo::System;
const INTERVAL: Duration = Duration::from_secs(3);
#[derive(Debug)] #[derive(Debug)]
pub struct ForceClose { pub struct ForceClose {
pub button_text: String, pub button_text: String,
pub prompting: bool, timer: Instant,
pub interval: Instant, counting: bool,
current_frame: bool,
} }
impl Default for ForceClose { impl Default for ForceClose {
fn default() -> Self { fn default() -> Self {
Self { Self {
button_text: "Force close game".to_owned(), button_text: "Force close game".to_owned(),
prompting: false, timer: Instant::now(),
interval: Instant::now(), counting: false,
current_frame: false,
} }
} }
} }
impl ForceClose { impl ForceClose {
pub fn prompting(&mut self) { pub fn prompt(&mut self, force_close_button_clicked: bool, sysinfo: &mut System) {
if force_close_button_clicked && !self.counting {
self.button_text = "Are you sure?".to_owned(); self.button_text = "Are you sure?".to_owned();
self.prompting = true; self.timer = Instant::now();
self.interval = 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(sysinfo);
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) { fn activate(sysinfo: &mut System) {
sysinfo.refresh_all(); sysinfo.refresh_all();
sysinfo sysinfo
.processes() .processes()
.iter() .iter()
.filter(|(_, p)| p.name() == ENHANCED || p.name() == LEGACY) .filter(|(_, p)| p.name() == EXE_ENHANCED || p.name() == EXE_LEGACY)
.for_each(|(_, p)| { .for_each(|(_, p)| {
p.kill(); p.kill();
}); });
+133 -58
View File
@@ -1,72 +1,147 @@
#![allow(clippy::zombie_processes)] use crate::util::consts::game::{EXE_ENHANCED, EXE_LEGACY};
use std::{
use crate::util::consts::{ENHANCED, LEGACY}; path::Path,
use std::{os::windows::process::CommandExt, path::Path, process::Command}; time::{Duration, Instant},
};
use strum::{Display, EnumIter};
use sysinfo::System; use sysinfo::System;
use windows::Win32::System::Threading::CREATE_NO_WINDOW; use windows::{
Win32::{
NetworkManagement::WindowsFirewall::{
INetFwPolicy2, INetFwRule, NET_FW_ACTION_BLOCK, NET_FW_IP_PROTOCOL_ANY,
NET_FW_RULE_DIR_IN, NET_FW_RULE_DIR_OUT, NetFwPolicy2, NetFwRule,
},
System::Com::{
CLSCTX_INPROC_SERVER, COINIT_MULTITHREADED, CoCreateInstance, CoInitializeEx,
CoUninitialize,
},
},
core::BSTR,
};
const FILTER_NAME: &str = "[GTA Tools] Block all traffic for GTA V"; const FILTER_NAME_IN: &str = "[GTA Tools] Block all inbound traffic for GTA V";
const FILTER_NAME_OUT: &str = "[GTA Tools] Block all outbound traffic for GTA V";
const INTERVAL: Duration = Duration::from_secs(3);
#[derive(Clone, Copy, Debug, Display, PartialEq, Eq, EnumIter)]
pub enum BlockedStatus {
Blocked,
Failed,
Unblocked,
}
impl From<bool> for BlockedStatus {
fn from(value: bool) -> Self {
if value {
Self::Blocked
} else {
Self::Unblocked
}
}
}
#[derive(Debug)]
pub struct GameNetworking {
com_initialized: bool,
pub blocked_status: BlockedStatus,
timer: Instant,
counting: bool,
}
impl Default for GameNetworking {
fn default() -> Self {
Self {
blocked_status: Self::is_blocked().into(),
com_initialized: unsafe { CoInitializeEx(None, COINIT_MULTITHREADED) }.is_ok(),
timer: Instant::now(),
counting: false,
}
}
}
impl Drop for GameNetworking {
fn drop(&mut self) {
unsafe {
if self.com_initialized {
CoUninitialize();
}
}
}
}
impl GameNetworking {
pub fn block_all(&mut self, sysinfo: &mut System) {
let Some(exe_path) = get_game_exe_path(sysinfo) else {
self.blocked_status = BlockedStatus::Failed;
return;
};
let policy: INetFwPolicy2 =
unsafe { CoCreateInstance(&NetFwPolicy2, None, CLSCTX_INPROC_SERVER).unwrap() };
let rules = unsafe { policy.Rules().unwrap() };
let exe_path = BSTR::from(exe_path.to_string_lossy().to_string());
for filter in [
(FILTER_NAME_IN, NET_FW_RULE_DIR_IN),
(FILTER_NAME_OUT, NET_FW_RULE_DIR_OUT),
] {
let _ = unsafe { rules.Remove(&BSTR::from(filter.0)) };
unsafe {
let rule: INetFwRule =
CoCreateInstance(&NetFwRule, None, CLSCTX_INPROC_SERVER).unwrap();
rule.SetName(&BSTR::from(filter.0)).unwrap();
rule.SetApplicationName(&exe_path).unwrap();
rule.SetDirection(filter.1).unwrap();
rule.SetEnabled(true.into()).unwrap();
rule.SetAction(NET_FW_ACTION_BLOCK).unwrap();
rule.SetProtocol(NET_FW_IP_PROTOCOL_ANY.0).unwrap();
rules.Add(&rule).unwrap();
}
}
self.blocked_status = Self::is_blocked().into();
}
pub fn unblock_all(&mut self) {
let policy: INetFwPolicy2 =
unsafe { CoCreateInstance(&NetFwPolicy2, None, CLSCTX_INPROC_SERVER).unwrap() };
let rules = unsafe { policy.Rules().unwrap() };
unsafe { rules.Remove(&BSTR::from(FILTER_NAME_IN)).unwrap() };
unsafe { rules.Remove(&BSTR::from(FILTER_NAME_OUT)).unwrap() };
self.blocked_status = Self::is_blocked().into();
}
fn is_blocked() -> bool {
let policy: INetFwPolicy2 =
unsafe { CoCreateInstance(&NetFwPolicy2, None, CLSCTX_INPROC_SERVER).unwrap() };
let rules = unsafe { policy.Rules().unwrap() };
let in_rule_exists = unsafe { rules.Item(&BSTR::from(FILTER_NAME_IN)).is_ok() };
let out_rule_exists = unsafe { rules.Item(&BSTR::from(FILTER_NAME_OUT)).is_ok() };
in_rule_exists || out_rule_exists
}
pub fn if_failed_return_to_boolean(&mut self) {
if self.blocked_status == BlockedStatus::Failed && !self.counting {
self.counting = true;
self.timer = Instant::now();
}
if self.blocked_status == BlockedStatus::Failed
&& self.counting
&& self.timer.elapsed() >= INTERVAL
{
self.counting = false;
self.blocked_status = Self::is_blocked().into();
}
}
}
fn get_game_exe_path(sysinfo: &mut System) -> Option<&Path> { fn get_game_exe_path(sysinfo: &mut System) -> Option<&Path> {
sysinfo.refresh_all(); sysinfo.refresh_all();
if let Some((_, process)) = sysinfo if let Some((_, process)) = sysinfo
.processes() .processes()
.iter() .iter()
.find(|(_, p)| p.name() == ENHANCED || p.name() == LEGACY) .find(|(_, p)| p.name() == EXE_ENHANCED || p.name() == EXE_LEGACY)
{ {
process.exe() process.exe()
} else { } else {
None None
} }
} }
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();
}
pub fn unblock_all() {
Command::new("netsh")
.args([
"advfirewall",
"firewall",
"delete",
"rule",
&format!("name={FILTER_NAME}"),
])
.creation_flags(CREATE_NO_WINDOW.0)
.spawn()
.unwrap();
}
+5 -14
View File
@@ -1,27 +1,18 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{fmt::Display, path::PathBuf, process::Command}; use std::{path::PathBuf, process::Command};
use strum::EnumIter; use strum::{Display, EnumIter};
use winreg::{RegKey, enums::HKEY_LOCAL_MACHINE}; 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 { pub enum Platform {
#[default] #[default]
Steam, Steam,
#[strum(to_string = "Rockstar Games")]
Rockstar, Rockstar,
#[strum(to_string = "Epic Games")]
Epic, 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)] #[derive(Debug, Default)]
pub struct Launch { pub struct Launch {
pub selected: Platform, pub selected: Platform,
+3
View File
@@ -1,4 +1,7 @@
mod app; mod app;
mod colours;
mod debug;
pub mod run; pub mod run;
pub mod settings; pub mod settings;
mod tools; mod tools;
mod ui_ext;
+97 -180
View File
@@ -1,92 +1,74 @@
use crate::{ use crate::{
features::{ features,
self, anti_afk::AntiAfk, empty_session::EmptySession, force_close::ForceClose,
launch::Launch,
},
gui::{ gui::{
settings::{self, Settings}, settings::{Settings, Theme},
tools, tools,
ui_ext::UiExt,
}, },
util::{ util::{consts::game::WINDOW_TITLE, meta::Meta, persistent_state::PersistentState, win},
self,
consts::{APP_STORAGE_PATH, ENHANCED, GTA_WINDOW_TITLE, LEGACY},
meta::Meta,
persistent_state::PersistentState,
},
}; };
use eframe::egui; use eframe::egui;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use strum::{Display, EnumIter};
pub const WINDOW_SIZE: [f32; 2] = [240.0, 240.0]; pub const WINDOW_SIZE: [f32; 2] = [240.0, 240.0];
#[derive(Debug, Default, PartialEq, Eq)] #[derive(Clone, Copy, Debug, Default, Display, PartialEq, Eq, EnumIter)]
pub enum Stage { enum Stage {
#[default] #[default]
Main, Main,
Settings, Settings,
About, About,
} }
#[allow(clippy::struct_excessive_bools)] #[derive(Debug)]
#[derive(Debug, Default)]
pub struct Flags { pub struct Flags {
pub elevated: bool, pub elevated: bool,
debug: bool, pub debug: bool,
closing: bool, closing: bool,
current_frame: bool,
} }
#[derive(Debug)] impl Default for Flags {
fn default() -> Self {
Self {
elevated: win::is_elevated(),
debug: false,
closing: false,
}
}
}
#[derive(Debug, Default)]
pub struct App { pub struct App {
meta: Meta, pub meta: Meta,
pub settings: Settings, pub settings: Settings,
stage: Stage, stage: Stage,
pub flags: Flags, pub flags: Flags,
pub sysinfo: sysinfo::System, pub sysinfo: sysinfo::System,
pub game_handle: windows::Win32::Foundation::HANDLE, game_handle: windows::Win32::Foundation::HANDLE,
pub launch: Launch, pub anti_afk: features::anti_afk::AntiAfk,
force_close: ForceClose, empty_session: features::empty_session::EmptySession,
empty_session: EmptySession, force_close: features::force_close::ForceClose,
anti_afk: AntiAfk, pub game_networking: features::game_networking::GameNetworking,
} pub launch: features::launch::Launch,
impl Default for App {
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(),
}
}
} }
impl eframe::App for App { impl eframe::App for App {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
catppuccin_egui::set_theme(ctx, self.settings.theme.into()); ctx.request_repaint_after(Duration::from_millis(100));
self.run_timers(); self.empty_session.run_timers(&mut self.game_handle);
egui::TopBottomPanel::bottom("bottom_panel") egui::TopBottomPanel::bottom("bottom_panel")
.exact_height(25.0) .exact_height(25.0)
.show(ctx, |ui| { .show(ctx, |ui| {
ui.with_layout(egui::Layout::left_to_right(egui::Align::Center), |ui| { ui.with_layout(egui::Layout::left_to_right(egui::Align::Center), |ui| {
ui.selectable_value(&mut self.stage, Stage::Main, "Main"); ui.build_menu(&mut self.stage);
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| { ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let button = ui let button = ui
.add_enabled(!self.flags.elevated, egui::Button::new("Elevate")) .add_enabled(!self.flags.elevated, egui::Button::new("Elevate"))
.on_hover_text("Relaunch ourselves as administrator.") .on_hover_text("Relaunch ourselves as administrator.")
.on_disabled_hover_text("We are already running elevated."); .on_disabled_hover_text("We are already running elevated.");
if button.clicked() { if button.clicked() {
util::win::elevate(util::win::ElevationExitMethod::Gentle( win::elevate(win::ElevationExitMethod::Gentle(&mut self.flags.closing));
&mut self.flags.closing,
));
} }
}); });
}); });
@@ -95,53 +77,17 @@ impl eframe::App for App {
egui::ScrollArea::vertical() egui::ScrollArea::vertical()
.auto_shrink([false, true]) .auto_shrink([false, true])
.show(ui, |ui| match self.stage { .show(ui, |ui| match self.stage {
Stage::Main => { Stage::Main => self.show_main_stage(ctx, ui),
self.show_game(ctx, ui); Stage::Settings => self.show_settings_stage(ctx, ui),
self.show_session(ctx, ui); Stage::About => self.show_about_stage(ctx, ui),
self.show_network(ctx, ui);
}
Stage::Settings => self.show_settings(ctx, ui),
Stage::About => self.show_about(ctx, ui),
}); });
}); });
if tools::check_debug_keycombo_pressed(ctx) { if tools::debug_keycombo_pressed(ctx) || tools::debug_viewport_close_pressed(ctx) {
self.flags.debug = !self.flags.debug; self.flags.debug = !self.flags.debug;
} }
if tools::check_debug_viewport_close_button_pressed(ctx) {
self.flags.debug = false;
}
if self.flags.debug { if self.flags.debug {
let main_rect = ctx.input(|i| { self.show_debug_viewport(ctx);
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);
});
});
},
);
}
ctx.request_repaint_after(Duration::from_millis(100));
if self.flags.closing { if self.flags.closing {
ctx.send_viewport_cmd(egui::ViewportCommand::Close); ctx.send_viewport_cmd(egui::ViewportCommand::Close);
} }
@@ -149,43 +95,29 @@ impl eframe::App for App {
} }
impl App { impl App {
fn show_game(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) { fn show_game_section(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) {
tools::header(ui, "Game"); ui.header("Game");
ui.horizontal(|ui| { ui.horizontal(|ui| {
if ui.button("Launch").clicked() { if ui.button("Launch").clicked() {
features::launch::launch(&self.launch.selected); features::launch::launch(&self.launch.selected);
} }
tools::build_combo_box::<features::launch::Platform>( egui::ComboBox::from_id_salt("Launch")
ui, .selected_text(self.launch.selected.to_string())
&mut self.launch.selected, .width(120.0)
"Launch", .show_ui(ui, |ui| {
); ui.build_menu(&mut self.launch.selected);
});
}); });
let force_close_button = ui.add_sized( let force_close_button = ui.add_sized(
[104.0, 0.0], [104.0, 0.0],
egui::Button::new(&self.force_close.button_text), egui::Button::new(&self.force_close.button_text),
); );
if force_close_button.clicked() && !self.force_close.prompting { self.force_close
self.force_close.prompting(); .prompt(force_close_button.clicked(), &mut self.sysinfo);
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;
}
} }
fn show_session(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) { fn show_session_section(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) {
tools::header(ui, "Session"); ui.header("Session");
ui.add_enabled_ui(!self.empty_session.disabled, |ui| { ui.add_enabled_ui(!self.empty_session.disabled, |ui| {
ui.horizontal(|ui| { ui.horizontal(|ui| {
if ui.button("Empty current session").clicked() { if ui.button("Empty current session").clicked() {
@@ -202,7 +134,7 @@ impl App {
if self.anti_afk.enabled { if self.anti_afk.enabled {
ui.add_space(8.0); ui.add_space(8.0);
ui.add_enabled_ui(false, |ui| { ui.add_enabled_ui(false, |ui| {
ui.label(if util::win::is_window_focused(GTA_WINDOW_TITLE) { ui.label(if win::is_window_focused(WINDOW_TITLE) {
"GTA is focused." "GTA is focused."
} else { } else {
"GTA is not focused!" "GTA is not focused!"
@@ -210,34 +142,42 @@ impl App {
}); });
} }
}); });
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(); self.anti_afk.activate();
} }
} }
fn show_network(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) { fn show_network_section(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) {
tools::header(ui, "Network"); ui.header("Network");
egui::Frame::new() egui::Frame::new()
.outer_margin(egui::vec2(0.0, -2.0)) .outer_margin(egui::vec2(0.0, -2.0))
.show(ui, |ui| { .show(ui, |ui| {
let response = ui.add_enabled_ui(self.flags.elevated, |ui| { let response = ui.add_enabled_ui(self.flags.elevated, |ui| {
let label = ui.horizontal(|ui| {
let label = ui.label("Game's network access"); let label = ui.label("Game's network access");
ui.add_space(1.0);
ui.create_indicator_dot(self.game_networking.blocked_status)
.on_hover_text(
"This turns yellow if GTA Tools\ncannot find your game.",
);
self.game_networking.if_failed_return_to_boolean();
label
});
ui.horizontal(|ui| { ui.horizontal(|ui| {
let available_width = label.rect.width(); let available_width = label.inner.rect.width();
let spacing = ui.spacing().item_spacing.x; let spacing = ui.spacing().item_spacing.x;
let button_width = (available_width - spacing) / 2.0; let button_width = (available_width - spacing) / 2.0;
if ui if ui
.add_sized([button_width, 18.0], egui::Button::new("Block")) .add_sized([button_width, 18.0], egui::Button::new("Block"))
.clicked() .clicked()
{ {
features::game_networking::block_all(&mut self.sysinfo); self.game_networking.block_all(&mut self.sysinfo);
} }
if ui if ui
.add_sized([button_width, 18.0], egui::Button::new("Unblock")) .add_sized([button_width, 18.0], egui::Button::new("Unblock"))
.clicked() .clicked()
{ {
features::game_networking::unblock_all(); self.game_networking.unblock_all();
} }
}); });
}); });
@@ -247,7 +187,36 @@ impl App {
}); });
} }
fn show_about(&self, _ctx: &egui::Context, ui: &mut egui::Ui) { fn show_main_stage(&mut self, ctx: &egui::Context, ui: &mut egui::Ui) {
self.show_game_section(ctx, ui);
self.show_session_section(ctx, ui);
self.show_network_section(ctx, ui);
}
fn show_settings_stage(&mut self, ctx: &egui::Context, ui: &mut egui::Ui) {
ui.horizontal(|ui| {
let selection = self.settings.theme;
ui.label("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 {
catppuccin_egui::set_theme(ctx, self.settings.theme.into());
}
if ui
.add_visible(self.settings.theme == Theme::Auto, egui::Button::new(""))
.on_hover_text("Refresh theme")
.clicked()
{
catppuccin_egui::set_theme(ctx, self.settings.theme.into());
}
});
ui.checkbox(&mut self.settings.start_elevated, "Always start elevated");
}
fn show_about_stage(&self, _ctx: &egui::Context, ui: &mut egui::Ui) {
ui.with_layout(egui::Layout::bottom_up(egui::Align::Center), |ui| { ui.with_layout(egui::Layout::bottom_up(egui::Align::Center), |ui| {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.horizontal(|ui| { ui.horizontal(|ui| {
@@ -282,68 +251,16 @@ 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 { impl Drop for App {
fn drop(&mut self) { fn drop(&mut self) {
// save any persistent state to config file // save any persistent state to config file
let persistent_state = PersistentState { PersistentState {
launcher: self.launch.selected, launcher: self.launch.selected,
settings: self.settings.clone(), settings: self.settings.clone(),
}; }
persistent_state.set(); .set();
// make sure we are not suspending game // make sure we are not suspending game
features::empty_session::deactivate(&mut self.game_handle); features::empty_session::deactivate(&mut self.game_handle);
} }
+16
View File
@@ -0,0 +1,16 @@
use crate::features::game_networking::BlockedStatus;
use eframe::egui;
pub const RED: egui::Color32 = egui::Color32::from_rgb(255, 96, 96);
pub const YELLOW: egui::Color32 = egui::Color32::from_rgb(255, 255, 96);
pub const GREEN: egui::Color32 = egui::Color32::from_rgb(96, 255, 96);
impl From<BlockedStatus> for egui::Color32 {
fn from(value: BlockedStatus) -> Self {
match value {
BlockedStatus::Blocked => RED,
BlockedStatus::Failed => YELLOW,
BlockedStatus::Unblocked => GREEN,
}
}
}
+95
View File
@@ -0,0 +1,95 @@
use crate::{
gui::{
app::{App, WINDOW_SIZE},
tools,
ui_ext::UiExt,
},
util::consts::{
APP_STORAGE_PATH,
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| {
if ui.button("open storage path").clicked() {
open::that_detached(APP_STORAGE_PATH.as_path()).unwrap();
}
ui.checkbox(
&mut self.meta.newer_version_available,
"spoof new version available",
)
.on_hover_text("(this could already be checked if\nthere actually IS a new version)");
ui.scope(|ui| {
use windows::Win32::UI::WindowsAndMessaging::{
GetForegroundWindow, GetWindowTextW,
};
let mut buffer = [0; 512];
let current_title = unsafe {
let hwnd = GetForegroundWindow();
let length = GetWindowTextW(hwnd, &mut buffer);
String::from_utf16_lossy(&buffer[..length as usize])
};
ui.label(format!("focused: \"{current_title}\""));
});
ui.horizontal(|ui| {
ui.label("blocked_status");
egui::ComboBox::from_id_salt("blocked_status")
.selected_text(self.game_networking.blocked_status.to_string())
.show_ui(ui, |ui| {
ui.build_menu(&mut self.game_networking.blocked_status);
});
});
});
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("sysinfo", |ui| {
if ui.button("refresh all").clicked() {
self.sysinfo.refresh_all();
}
let pid = self
.sysinfo
.processes()
.iter()
.find(|(_, p)| p.name() == EXE_ENHANCED || p.name() == EXE_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:#?}")));
}
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));
});
},
);
}
}
+31 -11
View File
@@ -3,10 +3,14 @@ use crate::{
app::{App, WINDOW_SIZE}, app::{App, WINDOW_SIZE},
tools, tools,
}, },
util::{self, consts::APP_STORAGE_PATH, persistent_state::PersistentState}, util::{consts::APP_STORAGE_PATH, persistent_state::PersistentState, win},
}; };
use eframe::egui; use eframe::egui;
use std::{fs::File, io::Write}; use std::{
fs::File,
io::Write,
time::{SystemTime, UNIX_EPOCH},
};
fn panic_hook(panic_info: &std::panic::PanicHookInfo<'_>) { fn panic_hook(panic_info: &std::panic::PanicHookInfo<'_>) {
let log_path = APP_STORAGE_PATH.join("panic.log"); let log_path = APP_STORAGE_PATH.join("panic.log");
@@ -15,32 +19,47 @@ fn panic_hook(panic_info: &std::panic::PanicHookInfo<'_>) {
.append(true) .append(true)
.open(log_path) .open(log_path)
.unwrap(); .unwrap();
let timestamp = chrono::Local::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, false); let timestamp = SystemTime::now()
let backtrace = std::backtrace::Backtrace::force_capture(); .duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let backtrace = std::backtrace::Backtrace::capture();
let message = format!("[{timestamp}]\n{panic_info}\nstack backtrace:\n{backtrace}\n"); let message = format!("[{timestamp}]\n{panic_info}\nstack backtrace:\n{backtrace}\n");
file.write_all(message.as_bytes()).unwrap(); file.write_all(message.as_bytes()).unwrap();
} }
#[allow(clippy::unnecessary_wraps)]
fn app_creator( fn app_creator(
cc: &eframe::CreationContext<'_>, cc: &eframe::CreationContext<'_>,
) -> Result<Box<dyn eframe::App>, Box<dyn std::error::Error + Send + Sync>> { ) -> Result<Box<dyn eframe::App>, Box<dyn std::error::Error + Send + Sync>> {
// use our own panic hook which logs all panics to a file
std::panic::set_hook(Box::new(panic_hook)); std::panic::set_hook(Box::new(panic_hook));
let mut app = Box::<App>::default(); // initialize http client (nyquest) for windows
nyquest_backend_winrt::register();
// 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() { if let Some(persistent_state) = PersistentState::get() {
app.launch.selected = persistent_state.launcher; app.launch.selected = persistent_state.launcher;
app.settings = persistent_state.settings; app.settings = persistent_state.settings;
} }
let elevated = util::win::is_elevated(); // check if we're elevated. if not, and the user wants an elevated launch - relaunch elevated
if app.settings.start_elevated && !elevated { if !app.flags.elevated && app.settings.start_elevated {
util::win::elevate(util::win::ElevationExitMethod::Forced); win::elevate(win::ElevationExitMethod::Forced);
} }
app.flags.elevated = elevated; // refresh sysinfo because it initializes with nothing
app.sysinfo.refresh_all();
// enable image loading support in egui
egui_extras::install_image_loaders(&cc.egui_ctx); 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
// to dark here to work around system theme based switching of the egui style
cc.egui_ctx.set_theme(egui::Theme::Dark);
catppuccin_egui::set_theme(&cc.egui_ctx, app.settings.theme.into());
// 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.spacing.item_spacing = egui::vec2(4.0, 4.0);
style.interaction.selectable_labels = false; style.interaction.selectable_labels = false;
}); });
// load any extra fonts that we need
let mut fonts = egui::FontDefinitions::default(); let mut fonts = egui::FontDefinitions::default();
fonts.font_data.insert( fonts.font_data.insert(
"Ubuntu-Regular".to_owned(), "Ubuntu-Regular".to_owned(),
@@ -51,6 +70,7 @@ fn app_creator(
vec!["Ubuntu-Regular".to_owned()], vec!["Ubuntu-Regular".to_owned()],
); );
cc.egui_ctx.set_fonts(fonts); cc.egui_ctx.set_fonts(fonts);
// finally return the App
Ok(app) Ok(app)
} }
+18 -32
View File
@@ -1,19 +1,32 @@
use crate::util::win;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt::Display; use strum::{Display, EnumIter};
use strum::EnumIter;
#[allow(clippy::enum_variant_names)] #[derive(Clone, Copy, Debug, Default, Display, PartialEq, Eq, Serialize, Deserialize, EnumIter)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, EnumIter)]
pub enum Theme { pub enum Theme {
#[default]
#[strum(to_string = "Auto")]
Auto,
#[strum(to_string = "Catppuccin Latte")]
CatppuccinLatte, CatppuccinLatte,
#[strum(to_string = "Catppuccin Frappe")]
CatppuccinFrappe, CatppuccinFrappe,
#[strum(to_string = "Catppuccin Macchiato")]
CatppuccinMacchiato, CatppuccinMacchiato,
#[strum(to_string = "Catppuccin Mocha")]
CatppuccinMocha, CatppuccinMocha,
} }
impl From<Theme> for catppuccin_egui::Theme { impl From<Theme> for catppuccin_egui::Theme {
fn from(val: Theme) -> Self { fn from(val: Theme) -> Self {
match val { match val {
Theme::Auto => {
if win::is_system_theme_dark() {
catppuccin_egui::MOCHA
} else {
catppuccin_egui::LATTE
}
}
Theme::CatppuccinLatte => catppuccin_egui::LATTE, Theme::CatppuccinLatte => catppuccin_egui::LATTE,
Theme::CatppuccinFrappe => catppuccin_egui::FRAPPE, Theme::CatppuccinFrappe => catppuccin_egui::FRAPPE,
Theme::CatppuccinMacchiato => catppuccin_egui::MACCHIATO, Theme::CatppuccinMacchiato => catppuccin_egui::MACCHIATO,
@@ -22,35 +35,8 @@ impl From<Theme> for catppuccin_egui::Theme {
} }
} }
impl Display for Theme { #[derive(Clone, Debug, Default, Serialize, Deserialize)]
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}")
}
}
impl Theme {
pub fn to_catppuccin(self) -> catppuccin_egui::Theme {
self.into()
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Settings { pub struct Settings {
pub theme: Theme, pub theme: Theme,
pub start_elevated: bool, pub start_elevated: bool,
} }
impl Default for Settings {
fn default() -> Self {
Self {
theme: Theme::CatppuccinMocha,
start_elevated: false,
}
}
}
+2 -25
View File
@@ -11,34 +11,11 @@ pub fn load_icon() -> egui::IconData {
} }
} }
pub fn header(ui: &mut egui::Ui, text: &str) { pub fn debug_keycombo_pressed(ctx: &egui::Context) -> bool {
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 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)) ctx.input(|i| i.modifiers.all() && i.key_pressed(egui::Key::D))
} }
pub fn check_debug_viewport_close_button_pressed(ctx: &egui::Context) -> bool { pub fn debug_viewport_close_pressed(ctx: &egui::Context) -> bool {
ctx.input(|i| { ctx.input(|i| {
i.raw i.raw
.viewports .viewports
+38
View File
@@ -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("Ubuntu-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),
)
}
}
+3
View File
@@ -1,5 +1,8 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
#[cfg(not(target_os = "windows"))]
compile_error!("This application must be compiled on Windows.");
mod features; mod features;
mod gui; mod gui;
mod util; mod util;
+1 -1
View File
@@ -1,4 +1,4 @@
pub mod codeberg; mod codeberg;
pub mod consts; pub mod consts;
pub mod countdown; pub mod countdown;
pub mod meta; pub mod meta;
+23 -10
View File
@@ -1,4 +1,6 @@
use nyquest::{ClientBuilder, blocking::Request};
use semver::Version; use semver::Version;
use serde::Deserialize;
const APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_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"; const CODEBERG_ENDPOINT_ROOT: &str = "https://codeberg.org/api/v1";
@@ -12,24 +14,35 @@ pub struct Release {
impl Default for Release { impl Default for Release {
fn default() -> Self { fn default() -> Self {
Self { Self {
version: Version::parse(env!("CARGO_PKG_VERSION")).unwrap(), version: Version::new(0, 0, 0),
download_url: String::new(), download_url: String::new(),
} }
} }
} }
#[derive(Deserialize)]
struct LatestRelease {
tag_name: String,
assets: Vec<Asset>,
}
#[derive(Deserialize)]
struct Asset {
browser_download_url: String,
}
pub fn get_latest_release() -> Option<Release> { pub fn get_latest_release() -> Option<Release> {
let request_url = format!("{CODEBERG_ENDPOINT_ROOT}/repos/futile/gta-tools/releases/latest"); let request_url = format!("{CODEBERG_ENDPOINT_ROOT}/repos/futile/gta-tools/releases/latest");
let ureq: ureq::Agent = ureq::Agent::config_builder() let client = ClientBuilder::default()
.user_agent(APP_USER_AGENT) .user_agent(APP_USER_AGENT)
.build() .build_blocking()
.into(); .unwrap();
let mut response = ureq.get(request_url).call().ok()?; let response = client.request(Request::get(request_url)).unwrap();
let json = response.body_mut().read_json::<serde_json::Value>().ok()?; let json = response.json::<LatestRelease>().ok()?;
let tag_name = json["tag_name"].as_str()?; let tag_name = json.tag_name;
let browser_download_url = json["assets"][0]["browser_download_url"].as_str()?; let browser_download_url = &json.assets[0].browser_download_url;
Some(Release { Some(Release {
version: Version::parse(tag_name).expect("expected a valid semver pattern"), version: Version::parse(&tag_name).ok()?,
download_url: String::from(browser_download_url), download_url: browser_download_url.to_owned(),
}) })
} }
+9 -7
View File
@@ -1,9 +1,11 @@
use std::{path::PathBuf, sync::LazyLock}; use std::{env, path::PathBuf, sync::LazyLock};
pub const ENHANCED: &str = "GTA5_Enhanced.exe"; pub static APP_STORAGE_PATH: LazyLock<PathBuf> = LazyLock::new(|| {
pub const LEGACY: &str = "GTA5.exe"; PathBuf::from(env::var("LOCALAPPDATA").unwrap_or_else(|_| String::from("."))).join("GTA Tools")
});
pub const GTA_WINDOW_TITLE: &str = "Grand Theft Auto V"; pub mod game {
pub const EXE_ENHANCED: &str = "GTA5_Enhanced.exe";
pub static APP_STORAGE_PATH: LazyLock<PathBuf> = pub const EXE_LEGACY: &str = "GTA5.exe";
LazyLock::new(|| dirs::config_local_dir().unwrap().join("GTA Tools")); pub const WINDOW_TITLE: &str = "Grand Theft Auto V";
}
+4 -4
View File
@@ -2,11 +2,11 @@ use std::time::{Duration, Instant};
#[derive(Debug)] #[derive(Debug)]
pub struct Countdown { pub struct Countdown {
pub i: u64, i: u64,
pub i_original: u64, i_original: u64,
pub i_string: String, pub i_string: String,
pub interval: Instant, interval: Instant,
pub first_count: bool, first_count: bool,
} }
impl Countdown { impl Countdown {
+17 -4
View File
@@ -4,7 +4,7 @@ use windows::{
Security::{GetTokenInformation, TOKEN_ELEVATION, TOKEN_QUERY, TokenElevation}, Security::{GetTokenInformation, TOKEN_ELEVATION, TOKEN_QUERY, TokenElevation},
System::Threading::{GetCurrentProcess, OpenProcessToken}, System::Threading::{GetCurrentProcess, OpenProcessToken},
UI::{ UI::{
Input::KeyboardAndMouse::GetAsyncKeyState, Input::KeyboardAndMouse::{GetAsyncKeyState, VIRTUAL_KEY},
Shell::ShellExecuteW, Shell::ShellExecuteW,
WindowsAndMessaging::{ WindowsAndMessaging::{
CURSOR_SHOWING, CURSORINFO, GetCursorInfo, GetForegroundWindow, GetWindowTextW, CURSOR_SHOWING, CURSORINFO, GetCursorInfo, GetForegroundWindow, GetWindowTextW,
@@ -31,7 +31,6 @@ pub fn is_cursor_visible() -> bool {
ci.flags == CURSOR_SHOWING ci.flags == CURSOR_SHOWING
} }
#[allow(clippy::cast_sign_loss)]
pub fn is_window_focused(target_title: &str) -> bool { pub fn is_window_focused(target_title: &str) -> bool {
let mut buffer = [0; 512]; let mut buffer = [0; 512];
unsafe { unsafe {
@@ -42,9 +41,9 @@ pub fn is_window_focused(target_title: &str) -> bool {
} }
} }
pub fn is_any_key_pressed(keys: &[u8]) -> bool { pub fn is_any_key_pressed(keys: &[VIRTUAL_KEY]) -> bool {
keys.iter() 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) { pub fn elevate(closing: ElevationExitMethod) {
@@ -84,3 +83,17 @@ pub fn is_elevated() -> bool {
result.is_ok() && elevation.TokenIsElevated != 0 result.is_ok() && elevation.TokenIsElevated != 0
} }
} }
pub fn is_system_theme_dark() -> 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(dword): Result<u32, std::io::Error> = subkey.get_value("AppsUseLightTheme") else {
return true;
};
dword != 1
}