17 Commits
Author SHA1 Message Date
futile 62efe3f411 0.3.0 2025-04-09 16:39:25 +01:00
futile 4bb0f151a9 improve anti afk with added checks and different keys 2025-04-09 16:34:30 +01:00
futile 1aebc6b17c make empty session interval 10 seconds again 2025-04-08 17:27:28 +01:00
futile 861ebe97d6 build CONFIG_PATH more nicely and fix crash when minimizing with debug open 2025-04-08 17:27:03 +01:00
futile 53b20a437a some clippy fixes 2025-04-08 17:11:25 +01:00
futile 03ca861a09 change ui window size + ui spacing size & make an enum Copy 2025-04-08 15:45:18 +01:00
futile a4ebfc515c some more ui changes 2025-04-08 13:18:45 +01:00
futile 52ffb34d83 better comments in drop of App 2025-04-08 10:52:34 +01:00
futile 6248e8379f change ui stuff 2025-04-08 10:50:34 +01:00
futile 99bc15b5ed ensure game isn't suspended when we close 2025-04-08 09:24:01 +01:00
futile e84d1347fd fix stupid justfile 2025-04-08 09:23:29 +01:00
futile f49bb56398 0.2.1 2025-04-08 09:05:29 +01:00
futile 99b6519ffb don't std::process::exit 2025-04-08 09:02:16 +01:00
futile 807cb5242f implement some persistent state (namely selected game launcher) 2025-04-08 08:35:29 +01:00
futile c1c1547e4d improve debug experience 2025-04-08 07:52:55 +01:00
futile 6e3aa70398 simplify load_icon 2025-04-08 06:35:15 +01:00
futile 9331f79038 edit justfile 2025-04-08 03:25:33 +01:00
12 changed files with 320 additions and 99 deletions
Generated
+66 -1
View File
@@ -734,6 +734,27 @@ dependencies = [
"crypto-common",
]
[[package]]
name = "dirs"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
dependencies = [
"dirs-sys",
]
[[package]]
name = "dirs-sys"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.59.0",
]
[[package]]
name = "dispatch"
version = "0.2.0"
@@ -1345,13 +1366,16 @@ dependencies = [
[[package]]
name = "gta-tools"
version = "0.2.0"
version = "0.3.0"
dependencies = [
"catppuccin-egui",
"dirs",
"eframe",
"egui_extras",
"image",
"open",
"serde",
"serde_json",
"static_vcruntime",
"sysinfo",
"windows 0.61.1",
@@ -1591,6 +1615,12 @@ dependencies = [
"once_cell",
]
[[package]]
name = "itoa"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "jni"
version = "0.21.1"
@@ -2212,6 +2242,12 @@ dependencies = [
"pathdiff",
]
[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "orbclient"
version = "0.3.48"
@@ -2538,6 +2574,17 @@ dependencies = [
"bitflags 2.9.0",
]
[[package]]
name = "redox_users"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
dependencies = [
"getrandom 0.2.15",
"libredox",
"thiserror 2.0.12",
]
[[package]]
name = "renderdoc-sys"
version = "1.1.0"
@@ -2582,6 +2629,12 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
[[package]]
name = "ryu"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "same-file"
version = "1.0.6"
@@ -2636,6 +2689,18 @@ dependencies = [
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]]
name = "serde_repr"
version = "0.1.20"
+4 -1
View File
@@ -1,16 +1,19 @@
[package]
name = "gta-tools"
version = "0.2.0"
version = "0.3.0"
edition = "2024"
[dependencies]
catppuccin-egui = { version = "5.5.0", default-features = false, features = [
"egui31",
] }
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"
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
sysinfo = "0.34.2"
windows = { version = "0.61.1", features = [
"Win32_UI_Input_KeyboardAndMouse",
+5 -2
View File
@@ -1,6 +1,9 @@
set windows-shell := ["pwsh", "-NoLogo", "-NoProfileLoadTime", "-Command"]
install:
build:
cargo build --release
upx .\target\release\gta-tools.exe --best
cp .\target\release\gta-tools.exe ~\.cargo\bin\
install: build
cp .\target\release\gta-tools.exe ~\.cargo\bin
cp .\target\release\gta-tools.exe ~\Documents
+17 -5
View File
@@ -1,11 +1,14 @@
use crate::util::{self, consts::GTA_WINDOW_TITLE};
use std::time::{Duration, Instant};
use windows::Win32::UI::Input::KeyboardAndMouse::{
KEYBD_EVENT_FLAGS, MAP_VIRTUAL_KEY_TYPE, MapVirtualKeyW, keybd_event,
};
pub const INTERVAL: Duration = Duration::from_secs(60);
const VK_SHIFT: u8 = 16;
const VK_NUMPAD4: u8 = 0x64;
const VK_NUMPAD6: u8 = 0x66;
#[derive(Debug)]
pub struct AntiAfk {
pub enabled: bool,
pub interval: Instant,
@@ -20,6 +23,19 @@ impl Default for AntiAfk {
}
}
impl AntiAfk {
pub fn activate(&mut self) {
if util::is_window_focused(GTA_WINDOW_TITLE)
&& !util::is_key_pressed(VK_NUMPAD4 as i32)
&& !util::is_key_pressed(VK_NUMPAD6 as i32)
{
send(VK_NUMPAD4);
send(VK_NUMPAD6);
}
self.interval = Instant::now();
}
}
pub fn send(vk_code: u8) {
unsafe {
keybd_event(
@@ -36,7 +52,3 @@ pub fn send(vk_code: u8) {
);
}
}
pub fn activate() {
send(VK_SHIFT);
}
+4 -1
View File
@@ -6,10 +6,11 @@ use windows::Win32::{
System::Threading::{OpenProcess, PROCESS_SUSPEND_RESUME},
};
pub const INTERVAL: Duration = Duration::from_secs(8);
pub const INTERVAL: Duration = Duration::from_secs(10);
const ENHANCED: &str = "GTA5_Enhanced.exe";
const LEGACY: &str = "GTA5.exe";
#[derive(Debug)]
pub struct EmptySession {
pub disabled: bool,
pub interval: Instant,
@@ -57,7 +58,9 @@ pub fn activate(app: &mut App) {
pub fn deactivate(app: &mut App) {
unsafe {
if !app.game_handle.is_invalid() {
let _ = NtResumeProcess(app.game_handle);
let _ = CloseHandle(app.game_handle);
}
}
}
+1
View File
@@ -2,6 +2,7 @@ use crate::util::consts::{ENHANCED, LEGACY};
use std::time::Instant;
use sysinfo::System;
#[derive(Debug)]
pub struct ForceClose {
pub button_text: String,
pub prompting: bool,
+3 -2
View File
@@ -1,7 +1,8 @@
use serde::{Deserialize, Serialize};
use std::{fmt::Display, path::PathBuf, process::Command};
use winreg::{RegKey, enums::HKEY_LOCAL_MACHINE};
#[derive(Default, Debug, PartialEq, Eq)]
#[derive(Clone, Copy, Default, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Platform {
#[default]
Steam,
@@ -20,7 +21,7 @@ impl Display for Platform {
}
}
#[derive(Default)]
#[derive(Debug, Default)]
pub struct Launch {
pub selected: Platform,
}
+161 -50
View File
@@ -7,28 +7,49 @@ use crate::{
launch::{Launch, Platform},
},
util::{
consts::{ENHANCED, LEGACY},
self,
consts::{ENHANCED, GTA_WINDOW_TITLE, LEGACY},
elevation,
},
};
use eframe::egui;
use std::time::{Duration, Instant};
use serde::{Deserialize, Serialize};
use std::{
fs::{self, File},
io::Write,
path::PathBuf,
sync::LazyLock,
time::{Duration, Instant},
};
use sysinfo::System;
use windows::Win32::Foundation::HANDLE;
const THEME: catppuccin_egui::Theme = catppuccin_egui::MOCHA;
const WINDOW_SIZE: [f32; 2] = [240.0, 240.0];
static CONFIG_PATH: LazyLock<PathBuf> = LazyLock::new(|| {
dirs::config_local_dir()
.unwrap()
.join("GTA Tools")
.join("config.json")
});
#[derive(Default, PartialEq, Eq)]
#[derive(Serialize, Deserialize)]
struct PersistentState {
launcher: Platform,
}
#[derive(Debug, Default, PartialEq, Eq)]
pub enum Stage {
#[default]
Main,
About,
Debug,
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug)]
pub struct App {
stage: Stage,
closing: bool,
debug: bool,
initialized: bool,
elevated: bool,
@@ -45,6 +66,7 @@ impl Default for App {
fn default() -> Self {
Self {
stage: Stage::default(),
closing: false,
initialized: false,
debug: false,
elevated: elevation::is_elevated(),
@@ -64,40 +86,33 @@ impl eframe::App for App {
if !self.initialized {
catppuccin_egui::set_theme(ctx, THEME);
egui_extras::install_image_loaders(ctx);
if let Ok(config) = fs::read_to_string(CONFIG_PATH.as_path()) {
if let Ok(persistent_state) = serde_json::from_str::<PersistentState>(&config) {
self.launch.selected = persistent_state.launcher;
}
}
ctx.style_mut(|style| style.spacing.item_spacing = egui::vec2(4.0, 4.0));
self.initialized = true;
}
ctx.request_repaint_after(Duration::from_millis(100));
ctx.input(|i| {
if i.modifiers.all() && i.key_pressed(egui::Key::D) {
self.debug = !self.debug;
if !self.debug && self.stage == Stage::Debug {
self.stage = Stage::Main;
}
}
});
self.run_timers();
egui::TopBottomPanel::bottom("bottom_panel").show(ctx, |ui| {
ui.horizontal(|ui| {
egui::TopBottomPanel::bottom("bottom_panel")
.exact_height(25.0)
.show(ctx, |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::About, "About");
if self.debug {
ui.selectable_value(&mut self.stage, Stage::Debug, "Debug");
}
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let button = ui
.add_enabled(!self.elevated, egui::Button::new("Elevate"))
.on_hover_text("Relaunch ourselves as administrator.")
.on_disabled_hover_text("We are already running elevated.");
if button.clicked() {
elevation::elevate();
elevation::elevate(&mut self.closing);
}
});
});
});
egui::CentralPanel::default().show(ctx, |ui| {
egui::ScrollArea::both()
.auto_shrink([false, false])
.show(ui, |ui| match self.stage {
egui::CentralPanel::default().show(ctx, |ui| match self.stage {
Stage::Main => {
self.header(ui, "Game");
self.show_game(ctx, ui);
@@ -106,14 +121,49 @@ impl eframe::App for App {
self.header(ui, "Network");
self.show_network(ctx, ui);
}
Stage::About => {
self.show_about(ctx, ui);
Stage::About => self.show_about(ctx, ui),
});
if check_debug_keycombo_pressed(ctx) {
self.debug = !self.debug;
}
Stage::Debug => {
if check_debug_viewport_close_button_pressed(ctx) {
self.debug = false;
}
if self.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(load_icon()),
|ctx, _class| {
if check_debug_keycombo_pressed(ctx) {
self.debug = !self.debug;
}
egui::CentralPanel::default().show(ctx, |ui| {
egui::ScrollArea::both()
.auto_shrink([false, false])
.show(ui, |ui| {
self.show_debug(ctx, ui);
});
});
},
);
}
ctx.request_repaint_after(Duration::from_millis(100));
if self.closing {
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
}
});
});
}
}
@@ -152,7 +202,10 @@ impl App {
);
});
});
let force_close_button = ui.button(&self.force_close.button_text);
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.current_frame = true;
@@ -183,12 +236,23 @@ impl App {
ui.label(&self.empty_session.countdown.i_string);
});
});
ui.horizontal(|ui| {
ui.checkbox(&mut self.anti_afk.enabled, "Anti AFK")
.on_hover_text("You should be tabbed in\nwhile this is enabled.");
.on_hover_text("You should be tabbed in\nfor this to work.");
if self.anti_afk.enabled {
ui.add_space(8.0);
ui.add_enabled_ui(false, |ui| {
ui.label(if util::is_window_focused(GTA_WINDOW_TITLE) {
"GTA is focused."
} else {
"GTA is not focused!"
})
});
}
});
if self.anti_afk.enabled && self.anti_afk.interval.elapsed() >= features::anti_afk::INTERVAL
{
features::anti_afk::activate();
self.anti_afk.interval = Instant::now();
self.anti_afk.activate();
}
}
@@ -198,19 +262,28 @@ impl App {
.stroke(egui::Stroke::new(1.0, THEME.overlay1))
.show(ui, |ui| {
let response = ui.add_enabled_ui(self.elevated, |ui| {
ui.label("Game's network access");
let label = ui.label("Game's network access");
ui.horizontal(|ui| {
if ui.button("Block").clicked() {
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()
{
features::game_networking::block_all(&mut self.sysinfo);
};
if ui.button("Unblock").clicked() {
if ui
.add_sized([button_width, 18.0], egui::Button::new("Unblock"))
.clicked()
{
features::game_networking::unblock_all();
};
});
});
response
.response
.on_disabled_hover_text("This requires administrator.\nUse the Elevate button.")
response.response.on_disabled_hover_text(
"This requires administrator.\nUse the Elevate button.",
);
});
}
@@ -231,11 +304,21 @@ impl App {
));
});
});
ui.add(egui::Image::new(egui::include_image!("../assets/icon.png")))
ui.add(egui::Image::new(egui::include_image!("../assets/icon.png")));
});
}
fn show_debug(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) {
ui.collapsing("times", |ui| {
ui.label(format!(
"anti afk timer: {}",
self.anti_afk.interval.elapsed().as_secs()
))
});
ui.collapsing("sysinfo", |ui| {
if ui.button("refresh all").clicked() {
self.sysinfo.refresh_all();
}
let pid = self
.sysinfo
.processes()
@@ -245,10 +328,9 @@ impl App {
|| "no pid found!".to_string(),
|(pid, _)| pid.as_u32().to_string(),
);
if ui.button("refresh sysinfo").clicked() {
self.sysinfo.refresh_all();
}
ui.label(format!("gta pid: {pid}"));
});
ui.collapsing("app state", |ui| ui.label(format!("{self:#?}")));
}
fn run_timers(&mut self) {
@@ -264,18 +346,47 @@ impl App {
}
}
impl Drop for App {
fn drop(&mut self) {
// save any persistent state to config file
let persistent_state = PersistentState {
launcher: self.launch.selected,
};
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 json = serde_json::to_string_pretty(&persistent_state).unwrap();
config_file.write_all(json.as_bytes()).unwrap();
// make sure we are not suspending game
features::empty_session::deactivate(self);
}
}
fn check_debug_keycombo_pressed(ctx: &egui::Context) -> bool {
ctx.input(|i| i.modifiers.all() && i.key_pressed(egui::Key::D))
}
fn check_debug_viewport_close_button_pressed(ctx: &egui::Context) -> bool {
ctx.input(|i| {
i.raw
.viewports
.get(&egui::ViewportId::from_hash_of("debug_viewport"))
.filter(|vp| vp.close_requested())
.is_some()
})
}
fn load_icon() -> eframe::egui::IconData {
let (icon_rgba, icon_width, icon_height) = {
let icon = include_bytes!("../assets/icon.png");
let image = image::load_from_memory(icon).unwrap().into_rgba8();
let (width, height) = image.dimensions();
let rgba = image.into_raw();
(rgba, width, height)
};
eframe::egui::IconData {
rgba: icon_rgba,
width: icon_width,
height: icon_height,
rgba: image.into_raw(),
width,
height,
}
}
@@ -284,7 +395,7 @@ pub fn run() {
viewport: egui::ViewportBuilder::default()
.with_resizable(false)
.with_maximize_button(false)
.with_inner_size([256.0, 232.0])
.with_inner_size(WINDOW_SIZE)
.with_icon(load_icon()),
centered: true,
..Default::default()
+2
View File
@@ -1,2 +1,4 @@
pub const ENHANCED: &str = "GTA5_Enhanced.exe";
pub const LEGACY: &str = "GTA5.exe";
pub const GTA_WINDOW_TITLE: &str = "Grand Theft Auto V";
+1
View File
@@ -1,5 +1,6 @@
use std::time::{Duration, Instant};
#[derive(Debug)]
pub struct Countdown {
pub i: u64,
pub i_original: u64,
+2 -2
View File
@@ -6,7 +6,7 @@ use windows::{
core::{HSTRING, PCWSTR},
};
pub fn elevate() {
pub fn elevate(closing: &mut bool) {
let exe = std::env::current_exe().unwrap();
unsafe {
ShellExecuteW(
@@ -18,7 +18,7 @@ pub fn elevate() {
SW_HIDE,
);
}
std::process::exit(0);
*closing = true;
}
pub fn is_elevated() -> bool {
+19
View File
@@ -1,3 +1,22 @@
pub mod consts;
pub mod countdown;
pub mod elevation;
use windows::Win32::UI::{
Input::KeyboardAndMouse::GetAsyncKeyState,
WindowsAndMessaging::{GetForegroundWindow, GetWindowTextW},
};
pub fn is_window_focused(target_title: &str) -> bool {
unsafe {
let hwnd = GetForegroundWindow();
let mut buffer: [u16; 512] = [0; 512];
let length = GetWindowTextW(hwnd, &mut buffer);
let current_title = String::from_utf16_lossy(&buffer[..length as usize]);
current_title == target_title
}
}
pub fn is_key_pressed(key: i32) -> bool {
unsafe { (GetAsyncKeyState(key) as i32 & 0x8000) != 0 }
}