33 Commits
Author SHA1 Message Date
futile 0cb211c103 0.8.0 2025-09-12 05:17:47 +01:00
futile 90b32c5c5a this couldn't hurt to be create_dir_all instead of create_dir 2025-09-12 05:12:11 +01:00
futile 71c3c69ea2 a few clippy fixes 2025-09-12 05:04:12 +01:00
futile b553e418f5 reduce clutter in system_info 2025-09-12 01:21:56 +01:00
futile ab026c0bc6 adapt codebase to own, new system info impl 2025-09-12 01:07:10 +01:00
futile 32e21f58a1 create a clone of sysinfo focused on only our needs 2025-09-12 00:52:19 +01:00
futile b0c339cc5f update readme 2025-09-09 14:09:17 +01:00
futile c2584b5d01 reimplement "update checking" in the form of a simple button that goes to latest release 2025-09-09 14:07:46 +01:00
futile 3322f9582f update readme 2025-09-09 03:16:00 +01:00
futile 5348a1d6ee rip out update checking (too unreliable) 2025-09-09 03:15:01 +01:00
futile 05ce68cb47 update readme 2025-09-09 03:03:36 +01:00
futile f63d24ea43 improve reliability and logging when running into game access denied issues 2025-09-09 03:03:32 +01:00
futile 356d5f17e5 me when i don't smoke the drugs that make you write retarded code 2025-09-08 14:32:36 +01:00
futile bc5e80aaca update readme 2025-09-06 21:49:44 +01:00
futile e050231692 update readme 2025-09-06 12:26:38 +01:00
futile 6e33536dfe 0.7.0 2025-09-06 12:11:39 +01:00
futile c36a41f919 remove extra release optimizations to save sanity on compile times 2025-09-06 12:11:20 +01:00
futile 660e7a874f improve reliability of panic hook by moving it to main and always creating its storage path 2025-09-06 12:10:38 +01:00
futile 72abda7352 partial fix some weird colouring that was introduced in egui 0.32 (?) 2025-09-05 17:12:38 +01:00
futile ebf704c6f4 update readme to reflect prior changes 2025-09-05 16:10:47 +01:00
futile 5c5e6d60a1 add option to launch legacy version of the game & fix epic games launch urls 2025-09-05 16:09:24 +01:00
futile 98a6545b27 update dependencies i think 2025-09-05 14:26:27 +01:00
futile a9a965462a random small fixes 2025-09-05 13:24:16 +01:00
futile 81b4b26f96 small grammar fix on non-windows compilation error 2025-09-04 09:44:07 +01:00
futile 06df466d24 switch back to ureq from nyquest for http client
note: even if this raises binary size by a significant amount, it should in theory be more reliable, so it is a worthy sacrifice.
2025-09-04 07:41:54 +01:00
futile b8c444dc5b move theme refresh button to debug menu 2025-09-04 06:21:27 +01:00
futile e848ca8d79 GET OUT 2025-09-04 05:36:00 +01:00
futile 0a831580e2 use reworked logging to log errors in util::meta::default 2025-05-16 10:10:16 +01:00
futile 10e69eaac3 rework (error) logging 2025-05-16 10:03:48 +01:00
futile 9dbfbc4419 rework storage paths a bit 2025-05-13 09:40:45 +01:00
futile ef0b6de6ba improve reliability of util::codeberg::get_latest_release 2025-05-13 09:40:18 +01:00
futile e7470bd694 update readme 2025-05-12 08:00:25 +01:00
futile 936f683385 update readme 2025-05-12 07:50:00 +01:00
21 changed files with 1051 additions and 886 deletions
+1
View File
@@ -1,2 +1,3 @@
/target
.vscode
.idea
Generated
+715 -632
View File
File diff suppressed because it is too large Load Diff
+16 -21
View File
@@ -1,43 +1,38 @@
[package]
name = "gta-tools"
version = "0.6.0"
version = "0.8.0"
edition = "2024"
[dependencies]
catppuccin-egui = { version = "5.5.0", default-features = false, features = [
"egui31",
catppuccin-egui = { version = "5.6.0", default-features = false, features = [
"egui32",
] }
eframe = { version = "0.31.1", default-features = false, features = [
chrono = { version = "0.4.41", default-features = false, features = ["clock"] }
eframe = { version = "0.32.2", default-features = false, features = [
"glow",
"default_fonts",
] }
egui_extras = { version = "0.31.1", default-features = false, features = [
egui_extras = { version = "0.32.2", default-features = false, features = [
"image",
"svg",
] }
image = { version = "0.25.6", default-features = false, features = ["png"] }
nyquest = { version = "0.1.1", features = ["blocking", "json"] }
nyquest-backend-winrt = { version = "0.1.0", features = ["blocking"] }
image = { version = "0.25.8", default-features = false, features = ["png"] }
open = "5.3.2"
semver = "1.0.26"
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
strum = { version = "0.27.1", features = ["derive"] }
sysinfo = "0.35.0"
windows = { version = "0.61.1", features = [
"Win32_UI_Input_KeyboardAndMouse",
serde_json = "1.0.143"
strum = { version = "0.27.2", features = ["derive"] }
windows = { version = "0.62.0", features = [
"Win32_NetworkManagement_WindowsFirewall",
"Win32_Security",
"Win32_System_Com",
"Win32_System_Diagnostics_ToolHelp",
"Win32_System_Threading",
"Win32_UI_Input_KeyboardAndMouse",
"Win32_UI_Shell",
"Win32_UI_WindowsAndMessaging",
"Win32_Security",
"Win32_NetworkManagement_WindowsFirewall",
] }
winreg = "0.55.0"
[build-dependencies]
static_vcruntime = "2.0.0"
winresource = "0.1.20"
[profile.release]
lto = true
codegen-units = 1
winresource = "0.1.23"
+14 -9
View File
@@ -2,7 +2,7 @@
A toolset of convenient things for GTA V Online.
![](https://i.vgy.me/aQ1qgw.png)
![](https://i.vgy.me/9j6ZSO.png) ![](https://i.vgy.me/xJgskM.png)
## Installing
@@ -14,15 +14,15 @@ You will need the Rust toolchain, which can be obtained [here](https://rustup.rs
## Guide
Almost every[^1] 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.
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 use an up-to-date version of GTA Tools from [releases](https://codeberg.org/futile/gta-tools/releases). You can check for updates to your existing installation by going to the <kbd>About</kbd> page of GTA Tools and hovering the <kbd>↓</kbd> button near the version number.
It is recommended to always use an up-to-date version of GTA Tools from [releases](https://codeberg.org/futile/gta-tools/releases). You can also easily access the current latest release by going to the <kbd>About</kbd> page of GTA Tools and clicking the <kbd>↓</kbd> button near the version number.
#### Game
This section is quite simple.
There is a <kbd>Launch</kbd>[^1] feature, which will start your game on the chosen launcher (I am unsure if launching Epic Games works, let me know).
There is a <kbd>Launch</kbd> feature, which will start your game on the chosen launcher and game version.
There is also a <kbd>Force close game</kbd> feature, which simply kills all game processes. This *does not* touch Rockstar Games Launcher or any other processes, only ones named `GTA5_Enhanced.exe` or `GTA5.exe`.
@@ -38,8 +38,8 @@ The first being <kbd>Empty current session</kbd>, which "removes everyone" from
The other feature is <kbd>Anti AFK</kbd>, which, when toggled, prevents you from getting kicked for idling by pressing keys on your keyboard every 60 seconds. At present, those keys are `VK_NUMPAD4` & `VK_NUMPAD6`, due to being a relatively good balance of keys that the game actually registers but don't interrupt gameplay too much if they activate while you're playing. You *must* be tabbed into GTA V for <kbd>Anti AFK</kbd> to work. While toggled, <kbd>Anti AFK</kbd> will only activate when all of the following conditions are true:
* The `Grand Theft Auto V` window is currently focused
* None of the designated keys are currently being pressed[^2]
* The mouse cursor is not currently visible[^3]
* None of the designated keys are currently being pressed[^1]
* The mouse cursor is not currently visible[^2]
#### Network
@@ -56,6 +56,11 @@ This feature is primarily useful for **replay glitching**, which is an exploit t
5. At this point, <kbd>Unblock</kbd> the game's internet access and load back into online
6. Finally, spend any amount of money, and you should receive the finale cut, but also be able to go play the finale again immediately
[^1]: <kbd>Launch</kbd> only launches Enhanced installations of the game.
[^2]: Otherwise, if you are, for example, using those keys to fly a plane, <kbd>Anti AFK</kbd> activating could cancel the input.
[^3]: Specifically, the Windows mouse cursor, not the in-game mouse cursor. This check is mostly used to prevent typing while you are in the Rockstar overlay.
## 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.
[^1]: Otherwise, if you are, for example, using those keys to fly a plane, <kbd>Anti AFK</kbd> activating could cancel the input.
[^2]: Specifically, the Windows mouse cursor, not the in-game mouse cursor. This check is mostly used to prevent typing while you are in the Rockstar overlay.
+20 -15
View File
@@ -1,9 +1,10 @@
use crate::util::{
consts::game::{EXE_ENHANCED, EXE_LEGACY},
countdown::Countdown,
log,
system_info::SystemInfo,
};
use std::time::{Duration, Instant};
use sysinfo::System;
use windows::Win32::{
Foundation::{CloseHandle, HANDLE, NTSTATUS},
System::Threading::{OpenProcess, PROCESS_SUSPEND_RESUME},
@@ -48,27 +49,31 @@ unsafe extern "system" {
unsafe fn NtResumeProcess(ProcessHandle: HANDLE) -> NTSTATUS;
}
fn get_gta_pid(sysinfo: &mut System) -> u32 {
sysinfo.refresh_all();
if let Some((pid, _)) = sysinfo
fn get_gta_pid(system_info: &mut SystemInfo) -> Option<u32> {
system_info.refresh();
system_info
.processes()
.iter()
.find(|(_, p)| p.name() == EXE_ENHANCED || p.name() == EXE_LEGACY)
{
return pid.as_u32();
}
u32::MAX
.find(|p| p.name() == EXE_ENHANCED || p.name() == EXE_LEGACY)
.map(|p| p.pid())
}
pub fn activate(game_handle: &mut HANDLE, sysinfo: &mut System) {
let pid = get_gta_pid(sysinfo);
if pid == u32::MAX {
return;
}
pub fn activate(game_handle: &mut HANDLE, system_info: &mut SystemInfo) -> Result<(), ()> {
let Some(pid) = get_gta_pid(system_info) else {
return Err(());
};
unsafe {
*game_handle = OpenProcess(PROCESS_SUSPEND_RESUME, false, pid).unwrap();
match OpenProcess(PROCESS_SUSPEND_RESUME, false, pid) {
Ok(handle) => *game_handle = handle,
Err(why) => {
let message = format!("failed to suspend game for empty session:\n{why}");
log::log(log::LogLevel::Error, &message);
return Err(());
}
}
let _ = NtSuspendProcess(*game_handle);
}
Ok(())
}
pub fn deactivate(game_handle: &mut HANDLE) {
+19 -11
View File
@@ -1,6 +1,9 @@
use crate::util::consts::game::{EXE_ENHANCED, EXE_LEGACY};
use crate::util::{
consts::game::{EXE_ENHANCED, EXE_LEGACY},
log,
system_info::SystemInfo,
};
use std::time::{Duration, Instant};
use sysinfo::System;
const INTERVAL: Duration = Duration::from_secs(3);
@@ -24,7 +27,7 @@ impl Default for ForceClose {
}
impl ForceClose {
pub fn prompt(&mut self, force_close_button_clicked: bool, sysinfo: &mut System) {
pub fn prompt(&mut self, force_close_button_clicked: bool, system_info: &mut SystemInfo) {
if force_close_button_clicked && !self.counting {
self.button_text = "Are you sure?".to_owned();
self.timer = Instant::now();
@@ -34,7 +37,7 @@ impl ForceClose {
if self.counting && self.timer.elapsed() >= INTERVAL {
self.reset();
} else if force_close_button_clicked && !self.current_frame {
activate(sysinfo);
activate(system_info);
self.reset();
}
self.finish_current_frame();
@@ -51,14 +54,19 @@ impl ForceClose {
}
}
fn activate(sysinfo: &mut System) {
sysinfo.refresh_all();
sysinfo
fn activate(system_info: &mut SystemInfo) {
system_info.refresh();
system_info
.processes()
.iter()
.filter(|(_, p)| p.name() == EXE_ENHANCED || p.name() == EXE_LEGACY)
.for_each(|(_, p)| {
p.kill();
.filter(|p| p.name() == EXE_ENHANCED || p.name() == EXE_LEGACY)
.for_each(|p| {
if !p.kill() {
log::log(
log::LogLevel::Error,
"failed to force close game, probably due to access denied",
);
}
});
sysinfo.refresh_all();
system_info.refresh();
}
+11 -13
View File
@@ -1,10 +1,12 @@
use crate::util::consts::game::{EXE_ENHANCED, EXE_LEGACY};
use crate::util::{
consts::game::{EXE_ENHANCED, EXE_LEGACY},
system_info::SystemInfo,
};
use std::{
path::Path,
time::{Duration, Instant},
};
use strum::{Display, EnumIter};
use sysinfo::System;
use windows::{
Win32::{
NetworkManagement::WindowsFirewall::{
@@ -71,8 +73,8 @@ 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 {
pub fn block_all(&mut self, system_info: &mut SystemInfo) {
let Some(exe_path) = get_game_exe_path(system_info) else {
self.blocked_status = BlockedStatus::Failed;
return;
};
@@ -133,15 +135,11 @@ impl GameNetworking {
}
}
fn get_game_exe_path(sysinfo: &mut System) -> Option<&Path> {
sysinfo.refresh_all();
if let Some((_, process)) = sysinfo
fn get_game_exe_path(system_info: &mut SystemInfo) -> Option<&Path> {
system_info.refresh();
system_info
.processes()
.iter()
.find(|(_, p)| p.name() == EXE_ENHANCED || p.name() == EXE_LEGACY)
{
process.exe()
} else {
None
}
.find(|p| p.name() == EXE_ENHANCED || p.name() == EXE_LEGACY)
.and_then(|p| p.exe())
}
+20 -8
View File
@@ -1,3 +1,4 @@
use crate::gui::settings::LaunchVersion;
use serde::{Deserialize, Serialize};
use std::{path::PathBuf, process::Command};
use strum::{Display, EnumIter};
@@ -18,16 +19,24 @@ pub struct Launch {
pub selected: Platform,
}
pub fn launch(platform: &Platform) {
pub fn launch(platform: &Platform, version: &LaunchVersion) {
match platform {
Platform::Steam => {
let _ = open::that_detached("steam://run/3240220");
let steam_url = if *version == LaunchVersion::Enhanced {
"steam://run/3240220"
} else {
"steam://run/271590"
};
let _ = open::that_detached(steam_url);
}
Platform::Rockstar => {
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
let Ok(gta_v_enhanced) =
hklm.open_subkey(r"SOFTWARE\WOW6432Node\Rockstar Games\GTAV Enhanced")
else {
let rockstar_url = if *version == LaunchVersion::Enhanced {
r"SOFTWARE\WOW6432Node\Rockstar Games\GTAV Enhanced"
} else {
r"SOFTWARE\WOW6432Node\Rockstar Games\Grand Theft Auto V"
};
let Ok(gta_v_enhanced) = hklm.open_subkey(rockstar_url) else {
return;
};
let Ok(install_folder): Result<String, std::io::Error> =
@@ -40,9 +49,12 @@ pub fn launch(platform: &Platform) {
let _ = Command::new(play_gtav_path).spawn();
}
Platform::Epic => {
let _ = open::that_detached(
"com.epicgames.launcher://apps/331226ba7c944720baa99103cb1fe80c?action=launch&silent=true",
);
let epic_url = if *version == LaunchVersion::Enhanced {
"com.epicgames.launcher://apps/8769e24080ea413b8ebca3f1b8c50951?action=launch&silent=true"
} else {
"com.epicgames.launcher://apps/9d2d0eb64d5c44529cece33fe2a46482?action=launch&silent=true"
};
let _ = open::that_detached(epic_url);
}
}
}
+39 -36
View File
@@ -1,11 +1,9 @@
use crate::{
features,
gui::{
settings::{Settings, Theme},
tools,
ui_ext::UiExt,
gui::{colours, settings::Settings, tools, ui_ext::UiExt},
util::{
consts::game::WINDOW_TITLE, persistent_state::PersistentState, system_info::SystemInfo, win,
},
util::{consts::game::WINDOW_TITLE, meta::Meta, persistent_state::PersistentState, win},
};
use eframe::egui;
use std::time::{Duration, Instant};
@@ -40,11 +38,10 @@ impl Default for Flags {
#[derive(Debug, Default)]
pub struct App {
pub meta: Meta,
pub settings: Settings,
stage: Stage,
pub flags: Flags,
pub sysinfo: sysinfo::System,
pub system_info: SystemInfo,
game_handle: windows::Win32::Foundation::HANDLE,
pub anti_afk: features::anti_afk::AntiAfk,
empty_session: features::empty_session::EmptySession,
@@ -99,7 +96,7 @@ impl App {
ui.header("Game");
ui.horizontal(|ui| {
if ui.button("Launch").clicked() {
features::launch::launch(&self.launch.selected);
features::launch::launch(&self.launch.selected, &self.settings.launch_version);
}
egui::ComboBox::from_id_salt("Launch")
.selected_text(self.launch.selected.to_string())
@@ -113,17 +110,22 @@ impl App {
egui::Button::new(&self.force_close.button_text),
);
self.force_close
.prompt(force_close_button.clicked(), &mut self.sysinfo);
.prompt(force_close_button.clicked(), &mut self.system_info);
}
fn show_session_section(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) {
ui.header("Session");
ui.add_enabled_ui(!self.empty_session.disabled, |ui| {
ui.horizontal(|ui| {
if ui.button("Empty current session").clicked() {
if ui.button("Empty current session").clicked()
&& features::empty_session::activate(
&mut self.game_handle,
&mut self.system_info,
)
.is_ok()
{
self.empty_session.interval = Instant::now();
self.empty_session.disabled = true;
features::empty_session::activate(&mut self.game_handle, &mut self.sysinfo);
}
ui.label(&self.empty_session.countdown.i_string);
});
@@ -171,7 +173,7 @@ impl App {
.add_sized([button_width, 18.0], egui::Button::new("Block"))
.clicked()
{
self.game_networking.block_all(&mut self.sysinfo);
self.game_networking.block_all(&mut self.system_info);
}
if ui
.add_sized([button_width, 18.0], egui::Button::new("Unblock"))
@@ -194,6 +196,14 @@ 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.horizontal(|ui| {
let selection = self.settings.theme;
ui.label("Theme");
@@ -205,13 +215,6 @@ impl App {
if selection != self.settings.theme {
catppuccin_egui::set_theme(ctx, self.settings.theme.into());
}
if ui
.add_visible(self.settings.theme == Theme::Auto, egui::Button::new(""))
.on_hover_text("Refresh theme")
.clicked()
{
catppuccin_egui::set_theme(ctx, self.settings.theme.into());
}
});
ui.checkbox(&mut self.settings.start_elevated, "Always start elevated");
}
@@ -222,28 +225,28 @@ impl App {
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 0.0;
ui.label("with ");
ui.scope(|ui| {
ui.style_mut().visuals.hyperlink_color = colours::RED;
ui.hyperlink_to("", "https://codeberg.org/futile/gta-tools");
});
ui.label(" from ");
ui.hyperlink_to("futile", "http://futile.eu");
ui.hyperlink_to("futile", "https://futile.eu");
});
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
ui.label(format!(
"v{} {}",
self.meta.current_version,
if cfg!(debug_assertions) { "(dev)" } else { "" }
));
let button = ui.add_enabled_ui(self.meta.newer_version_available, |ui| {
ui.style_mut().spacing.button_padding = egui::Vec2::new(3.0, 0.0);
ui.button("")
.on_disabled_hover_text("Already up to date.")
.on_hover_text(format!(
"New version available! ({})",
self.meta.latest_release.version
))
});
if button.inner.clicked() {
open::that(&self.meta.latest_release.download_url).unwrap();
if cfg!(debug_assertions) {
ui.label("(dev)");
}
ui.label(format!("v{}", env!("CARGO_PKG_VERSION")));
ui.scope(|ui| {
ui.style_mut().spacing.button_padding = egui::Vec2::new(3.0, 0.0);
let button = ui
.button("")
.on_hover_text("Go to current latest version.");
if button.clicked() {
let _ =
open::that("https://codeberg.org/futile/gta-tools/releases/latest");
}
});
});
});
ui.add(egui::Image::new(egui::include_image!(
+11 -16
View File
@@ -5,8 +5,8 @@ use crate::{
ui_ext::UiExt,
},
util::consts::{
APP_STORAGE_PATH,
game::{EXE_ENHANCED, EXE_LEGACY},
path,
},
};
use eframe::egui;
@@ -15,13 +15,8 @@ impl App {
fn add_debug_viewport_contents(&mut self, ui: &mut egui::Ui) {
ui.collapsing("misc", |ui| {
if ui.button("open storage path").clicked() {
open::that_detached(APP_STORAGE_PATH.as_path()).unwrap();
open::that_detached(path::APP_STORAGE.as_path()).unwrap();
}
ui.checkbox(
&mut self.meta.newer_version_available,
"spoof new version available",
)
.on_hover_text("(this could already be checked if\nthere actually IS a new version)");
ui.scope(|ui| {
use windows::Win32::UI::WindowsAndMessaging::{
GetForegroundWindow, GetWindowTextW,
@@ -42,6 +37,9 @@ impl App {
ui.build_menu(&mut self.game_networking.blocked_status);
});
});
if ui.add(egui::Button::new("force refresh theme")).clicked() {
catppuccin_egui::set_theme(ui.ctx(), self.settings.theme.into());
}
});
ui.collapsing("anti afk", |ui| {
ui.label(format!(
@@ -50,19 +48,16 @@ impl App {
));
ui.label(format!("can activate: {}", self.anti_afk.can_activate()));
});
ui.collapsing("sysinfo", |ui| {
if ui.button("refresh all").clicked() {
self.sysinfo.refresh_all();
ui.collapsing("system info", |ui| {
if ui.button("refresh").clicked() {
self.system_info.refresh();
}
let pid = self
.sysinfo
.system_info
.processes()
.iter()
.find(|(_, p)| p.name() == EXE_ENHANCED || p.name() == EXE_LEGACY)
.map_or_else(
|| "no pid found!".to_owned(),
|(pid, _)| pid.as_u32().to_string(),
);
.find(|p| p.name() == EXE_ENHANCED || p.name() == EXE_LEGACY)
.map_or_else(|| "no pid found!".to_owned(), |p| p.pid().to_string());
ui.label(format!("gta pid: {pid}"));
});
ui.collapsing("app state", |ui| ui.label(format!("{self:#?}")));
+4 -28
View File
@@ -3,38 +3,13 @@ use crate::{
app::{App, WINDOW_SIZE},
tools,
},
util::{consts::APP_STORAGE_PATH, persistent_state::PersistentState, win},
util::{persistent_state::PersistentState, win},
};
use eframe::egui;
use std::{
fs::File,
io::Write,
time::{SystemTime, UNIX_EPOCH},
};
fn panic_hook(panic_info: &std::panic::PanicHookInfo<'_>) {
let log_path = APP_STORAGE_PATH.join("panic.log");
let mut file = File::options()
.create(true)
.append(true)
.open(log_path)
.unwrap();
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let backtrace = std::backtrace::Backtrace::capture();
let message = format!("[{timestamp}]\n{panic_info}\nstack backtrace:\n{backtrace}\n");
file.write_all(message.as_bytes()).unwrap();
}
fn app_creator(
cc: &eframe::CreationContext<'_>,
) -> Result<Box<dyn eframe::App>, Box<dyn std::error::Error + Send + Sync>> {
// use our own panic hook which logs all panics to a file
std::panic::set_hook(Box::new(panic_hook));
// initialize http client (nyquest) for windows
nyquest_backend_winrt::register();
// initialize App early to modify some things before returning it
let mut app = Box::new(App::default());
// load previously selected launch platform & settings from persistent state
@@ -46,8 +21,8 @@ fn app_creator(
if !app.flags.elevated && app.settings.start_elevated {
win::elevate(win::ElevationExitMethod::Forced);
}
// refresh sysinfo because it initializes with nothing
app.sysinfo.refresh_all();
// refresh system info because it initializes with nothing
app.system_info.refresh();
// enable image loading support in egui
egui_extras::install_image_loaders(&cc.egui_ctx);
// set our initial theme, from earlier loaded settings. we set the egui theme
@@ -58,6 +33,7 @@ fn app_creator(
cc.egui_ctx.all_styles_mut(|style| {
style.spacing.item_spacing = egui::vec2(4.0, 4.0);
style.interaction.selectable_labels = false;
style.visuals.hyperlink_color = catppuccin_egui::Theme::from(app.settings.theme).text;
});
// load any extra fonts that we need
let mut fonts = egui::FontDefinitions::default();
+8
View File
@@ -2,6 +2,13 @@ use crate::util::win;
use serde::{Deserialize, Serialize};
use strum::{Display, EnumIter};
#[derive(Clone, Copy, Debug, Default, Display, PartialEq, Eq, Serialize, Deserialize, EnumIter)]
pub enum LaunchVersion {
#[default]
Enhanced,
Legacy,
}
#[derive(Clone, Copy, Debug, Default, Display, PartialEq, Eq, Serialize, Deserialize, EnumIter)]
pub enum Theme {
#[default]
@@ -37,6 +44,7 @@ impl From<Theme> for catppuccin_egui::Theme {
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct Settings {
pub launch_version: LaunchVersion,
pub theme: Theme,
pub start_elevated: bool,
}
+15 -1
View File
@@ -1,12 +1,26 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
#[cfg(not(target_os = "windows"))]
compile_error!("This application must be compiled on Windows.");
compile_error!("This application must be compiled for Windows.");
mod features;
mod gui;
mod util;
fn init_storage() {
if !crate::util::consts::path::APP_STORAGE.exists() {
std::fs::create_dir_all(crate::util::consts::path::APP_STORAGE.as_path()).unwrap();
}
}
fn panic_hook(panic_info: &std::panic::PanicHookInfo<'_>) {
let backtrace = std::backtrace::Backtrace::capture();
let message = format!("{panic_info}\nstack backtrace:\n{backtrace}\n");
crate::util::log::log(crate::util::log::LogLevel::Panic, &message);
}
fn main() {
init_storage();
std::panic::set_hook(Box::new(panic_hook));
gui::run::run();
}
+2 -2
View File
@@ -1,6 +1,6 @@
mod codeberg;
pub mod consts;
pub mod countdown;
pub mod meta;
pub mod log;
pub mod persistent_state;
pub mod system_info;
pub mod win;
-48
View File
@@ -1,48 +0,0 @@
use nyquest::{ClientBuilder, blocking::Request};
use semver::Version;
use serde::Deserialize;
const APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
const CODEBERG_ENDPOINT_ROOT: &str = "https://codeberg.org/api/v1";
#[derive(Debug)]
pub struct Release {
pub version: Version,
pub download_url: String,
}
impl Default for Release {
fn default() -> Self {
Self {
version: Version::new(0, 0, 0),
download_url: String::new(),
}
}
}
#[derive(Deserialize)]
struct LatestRelease {
tag_name: String,
assets: Vec<Asset>,
}
#[derive(Deserialize)]
struct Asset {
browser_download_url: String,
}
pub fn get_latest_release() -> Option<Release> {
let request_url = format!("{CODEBERG_ENDPOINT_ROOT}/repos/futile/gta-tools/releases/latest");
let client = ClientBuilder::default()
.user_agent(APP_USER_AGENT)
.build_blocking()
.unwrap();
let response = client.request(Request::get(request_url)).unwrap();
let json = response.json::<LatestRelease>().ok()?;
let tag_name = json.tag_name;
let browser_download_url = &json.assets[0].browser_download_url;
Some(Release {
version: Version::parse(&tag_name).ok()?,
download_url: browser_download_url.to_owned(),
})
}
+9 -5
View File
@@ -1,8 +1,12 @@
use std::{env, path::PathBuf, sync::LazyLock};
pub static APP_STORAGE_PATH: LazyLock<PathBuf> = LazyLock::new(|| {
PathBuf::from(env::var("LOCALAPPDATA").unwrap_or_else(|_| String::from("."))).join("GTA Tools")
});
pub mod path {
use std::{env, path::PathBuf, sync::LazyLock};
pub static APP_STORAGE: LazyLock<PathBuf> = LazyLock::new(|| {
PathBuf::from(env::var("LOCALAPPDATA").unwrap_or_else(|_| String::from(".")))
.join("GTA Tools")
});
pub static APP_CONFIG: LazyLock<PathBuf> = LazyLock::new(|| APP_STORAGE.join("config.json"));
pub static APP_LOG: LazyLock<PathBuf> = LazyLock::new(|| APP_STORAGE.join("gta-tools.log"));
}
pub mod game {
pub const EXE_ENHANCED: &str = "GTA5_Enhanced.exe";
+20
View File
@@ -0,0 +1,20 @@
use crate::util::consts::path;
use std::{fs::File, io::Write};
use strum::Display;
#[derive(Clone, Copy, Display)]
pub enum LogLevel {
Error,
Panic,
}
pub fn log(level: LogLevel, message: &str) {
let mut file = File::options()
.create(true)
.append(true)
.open(path::APP_LOG.as_path())
.unwrap();
let timestamp = chrono::Local::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, false);
let message = format!("[{timestamp}] [{level}]\n{message}\n\n");
file.write_all(message.as_bytes()).unwrap();
}
-25
View File
@@ -1,25 +0,0 @@
use crate::util::codeberg;
use semver::Version;
#[derive(Debug)]
pub struct Meta {
pub current_version: Version,
pub latest_release: codeberg::Release,
pub newer_version_available: bool,
}
impl Default for Meta {
fn default() -> Self {
let current_version = Version::parse(env!("CARGO_PKG_VERSION")).unwrap();
let latest_release = codeberg::get_latest_release().unwrap_or_default();
let newer_version_available = matches!(
&current_version.cmp_precedence(&latest_release.version),
std::cmp::Ordering::Less
);
Self {
current_version,
latest_release,
newer_version_available,
}
}
}
+3 -12
View File
@@ -1,14 +1,10 @@
use crate::{features::launch::Platform, gui::settings::Settings, util::consts::APP_STORAGE_PATH};
use crate::{features::launch::Platform, gui::settings::Settings, util::consts::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,
@@ -17,18 +13,13 @@ pub struct PersistentState {
impl PersistentState {
pub fn get() -> Option<Self> {
fs::read_to_string(CONFIG_PATH.as_path())
fs::read_to_string(path::APP_CONFIG.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 mut config_file = File::create(path::APP_CONFIG.as_path()).unwrap();
let json = serde_json::to_string_pretty(&self).unwrap();
config_file.write_all(json.as_bytes()).unwrap();
}
+120
View File
@@ -0,0 +1,120 @@
use std::{
os::windows::process::CommandExt,
path::{Path, PathBuf},
process::Command,
};
use windows::{
Win32::System::{
Diagnostics::ToolHelp::{
CreateToolhelp32Snapshot, PROCESSENTRY32, Process32First, Process32Next,
TH32CS_SNAPPROCESS,
},
Threading::{
CREATE_NO_WINDOW, OpenProcess, PROCESS_NAME_WIN32, PROCESS_QUERY_LIMITED_INFORMATION,
QueryFullProcessImageNameW,
},
},
core::PWSTR,
};
#[derive(Clone, Debug)]
pub struct Process {
pid: u32,
name: String,
exe: Option<PathBuf>,
}
impl Process {
pub fn pid(&self) -> u32 {
self.pid
}
pub fn name(&self) -> &str {
&self.name
}
pub fn exe(&self) -> Option<&Path> {
self.exe.as_deref()
}
pub fn kill(&self) -> bool {
let mut taskkill = Command::new("taskkill.exe");
taskkill.creation_flags(CREATE_NO_WINDOW.0);
taskkill.arg("/F").arg("/PID").arg(self.pid.to_string());
match taskkill.output() {
Ok(output) => output.status.success(),
Err(_) => false,
}
}
}
#[derive(Debug, Default)]
pub struct SystemInfo {
processes: Vec<Process>,
}
impl SystemInfo {
pub fn refresh(&mut self) {
let mut processes = Vec::new();
let snapshot_handle = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0).unwrap() };
let mut process_entry = PROCESSENTRY32 {
dwSize: size_of::<PROCESSENTRY32>() as u32,
..Default::default()
};
unsafe { Process32First(snapshot_handle, &raw mut process_entry).unwrap() };
let exe_full_path = get_exe_full_path(&process_entry);
processes.push(Process {
pid: process_entry.th32ProcessID,
name: c_char_arr_to_string(&process_entry.szExeFile),
exe: exe_full_path,
});
while unsafe { Process32Next(snapshot_handle, &raw mut process_entry) }.is_ok() {
let exe_full_path = get_exe_full_path(&process_entry);
processes.push(Process {
pid: process_entry.th32ProcessID,
name: c_char_arr_to_string(&process_entry.szExeFile),
exe: exe_full_path,
});
}
self.processes = processes;
}
pub fn processes(&self) -> &[Process] {
&self.processes
}
}
fn get_exe_full_path(process_entry: &PROCESSENTRY32) -> Option<PathBuf> {
let process_handle_result = unsafe {
OpenProcess(
PROCESS_QUERY_LIMITED_INFORMATION,
false,
process_entry.th32ProcessID,
)
};
let mut exename = [0u16; 260];
let mut dwsize = exename.len() as u32;
process_handle_result.map_or(None, |process_handle| {
let image_name_result = unsafe {
QueryFullProcessImageNameW(
process_handle,
PROCESS_NAME_WIN32,
PWSTR(exename.as_mut_ptr()),
&raw mut dwsize,
)
};
match image_name_result {
Ok(()) => Some(PathBuf::from(
unsafe { PWSTR(exename.as_mut_ptr()).to_string() }.unwrap(),
)),
Err(_) => None,
}
})
}
fn c_char_arr_to_string(arr: &[i8]) -> String {
arr.iter()
.take_while(|&&b| b != 0)
.map(|&b| b as u8 as char)
.collect()
}
+3 -3
View File
@@ -26,7 +26,7 @@ pub fn is_cursor_visible() -> bool {
..Default::default()
};
unsafe {
GetCursorInfo(&mut ci).unwrap();
GetCursorInfo(&raw mut ci).unwrap();
}
ci.flags == CURSOR_SHOWING
}
@@ -67,7 +67,7 @@ pub fn elevate(closing: ElevationExitMethod) {
pub fn is_elevated() -> bool {
let mut token: HANDLE = HANDLE::default();
unsafe {
if OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token).is_err() {
if OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &raw mut token).is_err() {
return false;
}
let mut elevation = TOKEN_ELEVATION::default();
@@ -77,7 +77,7 @@ pub fn is_elevated() -> bool {
TokenElevation,
Some((&raw mut elevation).cast()),
size,
&mut size,
&raw mut size,
);
CloseHandle(token).unwrap();
result.is_ok() && elevation.TokenIsElevated != 0