initial commit

This commit is contained in:
2025-03-26 11:02:25 +00:00
commit 4bc0d697dc
14 changed files with 4691 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
/target
.vscode
Generated
+4236
View File
File diff suppressed because it is too large Load Diff
+27
View File
@@ -0,0 +1,27 @@
[package]
name = "gta-tools"
version = "0.1.0"
edition = "2024"
[dependencies]
catppuccin-egui = { version = "5.5.0", default-features = false, features = [
"egui31",
] }
eframe = "0.31.1"
image = { version = "0.25.6", default-features = false }
open = "5.3.2"
sysinfo = "0.33.1"
windows = { version = "0.61.1", features = [
"Win32_UI_Input_KeyboardAndMouse",
"Win32_System_Threading",
] }
winreg = "0.55.0"
[build-dependencies]
static_vcruntime = "2.0.0"
winresource = "0.1.20"
[profile.release]
strip = true
lto = true
codegen-units = 1
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

+7
View File
@@ -0,0 +1,7 @@
fn main() {
static_vcruntime::metabuild();
winresource::WindowsResource::new()
.set_icon("assets/icon.ico")
.compile()
.unwrap();
}
+42
View File
@@ -0,0 +1,42 @@
use std::time::{Duration, Instant};
use windows::Win32::UI::Input::KeyboardAndMouse::{
KEYBD_EVENT_FLAGS, MAP_VIRTUAL_KEY_TYPE, MapVirtualKeyW, keybd_event,
};
pub const INTERVAL: Duration = Duration::from_secs(60);
const VK_SHIFT: u8 = 16;
pub struct AntiAfk {
pub enabled: bool,
pub interval: Instant,
}
impl Default for AntiAfk {
fn default() -> Self {
Self {
enabled: false,
interval: Instant::now(),
}
}
}
pub fn send(vk_code: u8) {
unsafe {
keybd_event(
vk_code,
u8::try_from(MapVirtualKeyW(u32::from(vk_code), MAP_VIRTUAL_KEY_TYPE(0))).unwrap(),
KEYBD_EVENT_FLAGS(0),
0,
);
keybd_event(
vk_code,
u8::try_from(MapVirtualKeyW(u32::from(vk_code), MAP_VIRTUAL_KEY_TYPE(0))).unwrap(),
KEYBD_EVENT_FLAGS(2),
0,
);
}
}
pub fn activate() {
send(VK_SHIFT);
}
+60
View File
@@ -0,0 +1,60 @@
use crate::gui::App;
use std::time::{Duration, Instant};
use sysinfo::System;
use windows::Win32::{
Foundation::{HANDLE, NTSTATUS},
System::Threading::{OpenProcess, PROCESS_SUSPEND_RESUME},
};
pub const INTERVAL: Duration = Duration::from_secs(10);
const ENHANCED: &str = "GTA5_Enhanced.exe";
const LEGACY: &str = "GTA5.exe";
pub struct EmptySession {
pub enabled: bool,
pub interval: Instant,
}
impl Default for EmptySession {
fn default() -> Self {
Self {
enabled: true,
interval: Instant::now(),
}
}
}
#[link(name = "ntdll")]
unsafe extern "system" {
pub unsafe fn NtSuspendProcess(ProcessHandle: HANDLE) -> NTSTATUS;
pub unsafe fn NtResumeProcess(ProcessHandle: HANDLE) -> NTSTATUS;
}
fn get_gta_pid(sysinfo: &mut System) -> u32 {
sysinfo.refresh_all();
if let Some((pid, _)) = sysinfo
.processes()
.iter()
.find(|(_, p)| p.name() == ENHANCED || p.name() == LEGACY)
{
return pid.as_u32();
};
u32::MAX
}
pub fn activate(app: &mut App) {
let pid = get_gta_pid(&mut app.sysinfo);
if pid == u32::MAX {
return;
}
unsafe {
app.game_handle = OpenProcess(PROCESS_SUSPEND_RESUME, false, pid).unwrap();
let _ = NtSuspendProcess(app.game_handle);
}
}
pub fn deactivate(app: &mut App) {
unsafe {
let _ = NtResumeProcess(app.game_handle);
}
}
+32
View File
@@ -0,0 +1,32 @@
use std::time::Instant;
use sysinfo::System;
const ENHANCED: &str = "GTA5_Enhanced.exe";
const LEGACY: &str = "GTA5.exe";
pub struct ForceClose {
pub button_text: String,
pub prompting: bool,
pub interval: Instant,
}
impl Default for ForceClose {
fn default() -> Self {
Self {
button_text: "Force close game".to_string(),
prompting: false,
interval: Instant::now(),
}
}
}
pub fn activate(sysinfo: &mut System) {
sysinfo.refresh_all();
sysinfo
.processes()
.iter()
.filter(|(_, p)| p.name() == ENHANCED || p.name() == LEGACY)
.for_each(|(_, p)| {
p.kill();
});
}
+55
View File
@@ -0,0 +1,55 @@
use std::{fmt::Display, path::PathBuf, process::Command};
use winreg::{RegKey, enums::HKEY_LOCAL_MACHINE};
#[derive(Default, Debug, PartialEq, Eq)]
pub enum Platform {
#[default]
Steam,
Rockstar,
Epic,
}
impl Display for Platform {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let x = match self {
Self::Steam => "Steam",
Self::Rockstar => "Rockstar Games",
Self::Epic => "Epic Games",
};
write!(f, "{x}")
}
}
#[derive(Default)]
pub struct Launch {
pub selected: Platform,
}
pub fn launch(platform: &Platform) {
match platform {
Platform::Steam => {
let _ = open::that_detached("steam://run/3240220");
}
Platform::Rockstar => {
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
let Ok(gta_v_enhanced) =
hklm.open_subkey(r"SOFTWARE\WOW6432Node\Rockstar Games\GTA V Enhanced")
else {
return;
};
let Ok(install_folder): Result<String, std::io::Error> =
gta_v_enhanced.get_value("InstallFolder")
else {
return;
};
let mut play_gtav_path = PathBuf::from(install_folder);
play_gtav_path.push("PlayGTAV.exe");
let _ = Command::new(play_gtav_path).spawn();
}
Platform::Epic => {
let _ = open::that_detached(
"com.epicgames.launcher://apps/331226ba7c944720baa99103cb1fe80c?action=launch&silent=true",
);
}
}
}
+4
View File
@@ -0,0 +1,4 @@
pub mod anti_afk;
pub mod empty_session;
pub mod force_close;
pub mod launch;
+177
View File
@@ -0,0 +1,177 @@
use crate::{
features::{
self,
anti_afk::AntiAfk,
empty_session::EmptySession,
force_close::ForceClose,
launch::{Launch, Platform},
},
util::Countdown,
};
use eframe::egui;
use std::time::{Duration, Instant};
use sysinfo::System;
use windows::Win32::Foundation::HANDLE;
pub struct App {
pub current_frame: bool,
pub sysinfo: System,
pub game_handle: HANDLE,
pub launch: Launch,
pub force_close: ForceClose,
pub empty_session: EmptySession,
pub empty_session_countdown: Countdown,
pub anti_afk: AntiAfk,
}
impl Default for App {
fn default() -> Self {
Self {
current_frame: false,
sysinfo: System::new_all(),
game_handle: HANDLE::default(),
launch: Launch::default(),
force_close: ForceClose::default(),
empty_session_countdown: Countdown::new(
features::empty_session::INTERVAL.as_secs() as usize
),
empty_session: EmptySession::default(),
anti_afk: AntiAfk::default(),
}
}
}
impl eframe::App for App {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
catppuccin_egui::set_theme(ctx, catppuccin_egui::MOCHA);
ctx.request_repaint_after(Duration::from_millis(100));
egui::CentralPanel::default().show(ctx, |ui| {
ui.horizontal(|ui| {
ui.label("Game");
ui.add(egui::Separator::default().horizontal());
});
ui.horizontal(|ui| {
if ui.button("Launch").clicked() {
features::launch::launch(&self.launch.selected);
};
egui::ComboBox::from_label("")
.selected_text(self.launch.selected.to_string())
.width(125.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.button(&self.force_close.button_text);
if force_close_button.clicked() && !self.force_close.prompting {
self.force_close.button_text = "Are you sure?".to_string();
self.force_close.prompting = true;
self.force_close.interval = Instant::now();
self.current_frame = true;
};
if self.force_close.prompting
&& self.force_close.interval.elapsed() <= Duration::from_secs(3)
{
if force_close_button.clicked() && !self.current_frame {
features::force_close::activate(&mut self.sysinfo);
self.force_close = ForceClose::default();
}
} else {
self.force_close = ForceClose::default();
}
if self.current_frame {
self.current_frame = false;
}
ui.horizontal(|ui| {
ui.label("Session");
ui.add(egui::Separator::default().horizontal());
});
ui.add_enabled_ui(self.empty_session.enabled, |ui| {
ui.horizontal(|ui| {
if ui.button("Empty current session").clicked() {
self.empty_session.interval = Instant::now();
self.empty_session.enabled = false;
features::empty_session::activate(self);
}
if !self.empty_session.enabled {
self.empty_session_countdown.count();
} else {
self.empty_session_countdown.reset();
}
if self.empty_session.interval.elapsed() >= features::empty_session::INTERVAL {
features::empty_session::deactivate(self);
self.empty_session.enabled = true;
}
ui.label(&self.empty_session_countdown.i_string);
});
});
ui.checkbox(&mut self.anti_afk.enabled, "Anti AFK")
.on_hover_text("You should be tabbed in\nwhile this is enabled.");
if self.anti_afk.enabled
&& self.anti_afk.interval.elapsed() >= features::anti_afk::INTERVAL
{
features::anti_afk::activate();
self.anti_afk.interval = Instant::now();
}
ui.with_layout(egui::Layout::bottom_up(egui::Align::Center), |ui| {
ui.horizontal(|ui| {
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 0.0;
ui.label("with love from ");
ui.hyperlink_to("futile", "http://futile.eu");
});
ui.separator();
ui.label(format!("v{}", env!("CARGO_PKG_VERSION")));
});
// ui.separator();
});
});
}
}
fn load_icon() -> eframe::egui::IconData {
let (icon_rgba, icon_width, icon_height) = {
let icon = include_bytes!("../assets/icon.png");
let image = image::load_from_memory(icon).unwrap().into_rgba8();
let (width, height) = image.dimensions();
let rgba = image.into_raw();
(rgba, width, height)
};
eframe::egui::IconData {
rgba: icon_rgba,
width: icon_width,
height: icon_height,
}
}
pub fn run() {
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_resizable(false)
.with_maximize_button(false)
.with_inner_size([250.0, 180.0])
.with_icon(load_icon()),
centered: true,
..Default::default()
};
eframe::run_native(
"GTA Tools",
options,
Box::new(|_cc| Ok(Box::<App>::default())),
)
.unwrap();
}
+9
View File
@@ -0,0 +1,9 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
mod features;
mod gui;
mod util;
fn main() {
gui::run();
}
+40
View File
@@ -0,0 +1,40 @@
use std::time::{Duration, Instant};
pub struct Countdown {
pub i: usize,
pub i_original: usize,
pub i_string: String,
pub interval: Instant,
pub first_count: bool,
}
impl Countdown {
pub fn new(i: usize) -> Self {
Self {
i,
i_original: i,
i_string: String::new(),
interval: Instant::now(),
first_count: true,
}
}
pub fn reset(&mut self) {
*self = Self::new(self.i_original);
}
pub fn count(&mut self) {
if self.first_count {
self.interval = Instant::now();
self.first_count = false;
}
if self.interval.elapsed() >= Duration::from_secs(1) {
self.i -= 1;
self.interval = Instant::now();
}
self.i_string = self.i.to_string();
if self.i == 0 {
self.reset();
};
}
}