12 Commits
10 changed files with 294 additions and 124 deletions
Generated
+1 -1
View File
@@ -812,7 +812,7 @@ dependencies = [
[[package]]
name = "gta-tools"
version = "0.10.0"
version = "0.11.0"
dependencies = [
"catppuccin-egui",
"eframe",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "gta-tools"
version = "0.10.0"
version = "0.11.0"
edition = "2024"
license = "Apache-2.0"
authors = ["futile <git@futile.eu>"]
+6 -6
View File
@@ -3,8 +3,8 @@
A toolset of convenient things for GTA V Online.
<picture>
<source srcset="https://i.vgy.me/M4sOHh.png" media="(prefers-color-scheme: dark)">
<img src="https://i.vgy.me/mpO9uc.png">
<source srcset="https://i.vgy.me/xt8EfK.png" media="(prefers-color-scheme: dark)">
<img src="https://i.vgy.me/lYlZnd.png">
</picture>
## Installing
@@ -13,14 +13,14 @@ A toolset of convenient things for GTA V Online.
Download the latest release [here](https://github.com/elituf/gta-tools/releases/latest/download/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**.
You will need Rust, which you can get [here](https://rustup.rs). Then, clone this repo and navigate to it. At this point, you should probably `git checkout x.x.x`, where `x.x.x` is the latest tag. You can then run `cargo build --release`. Once you do that, you can use the binary located at `.\target\release\gta-tools.exe` in the same way as **Option 1**.
It is recommended to always use an up-to-date version of GTA Tools from [releases](https://github.com/elituf/gta-tools/releases). You can also easily access this repository by going to the <kbd>About</kbd> page of GTA Tools and clicking the GitHub button beside the version number.
## Guide
Every 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 always use an up-to-date version of GTA Tools from [releases](https://github.com/elituf/gta-tools/releases). You can also easily access this repository by going to the <kbd>About</kbd> page of GTA Tools and clicking the GitHub button beside the version number.
#### Game
This section is quite simple.
@@ -61,7 +61,7 @@ This feature is primarily useful for **replay glitching**, which is an exploit t
## Issues
- I have noticed that on my current Windows 10 install, when not elevated (administrator), the <kbd>Force close game</kbd> and <kbd>Empty current session</kbd> features can fail due to being denied access. This was tested by other people and is not guaranteed to happen. If this does happen to you, I recommend using GTA Tools always in elevated mode. Check "Always start elevated" in the Settings tab.
- It is possible that when not elevated (administrator), the <kbd>Force close game</kbd> and <kbd>Empty current session</kbd> features can fail due to being denied access to the game. This is not guaranteed to happen. If this does happen to you, I recommend always using GTA Tools in elevated mode. For convenience, you may check "Always start elevated" in the Settings tab.
+13
View File
@@ -0,0 +1,13 @@
### <ins>todo</ins>
- rewrite entire codebase
- add global hotkeys
### <ins>doing</ins>
- better error reporting
### <ins>done</ins>
- change `is_system_theme_dark` to `is_system_theme_light`
- fix weird changing colour on futile link in about
+113 -46
View File
@@ -1,9 +1,13 @@
use crate::util::{
consts::game::{EXE_ENHANCED, EXE_LEGACY},
system_info::SystemInfo,
use crate::{
gui::settings::BlockMethod,
util::{
consts::game::{EXE_ENHANCED, EXE_LEGACY},
system_info::SystemInfo,
},
};
use std::{
path::Path,
error::Error,
path::{Path, PathBuf},
time::{Duration, Instant},
};
use strum::{Display, EnumIter};
@@ -11,7 +15,7 @@ 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,
NET_FW_RULE_DIR_OUT, NetFwPolicy2, NetFwRule,
},
System::Com::{
CLSCTX_INPROC_SERVER, COINIT_MULTITHREADED, CoCreateInstance, CoInitializeEx,
@@ -21,8 +25,8 @@ use windows::{
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 FILTER_NAME_EXE: &str = "[GTA Tools] Block outbound traffic for all of GTA V";
const FILTER_NAME_SAVE_SERVER: &str = "[GTA Tools] Block outbound traffic to Rockstar save server";
const INTERVAL: Duration = Duration::from_secs(3);
@@ -54,7 +58,11 @@ pub struct GameNetworking {
impl Default for GameNetworking {
fn default() -> Self {
Self {
blocked_status: Self::is_blocked().into(),
blocked_status: if Self::is_save_server_blocked().unwrap() {
Self::is_save_server_blocked().unwrap().into()
} else {
Self::is_exe_blocked().unwrap().into()
},
com_initialized: unsafe { CoInitializeEx(None, COINIT_MULTITHREADED) }.is_ok(),
timer: Instant::now(),
counting: false,
@@ -70,53 +78,93 @@ impl Drop for GameNetworking {
}
}
enum Mode {
EntireGame(PathBuf),
SaveServer(String),
}
impl GameNetworking {
pub fn block_all(&mut self, system_info: &mut SystemInfo) {
fn block_generic(&self, mode: Mode) -> Result<(), Box<dyn Error>> {
let policy: INetFwPolicy2 =
unsafe { CoCreateInstance(&NetFwPolicy2, None, CLSCTX_INPROC_SERVER) }?;
let rules = unsafe { policy.Rules() }?;
let filter_name = match mode {
Mode::EntireGame(_) => FILTER_NAME_EXE,
Mode::SaveServer(_) => FILTER_NAME_SAVE_SERVER,
};
unsafe { rules.Remove(&BSTR::from(filter_name)) }?;
let rule: INetFwRule = unsafe { CoCreateInstance(&NetFwRule, None, CLSCTX_INPROC_SERVER) }?;
unsafe { rule.SetName(&BSTR::from(filter_name)) }?;
match mode {
Mode::EntireGame(exe_path) => {
let exe_path = BSTR::from(exe_path.to_string_lossy().to_string());
unsafe { rule.SetApplicationName(&exe_path) }?;
}
Mode::SaveServer(save_server_ip) => {
unsafe { rule.SetRemoteAddresses(&BSTR::from(save_server_ip)) }?;
}
}
unsafe { rule.SetDirection(NET_FW_RULE_DIR_OUT) }?;
unsafe { rule.SetEnabled(true.into()) }?;
unsafe { rule.SetAction(NET_FW_ACTION_BLOCK) }?;
unsafe { rule.SetProtocol(NET_FW_IP_PROTOCOL_ANY.0) }?;
unsafe { rules.Add(&rule) }?;
Ok(())
}
fn unblock_generic(&self, filter_name: &str) -> Result<(), Box<dyn Error>> {
let policy: INetFwPolicy2 =
unsafe { CoCreateInstance(&NetFwPolicy2, None, CLSCTX_INPROC_SERVER) }?;
let rules = unsafe { policy.Rules() }?;
unsafe { rules.Remove(&BSTR::from(filter_name)) }?;
Ok(())
}
fn is_blocked_generic(filter_name: &str) -> Result<bool, Box<dyn Error>> {
let policy: INetFwPolicy2 =
unsafe { CoCreateInstance(&NetFwPolicy2, None, CLSCTX_INPROC_SERVER) }?;
let rules = unsafe { policy.Rules() }?;
let rule_exists = unsafe { rules.Item(&BSTR::from(filter_name)) }.is_ok();
Ok(rule_exists)
}
pub fn block_exe(&mut self, system_info: &mut SystemInfo) -> Result<(), Box<dyn Error>> {
let Some(exe_path) = get_game_exe_path(system_info) else {
self.blocked_status = BlockedStatus::Failed;
return;
return Ok(());
};
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),
] {
unsafe { rules.Remove(&BSTR::from(filter.0)) }.unwrap();
let rule: INetFwRule =
unsafe { CoCreateInstance(&NetFwRule, None, CLSCTX_INPROC_SERVER) }.unwrap();
unsafe { rule.SetName(&BSTR::from(filter.0)) }.unwrap();
unsafe { rule.SetApplicationName(&exe_path) }.unwrap();
unsafe { rule.SetDirection(filter.1) }.unwrap();
unsafe { rule.SetEnabled(true.into()) }.unwrap();
unsafe { rule.SetAction(NET_FW_ACTION_BLOCK) }.unwrap();
unsafe { rule.SetProtocol(NET_FW_IP_PROTOCOL_ANY.0) }.unwrap();
unsafe { rules.Add(&rule) }.unwrap();
}
self.blocked_status = Self::is_blocked().into();
self.block_generic(Mode::EntireGame(exe_path.to_path_buf()))?;
self.blocked_status = Self::is_exe_blocked()?.into();
Ok(())
}
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();
pub fn unblock_exe(&mut self) -> Result<(), Box<dyn Error>> {
self.unblock_generic(FILTER_NAME_EXE)?;
self.blocked_status = Self::is_exe_blocked()?.into();
Ok(())
}
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
fn is_exe_blocked() -> Result<bool, Box<dyn Error>> {
Self::is_blocked_generic(FILTER_NAME_EXE)
}
pub fn if_failed_return_to_boolean(&mut self) {
pub fn block_save_server(&mut self, save_server_ip: &str) -> Result<(), Box<dyn Error>> {
self.block_generic(Mode::SaveServer(save_server_ip.to_owned()))?;
self.blocked_status = Self::is_save_server_blocked()?.into();
Ok(())
}
pub fn unblock_save_server(&mut self) -> Result<(), Box<dyn Error>> {
self.unblock_generic(FILTER_NAME_SAVE_SERVER)?;
self.blocked_status = Self::is_save_server_blocked()?.into();
Ok(())
}
pub fn is_save_server_blocked() -> Result<bool, Box<dyn Error>> {
Self::is_blocked_generic(FILTER_NAME_SAVE_SERVER)
}
pub fn reset_indicator_if_failed(&mut self) {
if self.blocked_status == BlockedStatus::Failed && !self.counting {
self.counting = true;
self.timer = Instant::now();
@@ -126,7 +174,26 @@ impl GameNetworking {
&& self.timer.elapsed() >= INTERVAL
{
self.counting = false;
self.blocked_status = Self::is_blocked().into();
self.blocked_status = Self::is_exe_blocked().unwrap().into();
}
}
pub fn ensure_not_both_blocked_simultaneously(&mut self, block_method: BlockMethod) {
match block_method {
BlockMethod::EntireGame => {
if Self::is_save_server_blocked().unwrap() {
// ignoring the return because if this is an error the user can just thug it out at that point
let _ = self.unblock_save_server();
self.blocked_status = Self::is_exe_blocked().unwrap().into();
}
}
BlockMethod::SaveServer => {
if Self::is_exe_blocked().unwrap() {
// ignoring the return because if this is an error the user can just thug it out at that point
let _ = self.unblock_exe();
self.blocked_status = Self::is_save_server_blocked().unwrap().into();
}
}
}
}
}
+94 -33
View File
@@ -1,6 +1,10 @@
use crate::{
features,
gui::{settings::Settings, tools, ui_ext::UiExt},
gui::{
settings::{BlockMethod, ROCKSTAR_SAVE_SERVER, Settings},
tools,
ui_ext::UiExt,
},
util::{
consts::{colours, game::WINDOW_TITLE},
persistent_state::PersistentState,
@@ -156,15 +160,15 @@ impl App {
egui::Frame::new()
.outer_margin(egui::vec2(0.0, -2.0))
.show(ui, |ui| {
let response = ui.add_enabled_ui(self.flags.elevated, |ui| {
ui.add_enabled_ui(self.flags.elevated, |ui| {
let label = ui.horizontal(|ui| {
let label = ui.label("Game's network access");
let label = match self.settings.block_method {
BlockMethod::EntireGame => ui.label("Game's network access"),
BlockMethod::SaveServer => ui.label("Rockstar save server access"),
};
ui.add_space(1.0);
ui.create_indicator_dot(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();
ui.create_indicator_dot(self.game_networking.blocked_status);
self.game_networking.reset_indicator_if_failed();
label
});
ui.horizontal(|ui| {
@@ -175,19 +179,36 @@ impl App {
.add_sized([button_width, 18.0], egui::Button::new("Block"))
.clicked()
{
self.game_networking.block_all(&mut self.system_info);
match self.settings.block_method {
BlockMethod::EntireGame => {
self.game_networking
.block_exe(&mut self.system_info)
.unwrap();
}
BlockMethod::SaveServer => {
self.game_networking
.block_save_server(&self.settings.save_server_ip)
.unwrap();
}
}
}
if ui
.add_sized([button_width, 18.0], egui::Button::new("Unblock"))
.clicked()
{
self.game_networking.unblock_all();
match self.settings.block_method {
BlockMethod::EntireGame => {
self.game_networking.unblock_exe().unwrap();
}
BlockMethod::SaveServer => {
self.game_networking.unblock_save_server().unwrap();
}
}
}
});
});
response.response.on_disabled_hover_text(
"This requires administrator.\nUse the Elevate button.",
);
})
.response
.on_disabled_hover_text("This requires administrator.\nUse the Elevate button.");
});
}
@@ -198,27 +219,63 @@ impl App {
}
fn show_settings_stage(&mut self, ctx: &egui::Context, ui: &mut egui::Ui) {
ui.horizontal(|ui| {
ui.label("Launch version");
egui::ComboBox::from_id_salt("Launch version")
.selected_text(self.settings.launch_version.to_string())
.show_ui(ui, |ui| {
ui.build_menu(&mut self.settings.launch_version);
});
ui.collapsing("General", |ui| {
ui.checkbox(&mut self.settings.start_elevated, "Always start elevated");
ui.horizontal(|ui| {
let selection = self.settings.theme;
egui::ComboBox::from_id_salt("Theme")
.selected_text(self.settings.theme.to_string())
.show_ui(ui, |ui| {
ui.build_menu(&mut self.settings.theme);
});
if selection != self.settings.theme {
catppuccin_egui::set_theme(ctx, self.settings.theme.into());
}
ui.label("Theme");
});
});
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);
ui.collapsing("Game", |ui| {
ui.horizontal(|ui| {
egui::ComboBox::from_id_salt("Launch version")
.selected_text(self.settings.launch_version.to_string())
.show_ui(ui, |ui| {
ui.build_menu(&mut self.settings.launch_version);
});
ui.label("Launch version");
});
});
ui.collapsing("Network", |ui| {
ui.add_enabled_ui(self.flags.elevated, |ui| {
ui.horizontal(|ui| {
egui::ComboBox::from_id_salt("Block method")
.selected_text(self.settings.block_method.to_string())
.show_ui(ui, |ui| {
ui.build_menu(&mut self.settings.block_method);
});
ui.label("Block method");
self.game_networking
.ensure_not_both_blocked_simultaneously(self.settings.block_method);
});
if selection != self.settings.theme {
catppuccin_egui::set_theme(ctx, self.settings.theme.into());
}
ui.horizontal(|ui| {
ui.add_enabled_ui(
self.settings.block_method == BlockMethod::SaveServer,
|ui| {
ui.add(
egui::TextEdit::singleline(&mut self.settings.save_server_ip)
.char_limit(15)
.desired_width(92.0),
);
ui.label("Save server IP");
if ui.button("").clicked() {
self.settings.save_server_ip = String::from(ROCKSTAR_SAVE_SERVER);
}
},
);
});
})
.response
.on_disabled_hover_text("This requires administrator.\nUse the Elevate button.");
});
ui.checkbox(&mut self.settings.start_elevated, "Always start elevated");
}
fn show_about_stage(&self, _ctx: &egui::Context, ui: &mut egui::Ui) {
@@ -232,7 +289,11 @@ impl App {
ui.label("");
});
ui.label(" from ");
ui.hyperlink_to("futile", "https://futile.eu");
ui.scope(|ui| {
ui.style_mut().visuals.hyperlink_color =
catppuccin_egui::Theme::from(self.settings.theme).text;
ui.hyperlink_to("futile", "https://futile.eu");
});
});
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
ui.style_mut().spacing.button_padding = egui::Vec2::new(4.0, 0.0);
+57 -32
View File
@@ -2,6 +2,41 @@ use crate::util::win;
use serde::{Deserialize, Serialize};
use strum::{Display, EnumIter};
pub const ROCKSTAR_SAVE_SERVER: &str = "192.81.241.171";
#[derive(Clone, Copy, Debug, Default, Display, PartialEq, Eq, Serialize, Deserialize, EnumIter)]
pub enum Theme {
#[default]
#[strum(to_string = "Auto")]
Auto,
#[strum(to_string = "Latte")]
Latte,
#[strum(to_string = "Frappe")]
Frappe,
#[strum(to_string = "Macchiato")]
Macchiato,
#[strum(to_string = "Mocha")]
Mocha,
}
impl From<Theme> for catppuccin_egui::Theme {
fn from(val: Theme) -> Self {
match val {
Theme::Auto => {
if win::is_system_theme_light() {
catppuccin_egui::LATTE
} else {
catppuccin_egui::MOCHA
}
}
Theme::Latte => catppuccin_egui::LATTE,
Theme::Frappe => catppuccin_egui::FRAPPE,
Theme::Macchiato => catppuccin_egui::MACCHIATO,
Theme::Mocha => catppuccin_egui::MOCHA,
}
}
}
#[derive(Clone, Copy, Debug, Default, Display, PartialEq, Eq, Serialize, Deserialize, EnumIter)]
pub enum LaunchVersion {
#[default]
@@ -10,41 +45,31 @@ pub enum LaunchVersion {
}
#[derive(Clone, Copy, Debug, Default, Display, PartialEq, Eq, Serialize, Deserialize, EnumIter)]
pub enum Theme {
pub enum BlockMethod {
#[default]
#[strum(to_string = "Auto")]
Auto,
#[strum(to_string = "Catppuccin Latte")]
CatppuccinLatte,
#[strum(to_string = "Catppuccin Frappe")]
CatppuccinFrappe,
#[strum(to_string = "Catppuccin Macchiato")]
CatppuccinMacchiato,
#[strum(to_string = "Catppuccin Mocha")]
CatppuccinMocha,
#[strum(to_string = "Entire game")]
EntireGame,
#[strum(to_string = "Save server")]
SaveServer,
}
impl From<Theme> for catppuccin_egui::Theme {
fn from(val: Theme) -> Self {
match val {
Theme::Auto => {
if win::is_system_theme_dark() {
catppuccin_egui::MOCHA
} else {
catppuccin_egui::LATTE
}
}
Theme::CatppuccinLatte => catppuccin_egui::LATTE,
Theme::CatppuccinFrappe => catppuccin_egui::FRAPPE,
Theme::CatppuccinMacchiato => catppuccin_egui::MACCHIATO,
Theme::CatppuccinMocha => catppuccin_egui::MOCHA,
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Settings {
pub start_elevated: bool,
pub theme: Theme,
pub launch_version: LaunchVersion,
pub save_server_ip: String,
pub block_method: BlockMethod,
}
impl Default for Settings {
fn default() -> Self {
Self {
start_elevated: false,
theme: Theme::default(),
launch_version: LaunchVersion::default(),
block_method: BlockMethod::default(),
save_server_ip: String::from(ROCKSTAR_SAVE_SERVER),
}
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct Settings {
pub launch_version: LaunchVersion,
pub theme: Theme,
pub start_elevated: bool,
}
+3 -1
View File
@@ -4,6 +4,8 @@ mod features;
mod gui;
mod util;
use std::fmt::Write;
fn init_storage() {
if !util::consts::path::APP_STORAGE.exists() {
std::fs::create_dir_all(util::consts::path::APP_STORAGE.as_path()).unwrap();
@@ -14,7 +16,7 @@ fn panic_hook(panic_info: &std::panic::PanicHookInfo<'_>) {
let backtrace = std::backtrace::Backtrace::capture();
let mut message = format!("{panic_info}");
if backtrace.status() == std::backtrace::BacktraceStatus::Captured {
message += &format!("\nstack backtrace:\n{backtrace}");
write!(message, "\nstack backtrace:\n{backtrace}").unwrap();
}
log::error!("{message}");
}
+1 -1
View File
@@ -59,6 +59,6 @@ impl Logger {
humantime::format_rfc3339_seconds(SystemTime::now()),
record.level(),
record.args()
)
);
}
}
+5 -3
View File
@@ -80,7 +80,7 @@ pub fn is_elevated() -> bool {
result.is_ok() && elevation.TokenIsElevated != 0
}
pub fn is_system_theme_dark() -> bool {
pub fn is_system_theme_light() -> bool {
use winreg::RegKey;
let hkcu = RegKey::predef(winreg::enums::HKEY_CURRENT_USER);
let Ok(subkey) =
@@ -88,8 +88,10 @@ pub fn is_system_theme_dark() -> bool {
else {
return true;
};
let Ok(dword): Result<u32, std::io::Error> = subkey.get_value("AppsUseLightTheme") else {
let Ok(apps_use_light_theme): Result<u32, std::io::Error> =
subkey.get_value("AppsUseLightTheme")
else {
return true;
};
dword != 1
apps_use_light_theme == 1
}