14 Commits
9 changed files with 247 additions and 1624 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]
name = "gta-tools"
version = "0.5.0"
version = "0.5.2"
edition = "2024"
[dependencies]
catppuccin-egui = { version = "5.5.0", default-features = false, features = [
"egui31",
] }
chrono = "0.4.40"
chrono = { version = "0.4.40", default-features = false, features = ["clock"] }
dirs = "6.0.0"
eframe = "0.31.1"
egui_extras = { version = "0.31.1", features = ["image", "svg"] }
eframe = { version = "0.31.1", default-features = false, features = [
"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"] }
open = "5.3.2"
semver = "1.0.26"
@@ -34,6 +40,5 @@ static_vcruntime = "2.0.0"
winresource = "0.1.20"
[profile.release]
strip = true
lto = true
codegen-units = 1
+2 -2
View File
@@ -1,4 +1,4 @@
# GTA Tools
A toolset of convenient things for GTA 5 Online.
A toolset of convenient things for GTA V Online.
![](https://i.vgy.me/hbOUEa.png)
![](https://i.vgy.me/s6bf1g.png)
+21 -24
View File
@@ -1,8 +1,8 @@
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, VIRTUAL_KEY, 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,
};
const INTERVAL: Duration = Duration::from_secs(60);
@@ -40,26 +40,23 @@ impl AntiAfk {
}
fn send(vk_codes: &[VIRTUAL_KEY]) {
vk_codes.iter().for_each(|vk_code| unsafe {
keybd_event(
vk_code.0 as u8,
u8::try_from(MapVirtualKeyW(
u32::from(vk_code.0),
MAP_VIRTUAL_KEY_TYPE(0),
))
.unwrap(),
KEYBD_EVENT_FLAGS(0),
0,
);
keybd_event(
vk_code.0 as u8,
u8::try_from(MapVirtualKeyW(
u32::from(vk_code.0),
MAP_VIRTUAL_KEY_TYPE(0),
))
.unwrap(),
KEYBD_EVENT_FLAGS(2),
0,
);
});
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) };
}
}
+22 -18
View File
@@ -7,8 +7,8 @@ const INTERVAL: Duration = Duration::from_secs(3);
#[derive(Debug)]
pub struct ForceClose {
pub button_text: String,
prompting: bool,
interval: Instant,
timer: Instant,
counting: bool,
current_frame: bool,
}
@@ -16,8 +16,8 @@ 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,
}
}
@@ -25,22 +25,26 @@ impl Default for ForceClose {
impl ForceClose {
pub fn prompt(&mut self, force_close_button_clicked: bool, sysinfo: &mut System) {
if force_close_button_clicked && !self.prompting {
*self = Self {
button_text: "Are you sure?".to_owned(),
prompting: true,
interval: Instant::now(),
current_frame: true,
}
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.prompting && self.interval.elapsed() <= INTERVAL {
if force_close_button_clicked && !self.current_frame {
activate(sysinfo);
*self = Self::default();
}
} else {
*self = Self::default();
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();
}
fn finish_current_frame(&mut self) {
if self.current_frame {
self.current_frame = false;
}
+78 -60
View File
@@ -1,9 +1,11 @@
use crate::util::consts::game::{EXE_ENHANCED, EXE_LEGACY};
use std::path::Path;
use std::{
path::Path,
time::{Duration, Instant},
};
use sysinfo::System;
use windows::{
Win32::{
Foundation::E_INVALIDARG,
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,
@@ -13,29 +15,47 @@ use windows::{
CoUninitialize,
},
},
core::{BSTR, HRESULT},
core::BSTR,
};
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, PartialEq, Eq)]
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 {
pub is_blocked: bool,
com_initialized: bool,
policy: INetFwPolicy2,
pub blocked_status: BlockedStatus,
timer: Instant,
counting: bool,
}
impl Default for GameNetworking {
fn default() -> Self {
let result = unsafe { CoInitializeEx(None, COINIT_MULTITHREADED) };
let mut gn = Self {
is_blocked: false,
com_initialized: result != HRESULT(0x80010106u32 as i32),
policy: unsafe { CoCreateInstance(&NetFwPolicy2, None, CLSCTX_INPROC_SERVER).unwrap() },
};
gn.is_blocked = gn.is_blocked();
gn
Self {
blocked_status: Self::is_blocked().into(),
com_initialized: unsafe { CoInitializeEx(None, COINIT_MULTITHREADED) }.is_ok(),
timer: Instant::now(),
counting: false,
}
}
}
@@ -52,66 +72,64 @@ impl Drop for GameNetworking {
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 rules = unsafe { self.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 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());
unsafe {
let inbound_rule: INetFwRule =
CoCreateInstance(&NetFwRule, None, CLSCTX_INPROC_SERVER).unwrap();
inbound_rule.SetName(&filter_name_in).unwrap();
inbound_rule.SetApplicationName(&exe_path).unwrap();
inbound_rule.SetDirection(NET_FW_RULE_DIR_IN).unwrap();
inbound_rule.SetEnabled(true.into()).unwrap();
inbound_rule.SetAction(NET_FW_ACTION_BLOCK).unwrap();
inbound_rule.SetProtocol(NET_FW_IP_PROTOCOL_ANY.0).unwrap();
rules.Add(&inbound_rule).unwrap();
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();
}
}
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.is_blocked = self.is_blocked();
self.blocked_status = Self::is_blocked().into();
}
pub fn unblock_all(&mut self) {
let rules = unsafe { self.policy.Rules().unwrap() };
unsafe {
let result = rules.Remove(&BSTR::from(FILTER_NAME_IN));
if let Err(ref why) = result {
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.is_blocked = self.is_blocked();
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(&self) -> bool {
let rules = unsafe { self.policy.Rules().unwrap() };
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_unblocked(&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 = BlockedStatus::Unblocked;
}
}
}
fn get_game_exe_path(sysinfo: &mut System) -> Option<&Path> {
+13 -11
View File
@@ -139,9 +139,20 @@ impl App {
.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");
let label = ui.horizontal(|ui| {
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| {
let available_width = label.rect.width();
let available_width = label.inner.rect.width();
let spacing = ui.spacing().item_spacing.x;
let button_width = (available_width - spacing) / 2.0;
if ui
@@ -156,15 +167,6 @@ impl App {
{
self.game_networking.unblock_all();
}
let tint = match self.game_networking.is_blocked {
true => egui::Color32::from_hex("#f96554").unwrap(),
false => egui::Color32::from_hex("#68f954").unwrap(),
};
ui.add(
egui::Image::new(egui::include_image!("../../assets/circle.svg"))
.max_size([4.0, 4.0].into())
.tint(tint),
);
});
});
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, 128, 128);
pub const YELLOW: egui::Color32 = egui::Color32::from_rgb(255, 255, 128);
pub const GREEN: egui::Color32 = egui::Color32::from_rgb(128, 255, 128);
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()
}
}
+1
View File
@@ -1,4 +1,5 @@
mod app;
mod colours;
mod debug;
pub mod run;
pub mod settings;