27 Commits
Author SHA1 Message Date
futile 4ce7f3459d 0.4.1 2025-04-17 02:33:27 +01:00
futile c8cbb51f43 thgank you clippy that's very smar thtat's very clever 2025-04-17 02:27:10 +01:00
futile 9c53239276 remove an i32 cast from is_any_key_pressed 2025-04-17 02:25:07 +01:00
futile af5ea259e9 implicit type signature on buffer var in is_window_focused 2025-04-17 02:12:34 +01:00
futile 162350699a change flow of util::win::is_elevated 2025-04-17 02:01:31 +01:00
futile 2977398437 slightly reduce unsafe scopes 2025-04-17 01:48:45 +01:00
futile 36dce5aa1e reduce ai idiocy 2025-04-17 01:39:01 +01:00
futile 6935b852ce clippy 2025-04-17 01:31:49 +01:00
futile 1c048e6aff revert 33fbbc7f 2025-04-17 00:50:09 +01:00
futile 84a659a2b1 add button to debug to open app storage path 2025-04-17 00:49:20 +01:00
futile db4ca22f95 log panics to a file 2025-04-17 00:44:06 +01:00
futile 284c4bb46d add a mouse cursor check to anti afk activation 2025-04-16 23:59:50 +01:00
futile 5fe697223c 0.4.0 2025-04-11 17:27:28 +01:00
futile 33fbbc7fc0 change windows_subsystem = "windows" requirement to debug also 2025-04-11 17:27:22 +01:00
futile 5bcdccdfea use windows vkcodes instead of defining our own 2025-04-10 18:19:12 +01:00
futile 31ac09cd25 make the thing nicer 2025-04-10 18:14:24 +01:00
futile 1283f34174 make Frame of "Game's network access" area invisible 2025-04-10 17:40:40 +01:00
futile 072c8ae406 clippy 2025-04-10 17:28:58 +01:00
futile e283393889 move header 2025-04-10 17:23:22 +01:00
futile cf5c37c7dc add bolder ubuntu font for headers & move rest of setup code into app_creator 2025-04-10 17:19:42 +01:00
futile c6cd0cf2f9 reintroduce ScrollArea to CentralPanel and increase WINDOW_SIZE to accommodate 2025-04-10 16:52:20 +01:00
futile d182ca5318 implement ComboBox builder 2025-04-10 16:32:30 +01:00
futile 42ee688639 move persistent state into own file 2025-04-10 15:17:19 +01:00
futile ba49ed0e67 add basic settings 2025-04-10 13:56:00 +01:00
futile f9f326904e turn crate::gui into a dir module 2025-04-10 08:38:51 +01:00
futile c3a1e864a7 improvements 2025-04-10 08:26:24 +01:00
futile 7f795ba52e make label text not selectable 2025-04-10 07:48:58 +01:00
14 changed files with 440 additions and 218 deletions
Generated
+71 -3
View File
@@ -156,6 +156,12 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04"
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]] [[package]]
name = "android_system_properties" name = "android_system_properties"
version = "0.1.5" version = "0.1.5"
@@ -596,6 +602,20 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "chrono"
version = "0.4.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-link",
]
[[package]] [[package]]
name = "clipboard-win" name = "clipboard-win"
version = "5.4.0" version = "5.4.0"
@@ -1366,9 +1386,10 @@ dependencies = [
[[package]] [[package]]
name = "gta-tools" name = "gta-tools"
version = "0.3.0" version = "0.4.1"
dependencies = [ dependencies = [
"catppuccin-egui", "catppuccin-egui",
"chrono",
"dirs", "dirs",
"eframe", "eframe",
"egui_extras", "egui_extras",
@@ -1377,6 +1398,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"static_vcruntime", "static_vcruntime",
"strum 0.27.1",
"sysinfo", "sysinfo",
"windows 0.61.1", "windows 0.61.1",
"winreg", "winreg",
@@ -1425,6 +1447,30 @@ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
[[package]]
name = "iana-time-zone"
version = "0.1.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core 0.61.0",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]] [[package]]
name = "icu_collections" name = "icu_collections"
version = "1.5.0" version = "1.5.0"
@@ -1845,7 +1891,7 @@ dependencies = [
"log", "log",
"rustc-hash", "rustc-hash",
"spirv", "spirv",
"strum", "strum 0.26.3",
"termcolor", "termcolor",
"thiserror 2.0.12", "thiserror 2.0.12",
"unicode-xid", "unicode-xid",
@@ -2867,7 +2913,16 @@ version = "0.26.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
dependencies = [ dependencies = [
"strum_macros", "strum_macros 0.26.4",
]
[[package]]
name = "strum"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32"
dependencies = [
"strum_macros 0.27.1",
] ]
[[package]] [[package]]
@@ -2883,6 +2938,19 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "strum_macros"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8"
dependencies = [
"heck",
"proc-macro2",
"quote",
"rustversion",
"syn",
]
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.100" version = "2.0.100"
+3 -1
View File
@@ -1,12 +1,13 @@
[package] [package]
name = "gta-tools" name = "gta-tools"
version = "0.3.0" version = "0.4.1"
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"
dirs = "6.0.0" dirs = "6.0.0"
eframe = "0.31.1" eframe = "0.31.1"
egui_extras = { version = "0.31.1", features = ["image"] } egui_extras = { version = "0.31.1", features = ["image"] }
@@ -14,6 +15,7 @@ image = { version = "0.25.6", default-features = false, features = ["png"] }
open = "5.3.2" open = "5.3.2"
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"] }
sysinfo = "0.34.2" sysinfo = "0.34.2"
windows = { version = "0.61.1", features = [ windows = { version = "0.61.1", features = [
"Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Input_KeyboardAndMouse",
Binary file not shown.
+18 -16
View File
@@ -1,12 +1,13 @@
#![allow(clippy::cast_possible_truncation)]
use crate::util::{self, consts::GTA_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, keybd_event, KEYBD_EVENT_FLAGS, MAP_VIRTUAL_KEY_TYPE, MapVirtualKeyW, VK_NUMPAD4, VK_NUMPAD6, keybd_event,
}; };
pub const INTERVAL: Duration = Duration::from_secs(60); pub const INTERVAL: Duration = Duration::from_secs(60);
const VK_NUMPAD4: u8 = 0x64; const PRESS_KEYS: [u8; 2] = [VK_NUMPAD4.0 as u8, VK_NUMPAD6.0 as u8];
const VK_NUMPAD6: u8 = 0x66;
#[derive(Debug)] #[derive(Debug)]
pub struct AntiAfk { pub struct AntiAfk {
@@ -25,30 +26,31 @@ impl Default for AntiAfk {
impl AntiAfk { impl AntiAfk {
pub fn activate(&mut self) { pub fn activate(&mut self) {
if util::is_window_focused(GTA_WINDOW_TITLE) if can_activate() {
&& !util::is_key_pressed(VK_NUMPAD4 as i32) send(&PRESS_KEYS);
&& !util::is_key_pressed(VK_NUMPAD6 as i32)
{
send(VK_NUMPAD4);
send(VK_NUMPAD6);
} }
self.interval = Instant::now(); self.interval = Instant::now();
} }
} }
pub fn send(vk_code: u8) { pub fn can_activate() -> bool {
unsafe { use util::win::{is_any_key_pressed, is_cursor_visible, is_window_focused};
is_window_focused(GTA_WINDOW_TITLE) && !is_any_key_pressed(&PRESS_KEYS) && !is_cursor_visible()
}
pub fn send(vk_codes: &[u8]) {
vk_codes.iter().for_each(|vk_code: &u8| unsafe {
keybd_event( keybd_event(
vk_code, *vk_code,
u8::try_from(MapVirtualKeyW(u32::from(vk_code), MAP_VIRTUAL_KEY_TYPE(0))).unwrap(), u8::try_from(MapVirtualKeyW(u32::from(*vk_code), MAP_VIRTUAL_KEY_TYPE(0))).unwrap(),
KEYBD_EVENT_FLAGS(0), KEYBD_EVENT_FLAGS(0),
0, 0,
); );
keybd_event( keybd_event(
vk_code, *vk_code,
u8::try_from(MapVirtualKeyW(u32::from(vk_code), MAP_VIRTUAL_KEY_TYPE(0))).unwrap(), u8::try_from(MapVirtualKeyW(u32::from(*vk_code), MAP_VIRTUAL_KEY_TYPE(0))).unwrap(),
KEYBD_EVENT_FLAGS(2), KEYBD_EVENT_FLAGS(2),
0, 0,
); );
} });
} }
+1 -1
View File
@@ -41,7 +41,7 @@ fn get_gta_pid(sysinfo: &mut System) -> u32 {
.find(|(_, p)| p.name() == ENHANCED || p.name() == LEGACY) .find(|(_, p)| p.name() == ENHANCED || p.name() == LEGACY)
{ {
return pid.as_u32(); return pid.as_u32();
}; }
u32::MAX u32::MAX
} }
+2 -1
View File
@@ -1,8 +1,9 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{fmt::Display, path::PathBuf, process::Command}; use std::{fmt::Display, path::PathBuf, process::Command};
use strum::EnumIter;
use winreg::{RegKey, enums::HKEY_LOCAL_MACHINE}; use winreg::{RegKey, enums::HKEY_LOCAL_MACHINE};
#[derive(Clone, Copy, Default, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Copy, Default, Debug, PartialEq, Eq, Serialize, Deserialize, EnumIter)]
pub enum Platform { pub enum Platform {
#[default] #[default]
Steam, Steam,
+155 -124
View File
@@ -1,61 +1,50 @@
mod persistent_state;
mod settings;
use crate::{ use crate::{
features::{ features::{
self, self, anti_afk::AntiAfk, empty_session::EmptySession, force_close::ForceClose,
anti_afk::AntiAfk, launch::Launch,
empty_session::EmptySession,
force_close::ForceClose,
launch::{Launch, Platform},
}, },
gui::{persistent_state::PersistentState, settings::Settings},
util::{ util::{
self, self,
consts::{ENHANCED, GTA_WINDOW_TITLE, LEGACY}, consts::{APP_STORAGE_PATH, ENHANCED, GTA_WINDOW_TITLE, LEGACY},
elevation,
}, },
}; };
use eframe::egui; use eframe::egui;
use serde::{Deserialize, Serialize};
use std::{ use std::{
fs::{self, File}, fs,
io::Write, io::Write,
path::PathBuf,
sync::LazyLock,
time::{Duration, Instant}, 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, 245.0];
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(Serialize, Deserialize)]
struct PersistentState {
launcher: Platform,
}
#[derive(Debug, Default, PartialEq, Eq)] #[derive(Debug, Default, PartialEq, Eq)]
pub enum Stage { pub enum Stage {
#[default] #[default]
Main, Main,
Settings,
About, About,
} }
#[allow(clippy::struct_excessive_bools)] #[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Default)]
pub struct Flags {
elevated: bool,
debug: bool,
closing: bool,
current_frame: bool,
}
#[derive(Debug)] #[derive(Debug)]
pub struct App { pub struct App {
settings: Settings,
stage: Stage, stage: Stage,
closing: bool, flags: Flags,
debug: bool, pub sysinfo: sysinfo::System,
initialized: bool, pub game_handle: windows::Win32::Foundation::HANDLE,
elevated: bool,
current_frame: bool,
pub sysinfo: System,
pub game_handle: HANDLE,
launch: Launch, launch: Launch,
force_close: ForceClose, force_close: ForceClose,
empty_session: EmptySession, empty_session: EmptySession,
@@ -65,14 +54,11 @@ pub struct App {
impl Default for App { impl Default for App {
fn default() -> Self { fn default() -> Self {
Self { Self {
settings: Settings::default(),
stage: Stage::default(), stage: Stage::default(),
closing: false, flags: Flags::default(),
initialized: false, sysinfo: sysinfo::System::new_all(),
debug: false, game_handle: windows::Win32::Foundation::HANDLE::default(),
elevated: elevation::is_elevated(),
current_frame: false,
sysinfo: System::new_all(),
game_handle: HANDLE::default(),
launch: Launch::default(), launch: Launch::default(),
force_close: ForceClose::default(), force_close: ForceClose::default(),
empty_session: EmptySession::default(), empty_session: EmptySession::default(),
@@ -83,53 +69,48 @@ impl Default for App {
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) {
if !self.initialized { catppuccin_egui::set_theme(ctx, self.settings.theme.into());
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;
}
self.run_timers(); self.run_timers();
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.selectable_value(&mut self.stage, Stage::Main, "Main");
ui.selectable_value(&mut self.stage, Stage::Settings, "Settings");
ui.selectable_value(&mut self.stage, Stage::About, "About"); 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.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() {
elevation::elevate(&mut self.closing); util::win::elevate(util::win::ElevationExitMethod::Gentle(
&mut self.flags.closing,
));
} }
}); });
}); });
}); });
egui::CentralPanel::default().show(ctx, |ui| match self.stage { egui::CentralPanel::default().show(ctx, |ui| {
egui::ScrollArea::vertical()
.auto_shrink([false, true])
.show(ui, |ui| match self.stage {
Stage::Main => { Stage::Main => {
self.header(ui, "Game");
self.show_game(ctx, ui); self.show_game(ctx, ui);
self.header(ui, "Session");
self.show_session(ctx, ui); self.show_session(ctx, ui);
self.header(ui, "Network");
self.show_network(ctx, ui); self.show_network(ctx, ui);
} }
Stage::Settings => self.show_settings(ctx, ui),
Stage::About => self.show_about(ctx, ui), Stage::About => self.show_about(ctx, ui),
}); });
});
if check_debug_keycombo_pressed(ctx) { if check_debug_keycombo_pressed(ctx) {
self.debug = !self.debug; self.flags.debug = !self.flags.debug;
} }
if check_debug_viewport_close_button_pressed(ctx) { if check_debug_viewport_close_button_pressed(ctx) {
self.debug = false; self.flags.debug = false;
} }
if self.debug { if self.flags.debug {
let main_rect = ctx.input(|i| { let main_rect = ctx.input(|i| {
i.viewport() i.viewport()
.clone() .clone()
@@ -148,11 +129,11 @@ impl eframe::App for App {
.with_icon(load_icon()), .with_icon(load_icon()),
|ctx, _class| { |ctx, _class| {
if check_debug_keycombo_pressed(ctx) { if check_debug_keycombo_pressed(ctx) {
self.debug = !self.debug; self.flags.debug = !self.flags.debug;
} }
egui::CentralPanel::default().show(ctx, |ui| { egui::CentralPanel::default().show(ctx, |ui| {
egui::ScrollArea::both() egui::ScrollArea::both()
.auto_shrink([false, false]) .auto_shrink([false, true])
.show(ui, |ui| { .show(ui, |ui| {
self.show_debug(ctx, ui); self.show_debug(ctx, ui);
}); });
@@ -161,46 +142,20 @@ impl eframe::App for App {
); );
} }
ctx.request_repaint_after(Duration::from_millis(100)); ctx.request_repaint_after(Duration::from_millis(100));
if self.closing { if self.flags.closing {
ctx.send_viewport_cmd(egui::ViewportCommand::Close); ctx.send_viewport_cmd(egui::ViewportCommand::Close);
} }
} }
} }
impl App { impl App {
#[allow(clippy::unused_self)]
fn header(&self, ui: &mut egui::Ui, text: &str) {
ui.horizontal(|ui| {
ui.label(text);
ui.add(egui::Separator::default().horizontal());
});
}
fn show_game(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) { fn show_game(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) {
header(ui, "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);
}; }
egui::ComboBox::from_label("") build_combo_box::<features::launch::Platform>(ui, &mut self.launch.selected, "Launch");
.selected_text(self.launch.selected.to_string())
.width(120.0)
.show_ui(ui, |ui| {
ui.selectable_value(
&mut self.launch.selected,
Platform::Steam,
Platform::Steam.to_string(),
);
ui.selectable_value(
&mut self.launch.selected,
Platform::Rockstar,
Platform::Rockstar.to_string(),
);
ui.selectable_value(
&mut self.launch.selected,
Platform::Epic,
Platform::Epic.to_string(),
);
});
}); });
let force_close_button = ui.add_sized( let force_close_button = ui.add_sized(
[104.0, 0.0], [104.0, 0.0],
@@ -208,24 +163,25 @@ impl App {
); );
if force_close_button.clicked() && !self.force_close.prompting { if force_close_button.clicked() && !self.force_close.prompting {
self.force_close.prompting(); self.force_close.prompting();
self.current_frame = true; self.flags.current_frame = true;
}; }
if self.force_close.prompting if self.force_close.prompting
&& self.force_close.interval.elapsed() <= Duration::from_secs(3) && self.force_close.interval.elapsed() <= Duration::from_secs(3)
{ {
if force_close_button.clicked() && !self.current_frame { if force_close_button.clicked() && !self.flags.current_frame {
features::force_close::activate(&mut self.sysinfo); features::force_close::activate(&mut self.sysinfo);
self.force_close = ForceClose::default(); self.force_close = ForceClose::default();
} }
} else { } else {
self.force_close = ForceClose::default(); self.force_close = ForceClose::default();
} }
if self.current_frame { if self.flags.current_frame {
self.current_frame = false; self.flags.current_frame = false;
} }
} }
fn show_session(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) { fn show_session(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) {
header(ui, "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() {
@@ -242,7 +198,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::is_window_focused(GTA_WINDOW_TITLE) { ui.label(if util::win::is_window_focused(GTA_WINDOW_TITLE) {
"GTA is focused." "GTA is focused."
} else { } else {
"GTA is not focused!" "GTA is not focused!"
@@ -257,11 +213,11 @@ impl App {
} }
fn show_network(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) { fn show_network(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) {
header(ui, "Network");
egui::Frame::new() egui::Frame::new()
.inner_margin(egui::vec2(4.0, 4.0)) .outer_margin(egui::vec2(0.0, -2.0))
.stroke(egui::Stroke::new(1.0, THEME.overlay1))
.show(ui, |ui| { .show(ui, |ui| {
let response = ui.add_enabled_ui(self.elevated, |ui| { let response = ui.add_enabled_ui(self.flags.elevated, |ui| {
let label = ui.label("Game's network access"); let label = ui.label("Game's network access");
ui.horizontal(|ui| { ui.horizontal(|ui| {
let available_width = label.rect.width(); let available_width = label.rect.width();
@@ -272,13 +228,13 @@ impl App {
.clicked() .clicked()
{ {
features::game_networking::block_all(&mut self.sysinfo); features::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(); features::game_networking::unblock_all();
}; }
}); });
}); });
response.response.on_disabled_hover_text( response.response.on_disabled_hover_text(
@@ -304,16 +260,33 @@ 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_settings(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) {
ui.horizontal(|ui| {
ui.label("Theme");
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) { fn show_debug(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) {
ui.collapsing("times", |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!( ui.label(format!(
"anti afk timer: {}", "timer: {}",
self.anti_afk.interval.elapsed().as_secs() self.anti_afk.interval.elapsed().as_secs()
)) ));
ui.label(format!(
"can activate: {}",
features::anti_afk::can_activate()
));
}); });
ui.collapsing("sysinfo", |ui| { ui.collapsing("sysinfo", |ui| {
if ui.button("refresh all").clicked() { if ui.button("refresh all").clicked() {
@@ -351,20 +324,37 @@ impl Drop for App {
// save any persistent state to config file // save any persistent state to config file
let persistent_state = PersistentState { let persistent_state = PersistentState {
launcher: self.launch.selected, launcher: self.launch.selected,
settings: self.settings.clone(),
}; };
let config_path = CONFIG_PATH.as_path(); persistent_state.set();
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 // make sure we are not suspending game
features::empty_session::deactivate(self); features::empty_session::deactivate(self);
} }
} }
fn header(ui: &mut egui::Ui, text: &str) {
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());
});
}
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| {
for v in E::iter() {
ui.selectable_value(current_value, v, v.to_string());
}
});
}
fn check_debug_keycombo_pressed(ctx: &egui::Context) -> bool { 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))
} }
@@ -379,17 +369,63 @@ fn check_debug_viewport_close_button_pressed(ctx: &egui::Context) -> bool {
}) })
} }
fn load_icon() -> eframe::egui::IconData { fn load_icon() -> egui::IconData {
let icon = include_bytes!("../assets/icon.png"); let icon = include_bytes!("../../assets/icon.png");
let image = image::load_from_memory(icon).unwrap().into_rgba8(); let image = image::load_from_memory(icon).unwrap().into_rgba8();
let (width, height) = image.dimensions(); let (width, height) = image.dimensions();
eframe::egui::IconData { egui::IconData {
rgba: image.into_raw(), rgba: image.into_raw(),
width, width,
height, height,
} }
} }
fn panic_hook(panic_info: &std::panic::PanicHookInfo<'_>) {
let log_path = APP_STORAGE_PATH.join("panic.log");
let mut file = fs::File::options()
.create(true)
.append(true)
.open(log_path)
.unwrap();
let timestamp = chrono::Local::now().to_rfc3339();
let backtrace = std::backtrace::Backtrace::force_capture();
let message = format!("[{timestamp}]\n{panic_info}\n\n{backtrace}\n\n");
file.write_all(message.as_bytes()).unwrap();
}
#[allow(clippy::unnecessary_wraps)]
fn app_creator(
cc: &eframe::CreationContext<'_>,
) -> Result<Box<dyn eframe::App>, Box<dyn std::error::Error + Send + Sync>> {
std::panic::set_hook(Box::new(panic_hook));
let mut app = Box::<App>::default();
if let Some(persistent_state) = PersistentState::get() {
app.launch.selected = persistent_state.launcher;
app.settings = persistent_state.settings;
}
let elevated = util::win::is_elevated();
if app.settings.start_elevated && !elevated {
util::win::elevate(util::win::ElevationExitMethod::Forced);
}
app.flags.elevated = elevated;
egui_extras::install_image_loaders(&cc.egui_ctx);
cc.egui_ctx.style_mut(|style| {
style.spacing.item_spacing = egui::vec2(4.0, 4.0);
style.interaction.selectable_labels = false;
});
let mut fonts = egui::FontDefinitions::default();
fonts.font_data.insert(
"Ubuntu-Regular".to_string(),
egui::FontData::from_static(include_bytes!("../../assets/Ubuntu-Regular.ttf")).into(),
);
fonts.families.insert(
egui::FontFamily::Name("Ubuntu-Regular".into()),
vec!["Ubuntu-Regular".to_string()],
);
cc.egui_ctx.set_fonts(fonts);
Ok(app)
}
pub fn run() { pub fn run() {
let options = eframe::NativeOptions { let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default() viewport: egui::ViewportBuilder::default()
@@ -400,10 +436,5 @@ pub fn run() {
centered: true, centered: true,
..Default::default() ..Default::default()
}; };
eframe::run_native( eframe::run_native("GTA Tools", options, Box::new(app_creator)).unwrap();
"GTA Tools",
options,
Box::new(|_cc| Ok(Box::<App>::default())),
)
.unwrap();
} }
+35
View File
@@ -0,0 +1,35 @@
use crate::{features::launch::Platform, gui::settings::Settings, util::consts::APP_STORAGE_PATH};
use serde::{Deserialize, Serialize};
use std::{
fs::{self, File},
io::Write,
path::PathBuf,
sync::LazyLock,
};
static CONFIG_PATH: LazyLock<PathBuf> = LazyLock::new(|| APP_STORAGE_PATH.join("config.json"));
#[derive(Serialize, Deserialize)]
pub struct PersistentState {
pub launcher: Platform,
pub settings: Settings,
}
impl PersistentState {
pub fn get() -> Option<Self> {
fs::read_to_string(CONFIG_PATH.as_path())
.ok()
.and_then(|config| serde_json::from_str::<Self>(&config).ok())
}
pub fn set(&self) {
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(&self).unwrap();
config_file.write_all(json.as_bytes()).unwrap();
}
}
+56
View File
@@ -0,0 +1,56 @@
use serde::{Deserialize, Serialize};
use std::fmt::Display;
use strum::EnumIter;
#[allow(clippy::enum_variant_names)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, EnumIter)]
pub enum Theme {
CatppuccinLatte,
CatppuccinFrappe,
CatppuccinMacchiato,
CatppuccinMocha,
}
impl From<Theme> for catppuccin_egui::Theme {
fn from(val: Theme) -> Self {
match val {
Theme::CatppuccinLatte => catppuccin_egui::LATTE,
Theme::CatppuccinFrappe => catppuccin_egui::FRAPPE,
Theme::CatppuccinMacchiato => catppuccin_egui::MACCHIATO,
Theme::CatppuccinMocha => catppuccin_egui::MOCHA,
}
}
}
impl Display for Theme {
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 theme: Theme,
pub start_elevated: bool,
}
impl Default for Settings {
fn default() -> Self {
Self {
theme: Theme::CatppuccinMocha,
start_elevated: false,
}
}
}
+5
View File
@@ -1,4 +1,9 @@
use std::{path::PathBuf, sync::LazyLock};
pub const ENHANCED: &str = "GTA5_Enhanced.exe"; pub const ENHANCED: &str = "GTA5_Enhanced.exe";
pub const LEGACY: &str = "GTA5.exe"; pub const LEGACY: &str = "GTA5.exe";
pub const GTA_WINDOW_TITLE: &str = "Grand Theft Auto V"; pub const GTA_WINDOW_TITLE: &str = "Grand Theft Auto V";
pub static APP_STORAGE_PATH: LazyLock<PathBuf> =
LazyLock::new(|| dirs::config_local_dir().unwrap().join("GTA Tools"));
+1 -1
View File
@@ -36,6 +36,6 @@ impl Countdown {
self.i_string = self.i.to_string(); self.i_string = self.i.to_string();
if self.i == 0 { if self.i == 0 {
self.reset(); self.reset();
}; }
} }
} }
-45
View File
@@ -1,45 +0,0 @@
use windows::{
Win32::Foundation::{CloseHandle, HANDLE},
Win32::Security::{GetTokenInformation, TOKEN_ELEVATION, TOKEN_QUERY, TokenElevation},
Win32::System::Threading::{GetCurrentProcess, OpenProcessToken},
Win32::UI::{Shell::ShellExecuteW, WindowsAndMessaging::SW_HIDE},
core::{HSTRING, PCWSTR},
};
pub fn elevate(closing: &mut bool) {
let exe = std::env::current_exe().unwrap();
unsafe {
ShellExecuteW(
None,
&HSTRING::from("runas"),
&HSTRING::from(exe.as_path()),
PCWSTR::null(),
PCWSTR::null(),
SW_HIDE,
);
}
*closing = true;
}
pub fn is_elevated() -> bool {
unsafe {
let mut token: HANDLE = HANDLE::default();
if OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token).is_ok() {
let mut elevation = TOKEN_ELEVATION::default();
let size = u32::try_from(std::mem::size_of::<TOKEN_ELEVATION>()).unwrap();
let mut ret_size = size;
let result = GetTokenInformation(
token,
TokenElevation,
Some((&raw mut elevation).cast()),
size,
&mut ret_size,
);
CloseHandle(token).unwrap();
if result.is_ok() && elevation.TokenIsElevated != 0 {
return true;
}
}
false
}
}
+1 -20
View File
@@ -1,22 +1,3 @@
pub mod consts; pub mod consts;
pub mod countdown; pub mod countdown;
pub mod elevation; pub mod win;
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 }
}
+86
View File
@@ -0,0 +1,86 @@
use windows::{
Win32::{
Foundation::{CloseHandle, HANDLE},
Security::{GetTokenInformation, TOKEN_ELEVATION, TOKEN_QUERY, TokenElevation},
System::Threading::{GetCurrentProcess, OpenProcessToken},
UI::{
Input::KeyboardAndMouse::GetAsyncKeyState,
Shell::ShellExecuteW,
WindowsAndMessaging::{
CURSOR_SHOWING, CURSORINFO, GetCursorInfo, GetForegroundWindow, GetWindowTextW,
SW_NORMAL,
},
},
},
core::{HSTRING, PCWSTR},
};
pub enum ElevationExitMethod<'a> {
Gentle(&'a mut bool),
Forced,
}
pub fn is_cursor_visible() -> bool {
let mut ci = CURSORINFO {
cbSize: u32::try_from(std::mem::size_of::<CURSORINFO>()).unwrap(),
..Default::default()
};
unsafe {
GetCursorInfo(&mut ci).unwrap();
}
ci.flags == CURSOR_SHOWING
}
#[allow(clippy::cast_sign_loss)]
pub fn is_window_focused(target_title: &str) -> bool {
let mut buffer = [0; 512];
unsafe {
let hwnd = GetForegroundWindow();
let length = GetWindowTextW(hwnd, &mut buffer);
let current_title = String::from_utf16_lossy(&buffer[..length as usize]);
current_title == target_title
}
}
pub fn is_any_key_pressed(keys: &[u8]) -> bool {
keys.iter()
.any(|&key| unsafe { (GetAsyncKeyState(i32::from(key)) & i16::MIN) != 0 })
}
pub fn elevate(closing: ElevationExitMethod) {
let exe = std::env::current_exe().unwrap();
unsafe {
ShellExecuteW(
None,
&HSTRING::from("runas"),
&HSTRING::from(exe.as_path()),
PCWSTR::null(),
PCWSTR::null(),
SW_NORMAL,
);
}
match closing {
ElevationExitMethod::Gentle(closing) => *closing = true,
ElevationExitMethod::Forced => std::process::exit(0),
}
}
pub fn is_elevated() -> bool {
let mut token: HANDLE = HANDLE::default();
unsafe {
if OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token).is_err() {
return false;
}
let mut elevation = TOKEN_ELEVATION::default();
let mut size = u32::try_from(std::mem::size_of::<TOKEN_ELEVATION>()).unwrap();
let result = GetTokenInformation(
token,
TokenElevation,
Some((&raw mut elevation).cast()),
size,
&mut size,
);
CloseHandle(token).unwrap();
result.is_ok() && elevation.TokenIsElevated != 0
}
}