19 Commits
Author SHA1 Message Date
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
12 changed files with 212 additions and 1634 deletions
Generated
+78 -1504
View File
File diff suppressed because it is too large Load Diff
+10 -5
View File
@@ -1,16 +1,22 @@
[package] [package]
name = "gta-tools" name = "gta-tools"
version = "0.5.1" version = "0.5.3"
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" chrono = { version = "0.4.40", default-features = false, features = ["clock"] }
dirs = "6.0.0" dirs = "6.0.0"
eframe = "0.31.1" eframe = { version = "0.31.1", default-features = false, features = [
egui_extras = { version = "0.31.1", features = ["image", "svg"] } "glow",
"default_fonts",
] }
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"] }
open = "5.3.2" open = "5.3.2"
semver = "1.0.26" semver = "1.0.26"
@@ -34,6 +40,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
+10 -1
View File
@@ -1,4 +1,13 @@
# GTA Tools # GTA Tools
A toolset of convenient things for GTA V Online. A toolset of convenient things for GTA V Online.
![](https://i.vgy.me/s6bf1g.png) ![](https://i.vgy.me/maLh8W.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**.
+21 -24
View File
@@ -1,8 +1,8 @@
use crate::util::{self, consts::game::WINDOW_TITLE}; use crate::util::{self, consts::game::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, VIRTUAL_KEY, VK_NUMPAD4, VK_NUMPAD6, INPUT, INPUT_KEYBOARD, KEYBD_EVENT_FLAGS, KEYBDINPUT, KEYEVENTF_KEYUP, MAPVK_VK_TO_VSC,
keybd_event, MapVirtualKeyW, SendInput, VIRTUAL_KEY, VK_NUMPAD4, VK_NUMPAD6,
}; };
const INTERVAL: Duration = Duration::from_secs(60); const INTERVAL: Duration = Duration::from_secs(60);
@@ -40,26 +40,23 @@ impl AntiAfk {
} }
fn send(vk_codes: &[VIRTUAL_KEY]) { fn send(vk_codes: &[VIRTUAL_KEY]) {
vk_codes.iter().for_each(|vk_code| unsafe { let mut inputs = Vec::new();
keybd_event( for &vk_code in vk_codes {
vk_code.0 as u8, let scan_code = unsafe { MapVirtualKeyW(u32::from(vk_code.0), MAPVK_VK_TO_VSC) as u16 };
u8::try_from(MapVirtualKeyW( for event in [KEYBD_EVENT_FLAGS(0), KEYEVENTF_KEYUP] {
u32::from(vk_code.0), let mut input = INPUT {
MAP_VIRTUAL_KEY_TYPE(0), r#type: INPUT_KEYBOARD,
)) ..Default::default()
.unwrap(), };
KEYBD_EVENT_FLAGS(0), input.Anonymous.ki = KEYBDINPUT {
0, wVk: vk_code,
); wScan: scan_code,
keybd_event( dwFlags: event,
vk_code.0 as u8, time: 0,
u8::try_from(MapVirtualKeyW( dwExtraInfo: 0,
u32::from(vk_code.0), };
MAP_VIRTUAL_KEY_TYPE(0), inputs.push(input);
)) }
.unwrap(), unsafe { SendInput(&inputs, size_of::<INPUT>() as i32) };
KEYBD_EVENT_FLAGS(2), }
0,
);
});
} }
+1 -3
View File
@@ -33,12 +33,10 @@ impl ForceClose {
} }
if self.counting && self.timer.elapsed() >= INTERVAL { if self.counting && self.timer.elapsed() >= INTERVAL {
self.reset(); self.reset();
} else { } else if force_close_button_clicked && !self.current_frame {
if force_close_button_clicked && !self.current_frame {
activate(sysinfo); activate(sysinfo);
self.reset(); self.reset();
} }
}
self.finish_current_frame(); self.finish_current_frame();
} }
+29 -72
View File
@@ -1,15 +1,12 @@
use crate::util::consts::{ use crate::util::consts::game::{EXE_ENHANCED, EXE_LEGACY};
colours,
game::{EXE_ENHANCED, EXE_LEGACY},
};
use std::{ use std::{
path::Path, path::Path,
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use strum::{Display, EnumIter};
use sysinfo::System; use sysinfo::System;
use windows::{ use windows::{
Win32::{ Win32::{
Foundation::E_INVALIDARG,
NetworkManagement::WindowsFirewall::{ NetworkManagement::WindowsFirewall::{
INetFwPolicy2, INetFwRule, NET_FW_ACTION_BLOCK, NET_FW_IP_PROTOCOL_ANY, INetFwPolicy2, INetFwRule, NET_FW_ACTION_BLOCK, NET_FW_IP_PROTOCOL_ANY,
NET_FW_RULE_DIR_IN, NET_FW_RULE_DIR_OUT, NetFwPolicy2, NetFwRule, NET_FW_RULE_DIR_IN, NET_FW_RULE_DIR_OUT, NetFwPolicy2, NetFwRule,
@@ -27,7 +24,7 @@ const FILTER_NAME_OUT: &str = "[GTA Tools] Block all outbound traffic for GTA V"
const INTERVAL: Duration = Duration::from_secs(3); const INTERVAL: Duration = Duration::from_secs(3);
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, Display, PartialEq, Eq, EnumIter)]
pub enum BlockedStatus { pub enum BlockedStatus {
Blocked, Blocked,
Failed, Failed,
@@ -36,29 +33,14 @@ pub enum BlockedStatus {
impl From<bool> for BlockedStatus { impl From<bool> for BlockedStatus {
fn from(value: bool) -> Self { fn from(value: bool) -> Self {
match value { if value {
true => Self::Blocked, Self::Blocked
false => Self::Unblocked, } else {
Self::Unblocked
} }
} }
} }
impl From<BlockedStatus> for eframe::egui::Color32 {
fn from(value: BlockedStatus) -> Self {
match value {
BlockedStatus::Blocked => colours::RED,
BlockedStatus::Failed => colours::YELLOW,
BlockedStatus::Unblocked => colours::GREEN,
}
}
}
impl BlockedStatus {
pub fn to_color32(&self) -> eframe::egui::Color32 {
(*self).into()
}
}
#[derive(Debug)] #[derive(Debug)]
pub struct GameNetworking { pub struct GameNetworking {
com_initialized: bool, com_initialized: bool,
@@ -69,14 +51,12 @@ pub struct GameNetworking {
impl Default for GameNetworking { impl Default for GameNetworking {
fn default() -> Self { fn default() -> Self {
let mut gn = Self { Self {
blocked_status: BlockedStatus::Unblocked, blocked_status: Self::is_blocked().into(),
com_initialized: unsafe { CoInitializeEx(None, COINIT_MULTITHREADED) }.is_ok(), com_initialized: unsafe { CoInitializeEx(None, COINIT_MULTITHREADED) }.is_ok(),
timer: Instant::now(), timer: Instant::now(),
counting: false, counting: false,
}; }
gn.blocked_status = gn.is_blocked().into();
gn
} }
} }
@@ -99,60 +79,37 @@ impl GameNetworking {
let policy: INetFwPolicy2 = let policy: INetFwPolicy2 =
unsafe { CoCreateInstance(&NetFwPolicy2, None, CLSCTX_INPROC_SERVER).unwrap() }; unsafe { CoCreateInstance(&NetFwPolicy2, None, CLSCTX_INPROC_SERVER).unwrap() };
let rules = unsafe { policy.Rules().unwrap() }; let rules = unsafe { policy.Rules().unwrap() };
let filter_name_in = BSTR::from(FILTER_NAME_IN);
let filter_name_out = BSTR::from(FILTER_NAME_OUT);
unsafe {
let _ = rules.Remove(&filter_name_in);
let _ = rules.Remove(&filter_name_out);
}
let exe_path = BSTR::from(exe_path.to_string_lossy().to_string()); 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 { unsafe {
let inbound_rule: INetFwRule = let rule: INetFwRule =
CoCreateInstance(&NetFwRule, None, CLSCTX_INPROC_SERVER).unwrap(); CoCreateInstance(&NetFwRule, None, CLSCTX_INPROC_SERVER).unwrap();
inbound_rule.SetName(&filter_name_in).unwrap(); rule.SetName(&BSTR::from(filter.0)).unwrap();
inbound_rule.SetApplicationName(&exe_path).unwrap(); rule.SetApplicationName(&exe_path).unwrap();
inbound_rule.SetDirection(NET_FW_RULE_DIR_IN).unwrap(); rule.SetDirection(filter.1).unwrap();
inbound_rule.SetEnabled(true.into()).unwrap(); rule.SetEnabled(true.into()).unwrap();
inbound_rule.SetAction(NET_FW_ACTION_BLOCK).unwrap(); rule.SetAction(NET_FW_ACTION_BLOCK).unwrap();
inbound_rule.SetProtocol(NET_FW_IP_PROTOCOL_ANY.0).unwrap(); rule.SetProtocol(NET_FW_IP_PROTOCOL_ANY.0).unwrap();
rules.Add(&inbound_rule).unwrap(); rules.Add(&rule).unwrap();
} }
unsafe {
let outbound_rule: INetFwRule =
CoCreateInstance(&NetFwRule, None, CLSCTX_INPROC_SERVER).unwrap();
outbound_rule.SetName(&filter_name_out).unwrap();
outbound_rule.SetApplicationName(&exe_path).unwrap();
outbound_rule.SetDirection(NET_FW_RULE_DIR_OUT).unwrap();
outbound_rule.SetEnabled(true.into()).unwrap();
outbound_rule.SetAction(NET_FW_ACTION_BLOCK).unwrap();
outbound_rule.SetProtocol(NET_FW_IP_PROTOCOL_ANY.0).unwrap();
rules.Add(&outbound_rule).unwrap();
} }
self.blocked_status = self.is_blocked().into(); self.blocked_status = Self::is_blocked().into();
} }
pub fn unblock_all(&mut self) { pub fn unblock_all(&mut self) {
let policy: INetFwPolicy2 = let policy: INetFwPolicy2 =
unsafe { CoCreateInstance(&NetFwPolicy2, None, CLSCTX_INPROC_SERVER).unwrap() }; unsafe { CoCreateInstance(&NetFwPolicy2, None, CLSCTX_INPROC_SERVER).unwrap() };
let rules = unsafe { policy.Rules().unwrap() }; let rules = unsafe { policy.Rules().unwrap() };
unsafe { unsafe { rules.Remove(&BSTR::from(FILTER_NAME_IN)).unwrap() };
let result = rules.Remove(&BSTR::from(FILTER_NAME_IN)); unsafe { rules.Remove(&BSTR::from(FILTER_NAME_OUT)).unwrap() };
if let Err(ref why) = result { self.blocked_status = Self::is_blocked().into();
if why.code() != E_INVALIDARG {
result.unwrap();
}
}
let result = rules.Remove(&BSTR::from(FILTER_NAME_OUT));
if let Err(ref why) = result {
if why.code() != E_INVALIDARG {
result.unwrap();
}
}
}
self.blocked_status = self.is_blocked().into();
} }
fn is_blocked(&self) -> bool { fn is_blocked() -> bool {
let policy: INetFwPolicy2 = let policy: INetFwPolicy2 =
unsafe { CoCreateInstance(&NetFwPolicy2, None, CLSCTX_INPROC_SERVER).unwrap() }; unsafe { CoCreateInstance(&NetFwPolicy2, None, CLSCTX_INPROC_SERVER).unwrap() };
let rules = unsafe { policy.Rules().unwrap() }; let rules = unsafe { policy.Rules().unwrap() };
@@ -172,7 +129,7 @@ impl GameNetworking {
{ {
self.counting = false; self.counting = false;
self.blocked_status = BlockedStatus::Unblocked; self.blocked_status = BlockedStatus::Unblocked;
}; }
} }
} }
+24 -9
View File
@@ -17,13 +17,23 @@ enum Stage {
About, About,
} }
#[derive(Debug, Default)] #[derive(Debug)]
pub struct Flags { pub struct Flags {
pub elevated: bool, pub elevated: bool,
pub debug: bool, pub debug: bool,
closing: bool, closing: bool,
} }
impl Default for Flags {
fn default() -> Self {
Self {
elevated: win::is_elevated(),
debug: false,
closing: false,
}
}
}
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct App { pub struct App {
pub meta: Meta, pub meta: Meta,
@@ -35,7 +45,7 @@ pub struct App {
pub anti_afk: features::anti_afk::AntiAfk, pub anti_afk: features::anti_afk::AntiAfk,
empty_session: features::empty_session::EmptySession, empty_session: features::empty_session::EmptySession,
force_close: features::force_close::ForceClose, force_close: features::force_close::ForceClose,
game_networking: features::game_networking::GameNetworking, pub game_networking: features::game_networking::GameNetworking,
pub launch: features::launch::Launch, pub launch: features::launch::Launch,
} }
@@ -139,9 +149,20 @@ impl App {
.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.add(
egui::Image::new(egui::include_image!("../../assets/circle.svg"))
.max_size([4.0, 4.0].into())
.tint(self.game_networking.blocked_status.to_color32()),
)
.on_hover_text("This turns yellow if GTA Tools\ncannot find your game.");
self.game_networking.if_failed_return_to_unblocked();
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
@@ -156,12 +177,6 @@ impl App {
{ {
self.game_networking.unblock_all(); self.game_networking.unblock_all();
} }
ui.add(
egui::Image::new(egui::include_image!("../../assets/circle.svg"))
.max_size([4.0, 4.0].into())
.tint(self.game_networking.blocked_status.to_color32()),
);
self.game_networking.if_failed_return_to_unblocked();
}); });
}); });
response.response.on_disabled_hover_text( response.response.on_disabled_hover_text(
+22
View File
@@ -0,0 +1,22 @@
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,
}
}
}
impl BlockedStatus {
pub fn to_color32(self) -> egui::Color32 {
self.into()
}
}
+9
View File
@@ -50,6 +50,15 @@ impl App {
}; };
ui.label(format!("focused: \"{current_title}\"")); 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| {
tools::build_menu(ui, &mut self.game_networking.blocked_status);
});
});
}); });
ui.collapsing("anti afk", |ui| { ui.collapsing("anti afk", |ui| {
ui.label(format!( ui.label(format!(
+1
View File
@@ -1,4 +1,5 @@
mod app; mod app;
mod colours;
mod debug; mod debug;
pub mod run; pub mod run;
pub mod settings; pub mod settings;
+1 -3
View File
@@ -34,11 +34,9 @@ fn app_creator(
app.settings = persistent_state.settings; app.settings = persistent_state.settings;
} }
// check if we're elevated. if not, and the user wants an elevated launch - relaunch elevated // check if we're elevated. if not, and the user wants an elevated launch - relaunch elevated
let elevated = util::win::is_elevated(); if !app.flags.elevated && app.settings.start_elevated {
if app.settings.start_elevated && !elevated {
util::win::elevate(util::win::ElevationExitMethod::Forced); util::win::elevate(util::win::ElevationExitMethod::Forced);
} }
app.flags.elevated = elevated;
// refresh sysinfo because it initializes with nothing // refresh sysinfo because it initializes with nothing
app.sysinfo.refresh_all(); app.sysinfo.refresh_all();
// enable image loading support in egui // enable image loading support in egui
-7
View File
@@ -8,10 +8,3 @@ pub mod game {
pub const EXE_LEGACY: &str = "GTA5.exe"; pub const EXE_LEGACY: &str = "GTA5.exe";
pub const WINDOW_TITLE: &str = "Grand Theft Auto V"; pub const WINDOW_TITLE: &str = "Grand Theft Auto V";
} }
pub mod colours {
use eframe::egui::Color32;
pub const RED: Color32 = Color32::from_rgb(249, 101, 84);
pub const YELLOW: Color32 = Color32::from_rgb(249, 236, 84);
pub const GREEN: Color32 = Color32::from_rgb(104, 249, 84);
}