Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e33536dfe | ||
|
|
c36a41f919 | ||
|
|
660e7a874f | ||
|
|
72abda7352 | ||
|
|
ebf704c6f4 | ||
|
|
5c5e6d60a1 | ||
|
|
98a6545b27 | ||
|
|
a9a965462a | ||
|
|
81b4b26f96 | ||
|
|
06df466d24 | ||
|
|
b8c444dc5b | ||
|
|
e848ca8d79 | ||
|
|
0a831580e2 | ||
|
|
10e69eaac3 | ||
|
|
9dbfbc4419 | ||
|
|
ef0b6de6ba | ||
|
|
e7470bd694 | ||
|
|
936f683385 | ||
|
|
b3158f71fb | ||
|
|
070e477fd4 | ||
|
|
7745bba10e | ||
|
|
8d7a0d12b9 | ||
|
|
b325ebb03c | ||
|
|
38c6402a12 | ||
|
|
2d42c0c3c4 | ||
|
|
a423721aee |
@@ -1,2 +1,3 @@
|
|||||||
/target
|
/target
|
||||||
.vscode
|
.vscode
|
||||||
|
.idea
|
||||||
Generated
+1016
-593
File diff suppressed because it is too large
Load Diff
+17
-20
@@ -1,43 +1,40 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "gta-tools"
|
name = "gta-tools"
|
||||||
version = "0.5.4"
|
version = "0.7.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
catppuccin-egui = { version = "5.5.0", default-features = false, features = [
|
catppuccin-egui = { version = "5.6.0", default-features = false, features = [
|
||||||
"egui31",
|
"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",
|
"glow",
|
||||||
"default_fonts",
|
"default_fonts",
|
||||||
] }
|
] }
|
||||||
egui_extras = { version = "0.31.1", default-features = false, features = [
|
egui_extras = { version = "0.32.2", default-features = false, features = [
|
||||||
"image",
|
"image",
|
||||||
"svg",
|
"svg",
|
||||||
] }
|
] }
|
||||||
image = { version = "0.25.6", default-features = false, features = ["png"] }
|
image = { version = "0.25.8", default-features = false, features = ["png"] }
|
||||||
nyquest = { version = "0.1.0", features = ["blocking"] }
|
|
||||||
nyquest-backend-winrt = { version = "0.1.0", features = ["blocking"] }
|
|
||||||
open = "5.3.2"
|
open = "5.3.2"
|
||||||
semver = "1.0.26"
|
semver = "1.0.26"
|
||||||
serde = { version = "1.0.219", features = ["derive"] }
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
serde_json = "1.0.140"
|
serde_json = "1.0.143"
|
||||||
strum = { version = "0.27.1", features = ["derive"] }
|
strum = { version = "0.27.2", features = ["derive"] }
|
||||||
sysinfo = "0.35.0"
|
sysinfo = "0.37.0"
|
||||||
windows = { version = "0.61.1", features = [
|
ureq = { version = "3.1.0", features = ["json"] }
|
||||||
"Win32_UI_Input_KeyboardAndMouse",
|
windows = { version = "0.62.0", features = [
|
||||||
|
"Win32_NetworkManagement_WindowsFirewall",
|
||||||
|
"Win32_Security",
|
||||||
|
"Win32_System_Com",
|
||||||
"Win32_System_Threading",
|
"Win32_System_Threading",
|
||||||
|
"Win32_UI_Input_KeyboardAndMouse",
|
||||||
"Win32_UI_Shell",
|
"Win32_UI_Shell",
|
||||||
"Win32_UI_WindowsAndMessaging",
|
"Win32_UI_WindowsAndMessaging",
|
||||||
"Win32_Security",
|
|
||||||
"Win32_NetworkManagement_WindowsFirewall",
|
|
||||||
] }
|
] }
|
||||||
winreg = "0.55.0"
|
winreg = "0.55.0"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
static_vcruntime = "2.0.0"
|
static_vcruntime = "2.0.0"
|
||||||
winresource = "0.1.20"
|
winresource = "0.1.23"
|
||||||
|
|
||||||
[profile.release]
|
|
||||||
lto = true
|
|
||||||
codegen-units = 1
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
A toolset of convenient things for GTA V Online.
|
A toolset of convenient things for GTA V Online.
|
||||||
|
|
||||||

|
 
|
||||||
|
|
||||||
## Installing
|
## Installing
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ You will need the Rust toolchain, which can be obtained [here](https://rustup.rs
|
|||||||
|
|
||||||
## Guide
|
## 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 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.
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ It is recommended to use an up-to-date version of GTA Tools from [releases](http
|
|||||||
|
|
||||||
This section is quite simple.
|
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>[^1] 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`.
|
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 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
|
* The `Grand Theft Auto V` window is currently focused
|
||||||
* None of the designated keys are currently being pressed[^2]
|
* None of the designated keys are currently being pressed[^1]
|
||||||
* The mouse cursor is not currently visible[^3]
|
* The mouse cursor is not currently visible[^2]
|
||||||
|
|
||||||
#### Network
|
#### Network
|
||||||
|
|
||||||
@@ -56,6 +56,5 @@ 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
|
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
|
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.
|
[^1]: Otherwise, if you are, for example, using those keys to fly a plane, <kbd>Anti AFK</kbd> activating could cancel the input.
|
||||||
[^2]: 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.
|
||||||
[^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.
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ impl ForceClose {
|
|||||||
*self = Self::default();
|
*self = Self::default();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn finish_current_frame(&mut self) {
|
const fn finish_current_frame(&mut self) {
|
||||||
if self.current_frame {
|
if self.current_frame {
|
||||||
self.current_frame = false;
|
self.current_frame = false;
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-8
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::gui::settings::LaunchVersion;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{path::PathBuf, process::Command};
|
use std::{path::PathBuf, process::Command};
|
||||||
use strum::{Display, EnumIter};
|
use strum::{Display, EnumIter};
|
||||||
@@ -18,16 +19,24 @@ pub struct Launch {
|
|||||||
pub selected: Platform,
|
pub selected: Platform,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn launch(platform: &Platform) {
|
pub fn launch(platform: &Platform, version: &LaunchVersion) {
|
||||||
match platform {
|
match platform {
|
||||||
Platform::Steam => {
|
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 => {
|
Platform::Rockstar => {
|
||||||
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
|
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
|
||||||
let Ok(gta_v_enhanced) =
|
let rockstar_url = if *version == LaunchVersion::Enhanced {
|
||||||
hklm.open_subkey(r"SOFTWARE\WOW6432Node\Rockstar Games\GTAV Enhanced")
|
r"SOFTWARE\WOW6432Node\Rockstar Games\GTAV Enhanced"
|
||||||
else {
|
} else {
|
||||||
|
r"SOFTWARE\WOW6432Node\Rockstar Games\Grand Theft Auto V"
|
||||||
|
};
|
||||||
|
let Ok(gta_v_enhanced) = hklm.open_subkey(rockstar_url) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let Ok(install_folder): Result<String, std::io::Error> =
|
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();
|
let _ = Command::new(play_gtav_path).spawn();
|
||||||
}
|
}
|
||||||
Platform::Epic => {
|
Platform::Epic => {
|
||||||
let _ = open::that_detached(
|
let epic_url = if *version == LaunchVersion::Enhanced {
|
||||||
"com.epicgames.launcher://apps/331226ba7c944720baa99103cb1fe80c?action=launch&silent=true",
|
"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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-3
@@ -1,6 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
features,
|
features,
|
||||||
gui::{settings::Settings, tools, ui_ext::UiExt},
|
gui::{colours, settings::Settings, tools, ui_ext::UiExt},
|
||||||
util::{consts::game::WINDOW_TITLE, meta::Meta, persistent_state::PersistentState, win},
|
util::{consts::game::WINDOW_TITLE, meta::Meta, persistent_state::PersistentState, win},
|
||||||
};
|
};
|
||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
@@ -95,7 +95,7 @@ impl App {
|
|||||||
ui.header("Game");
|
ui.header("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, &self.settings.launch_version);
|
||||||
}
|
}
|
||||||
egui::ComboBox::from_id_salt("Launch")
|
egui::ComboBox::from_id_salt("Launch")
|
||||||
.selected_text(self.launch.selected.to_string())
|
.selected_text(self.launch.selected.to_string())
|
||||||
@@ -190,6 +190,14 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn show_settings_stage(&mut self, ctx: &egui::Context, ui: &mut egui::Ui) {
|
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| {
|
ui.horizontal(|ui| {
|
||||||
let selection = self.settings.theme;
|
let selection = self.settings.theme;
|
||||||
ui.label("Theme");
|
ui.label("Theme");
|
||||||
@@ -211,9 +219,12 @@ impl App {
|
|||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.spacing_mut().item_spacing.x = 0.0;
|
ui.spacing_mut().item_spacing.x = 0.0;
|
||||||
ui.label("with ");
|
ui.label("with ");
|
||||||
|
ui.style_mut().visuals.hyperlink_color = colours::RED;
|
||||||
ui.hyperlink_to("❤", "https://codeberg.org/futile/gta-tools");
|
ui.hyperlink_to("❤", "https://codeberg.org/futile/gta-tools");
|
||||||
ui.label(" from ");
|
ui.label(" from ");
|
||||||
ui.hyperlink_to("futile", "http://futile.eu");
|
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.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||||
ui.label(format!(
|
ui.label(format!(
|
||||||
|
|||||||
+64
-58
@@ -5,13 +5,75 @@ use crate::{
|
|||||||
ui_ext::UiExt,
|
ui_ext::UiExt,
|
||||||
},
|
},
|
||||||
util::consts::{
|
util::consts::{
|
||||||
APP_STORAGE_PATH,
|
|
||||||
game::{EXE_ENHANCED, EXE_LEGACY},
|
game::{EXE_ENHANCED, EXE_LEGACY},
|
||||||
|
path,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
|
|
||||||
impl App {
|
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(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,
|
||||||
|
};
|
||||||
|
let mut buffer = [0; 512];
|
||||||
|
let current_title = unsafe {
|
||||||
|
let hwnd = GetForegroundWindow();
|
||||||
|
let length = GetWindowTextW(hwnd, &mut buffer);
|
||||||
|
String::from_utf16_lossy(&buffer[..length as usize])
|
||||||
|
};
|
||||||
|
ui.label(format!("focused: \"{current_title}\""));
|
||||||
|
});
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label("blocked_status");
|
||||||
|
egui::ComboBox::from_id_salt("blocked_status")
|
||||||
|
.selected_text(self.game_networking.blocked_status.to_string())
|
||||||
|
.show_ui(ui, |ui| {
|
||||||
|
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!(
|
||||||
|
"timer: {}",
|
||||||
|
self.anti_afk.interval.elapsed().as_secs()
|
||||||
|
));
|
||||||
|
ui.label(format!("can activate: {}", self.anti_afk.can_activate()));
|
||||||
|
});
|
||||||
|
ui.collapsing("sysinfo", |ui| {
|
||||||
|
if ui.button("refresh all").clicked() {
|
||||||
|
self.sysinfo.refresh_all();
|
||||||
|
}
|
||||||
|
let pid = self
|
||||||
|
.sysinfo
|
||||||
|
.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(),
|
||||||
|
);
|
||||||
|
ui.label(format!("gta pid: {pid}"));
|
||||||
|
});
|
||||||
|
ui.collapsing("app state", |ui| ui.label(format!("{self:#?}")));
|
||||||
|
}
|
||||||
|
|
||||||
pub fn show_debug_viewport(&mut self, ctx: &egui::Context) {
|
pub fn show_debug_viewport(&mut self, ctx: &egui::Context) {
|
||||||
let main = ctx.input(|i| i.viewport().outer_rect.unwrap_or(egui::Rect::EVERYTHING));
|
let main = ctx.input(|i| i.viewport().outer_rect.unwrap_or(egui::Rect::EVERYTHING));
|
||||||
let builder = egui::ViewportBuilder::default()
|
let builder = egui::ViewportBuilder::default()
|
||||||
@@ -31,63 +93,7 @@ impl App {
|
|||||||
egui::CentralPanel::default().show(ctx, |ui| {
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
egui::ScrollArea::both()
|
egui::ScrollArea::both()
|
||||||
.auto_shrink([false, true])
|
.auto_shrink([false, true])
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| self.add_debug_viewport_contents(ui));
|
||||||
ui.collapsing("misc", |ui| {
|
|
||||||
if ui.button("open storage path").clicked() {
|
|
||||||
open::that_detached(APP_STORAGE_PATH.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};
|
|
||||||
let mut buffer = [0; 512];
|
|
||||||
let current_title = unsafe {
|
|
||||||
let hwnd = GetForegroundWindow();
|
|
||||||
let length = GetWindowTextW(hwnd, &mut buffer);
|
|
||||||
String::from_utf16_lossy(&buffer[..length as usize])
|
|
||||||
};
|
|
||||||
ui.label(format!("focused: \"{current_title}\""));
|
|
||||||
});
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.label("blocked_status");
|
|
||||||
egui::ComboBox::from_id_salt("blocked_status")
|
|
||||||
.selected_text(&self.game_networking.blocked_status.to_string())
|
|
||||||
.show_ui(ui, |ui| {
|
|
||||||
ui.build_menu(&mut self.game_networking.blocked_status);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
});
|
|
||||||
ui.collapsing("anti afk", |ui| {
|
|
||||||
ui.label(format!(
|
|
||||||
"timer: {}",
|
|
||||||
self.anti_afk.interval.elapsed().as_secs()
|
|
||||||
));
|
|
||||||
ui.label(format!(
|
|
||||||
"can activate: {}",
|
|
||||||
self.anti_afk.can_activate()
|
|
||||||
));
|
|
||||||
});
|
|
||||||
ui.collapsing("sysinfo", |ui| {
|
|
||||||
if ui.button("refresh all").clicked() {
|
|
||||||
self.sysinfo.refresh_all();
|
|
||||||
}
|
|
||||||
let pid = self
|
|
||||||
.sysinfo
|
|
||||||
.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(),
|
|
||||||
);
|
|
||||||
ui.label(format!("gta pid: {pid}"));
|
|
||||||
});
|
|
||||||
ui.collapsing("app state", |ui| ui.label(format!("{self:#?}")));
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
+7
-30
@@ -3,40 +3,15 @@ use crate::{
|
|||||||
app::{App, WINDOW_SIZE},
|
app::{App, WINDOW_SIZE},
|
||||||
tools,
|
tools,
|
||||||
},
|
},
|
||||||
util::{self, consts::APP_STORAGE_PATH, persistent_state::PersistentState},
|
util::{persistent_state::PersistentState, win},
|
||||||
};
|
};
|
||||||
use eframe::egui;
|
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::force_capture();
|
|
||||||
let message = format!("[{timestamp}]\n{panic_info}\nstack backtrace:\n{backtrace}\n");
|
|
||||||
file.write_all(message.as_bytes()).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn app_creator(
|
fn app_creator(
|
||||||
cc: &eframe::CreationContext<'_>,
|
cc: &eframe::CreationContext<'_>,
|
||||||
) -> Result<Box<dyn eframe::App>, Box<dyn std::error::Error + Send + Sync>> {
|
) -> 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
|
// initialize App early to modify some things before returning it
|
||||||
let mut app = Box::<App>::default();
|
let mut app = Box::new(App::default());
|
||||||
// load previously selected launch platform & settings from persistent state
|
// load previously selected launch platform & settings from persistent state
|
||||||
if let Some(persistent_state) = PersistentState::get() {
|
if let Some(persistent_state) = PersistentState::get() {
|
||||||
app.launch.selected = persistent_state.launcher;
|
app.launch.selected = persistent_state.launcher;
|
||||||
@@ -44,16 +19,18 @@ fn app_creator(
|
|||||||
}
|
}
|
||||||
// check if we're elevated. if not, and the user wants an elevated launch - relaunch elevated
|
// check if we're elevated. if not, and the user wants an elevated launch - relaunch elevated
|
||||||
if !app.flags.elevated && app.settings.start_elevated {
|
if !app.flags.elevated && app.settings.start_elevated {
|
||||||
util::win::elevate(util::win::ElevationExitMethod::Forced);
|
win::elevate(win::ElevationExitMethod::Forced);
|
||||||
}
|
}
|
||||||
// refresh sysinfo because it initializes with nothing
|
// refresh sysinfo because it initializes with nothing
|
||||||
app.sysinfo.refresh_all();
|
app.sysinfo.refresh_all();
|
||||||
// enable image loading support in egui
|
// enable image loading support in egui
|
||||||
egui_extras::install_image_loaders(&cc.egui_ctx);
|
egui_extras::install_image_loaders(&cc.egui_ctx);
|
||||||
// set our initial theme, from earlier loaded settings
|
// set our initial theme, from earlier loaded settings. we set the egui theme
|
||||||
|
// to dark here to work around system theme based switching of the egui style
|
||||||
|
cc.egui_ctx.set_theme(egui::Theme::Dark);
|
||||||
catppuccin_egui::set_theme(&cc.egui_ctx, app.settings.theme.into());
|
catppuccin_egui::set_theme(&cc.egui_ctx, app.settings.theme.into());
|
||||||
// apply some global styling that we like
|
// apply some global styling that we like
|
||||||
cc.egui_ctx.style_mut(|style| {
|
cc.egui_ctx.all_styles_mut(|style| {
|
||||||
style.spacing.item_spacing = egui::vec2(4.0, 4.0);
|
style.spacing.item_spacing = egui::vec2(4.0, 4.0);
|
||||||
style.interaction.selectable_labels = false;
|
style.interaction.selectable_labels = false;
|
||||||
});
|
});
|
||||||
|
|||||||
+21
-11
@@ -1,8 +1,19 @@
|
|||||||
|
use crate::util::win;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use strum::{Display, EnumIter};
|
use strum::{Display, EnumIter};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Display, PartialEq, Eq, Serialize, Deserialize, 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 {
|
pub enum Theme {
|
||||||
|
#[default]
|
||||||
|
#[strum(to_string = "Auto")]
|
||||||
|
Auto,
|
||||||
#[strum(to_string = "Catppuccin Latte")]
|
#[strum(to_string = "Catppuccin Latte")]
|
||||||
CatppuccinLatte,
|
CatppuccinLatte,
|
||||||
#[strum(to_string = "Catppuccin Frappe")]
|
#[strum(to_string = "Catppuccin Frappe")]
|
||||||
@@ -16,6 +27,13 @@ pub enum Theme {
|
|||||||
impl From<Theme> for catppuccin_egui::Theme {
|
impl From<Theme> for catppuccin_egui::Theme {
|
||||||
fn from(val: Theme) -> Self {
|
fn from(val: Theme) -> Self {
|
||||||
match val {
|
match val {
|
||||||
|
Theme::Auto => {
|
||||||
|
if win::is_system_theme_dark() {
|
||||||
|
catppuccin_egui::MOCHA
|
||||||
|
} else {
|
||||||
|
catppuccin_egui::LATTE
|
||||||
|
}
|
||||||
|
}
|
||||||
Theme::CatppuccinLatte => catppuccin_egui::LATTE,
|
Theme::CatppuccinLatte => catppuccin_egui::LATTE,
|
||||||
Theme::CatppuccinFrappe => catppuccin_egui::FRAPPE,
|
Theme::CatppuccinFrappe => catppuccin_egui::FRAPPE,
|
||||||
Theme::CatppuccinMacchiato => catppuccin_egui::MACCHIATO,
|
Theme::CatppuccinMacchiato => catppuccin_egui::MACCHIATO,
|
||||||
@@ -24,17 +42,9 @@ impl From<Theme> for catppuccin_egui::Theme {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||||
pub struct Settings {
|
pub struct Settings {
|
||||||
|
pub launch_version: LaunchVersion,
|
||||||
pub theme: Theme,
|
pub theme: Theme,
|
||||||
pub start_elevated: bool,
|
pub start_elevated: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Settings {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
theme: Theme::CatppuccinMocha,
|
|
||||||
start_elevated: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+15
-1
@@ -1,12 +1,26 @@
|
|||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
#[cfg(not(target_os = "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 features;
|
||||||
mod gui;
|
mod gui;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
|
fn init_storage() {
|
||||||
|
if !crate::util::consts::path::APP_STORAGE.exists() {
|
||||||
|
std::fs::create_dir(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() {
|
fn main() {
|
||||||
|
init_storage();
|
||||||
|
std::panic::set_hook(Box::new(panic_hook));
|
||||||
gui::run::run();
|
gui::run::run();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
mod codeberg;
|
mod codeberg;
|
||||||
pub mod consts;
|
pub mod consts;
|
||||||
pub mod countdown;
|
pub mod countdown;
|
||||||
|
pub mod log;
|
||||||
pub mod meta;
|
pub mod meta;
|
||||||
pub mod persistent_state;
|
pub mod persistent_state;
|
||||||
pub mod win;
|
pub mod win;
|
||||||
|
|||||||
+27
-13
@@ -1,5 +1,6 @@
|
|||||||
use nyquest::{ClientBuilder, blocking::Request};
|
|
||||||
use semver::Version;
|
use semver::Version;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
const APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
|
const APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
|
||||||
const CODEBERG_ENDPOINT_ROOT: &str = "https://codeberg.org/api/v1";
|
const CODEBERG_ENDPOINT_ROOT: &str = "https://codeberg.org/api/v1";
|
||||||
@@ -13,24 +14,37 @@ pub struct Release {
|
|||||||
impl Default for Release {
|
impl Default for Release {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
version: Version::parse(env!("CARGO_PKG_VERSION")).unwrap(),
|
version: Version::new(0, 0, 0),
|
||||||
download_url: String::new(),
|
download_url: String::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_latest_release() -> Option<Release> {
|
#[derive(Deserialize)]
|
||||||
|
struct LatestRelease {
|
||||||
|
tag_name: String,
|
||||||
|
assets: Vec<Asset>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Asset {
|
||||||
|
browser_download_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_latest_release() -> Result<Release, Box<dyn std::error::Error>> {
|
||||||
let request_url = format!("{CODEBERG_ENDPOINT_ROOT}/repos/futile/gta-tools/releases/latest");
|
let request_url = format!("{CODEBERG_ENDPOINT_ROOT}/repos/futile/gta-tools/releases/latest");
|
||||||
let client = ClientBuilder::default()
|
let mut response = ureq::get(request_url)
|
||||||
|
.config()
|
||||||
|
.timeout_global(Some(Duration::from_secs(10)))
|
||||||
.user_agent(APP_USER_AGENT)
|
.user_agent(APP_USER_AGENT)
|
||||||
.build_blocking()
|
.build()
|
||||||
.unwrap();
|
.call()?;
|
||||||
let response = client.request(Request::get(request_url)).unwrap();
|
let body = response.body_mut();
|
||||||
let json = serde_json::from_slice::<serde_json::Value>(&response.bytes().unwrap()).unwrap();
|
let json = body.read_json::<LatestRelease>()?;
|
||||||
let tag_name = json["tag_name"].as_str()?;
|
let tag_name = json.tag_name;
|
||||||
let browser_download_url = json["assets"][0]["browser_download_url"].as_str()?;
|
let browser_download_url = &json.assets[0].browser_download_url;
|
||||||
Some(Release {
|
Ok(Release {
|
||||||
version: Version::parse(tag_name).expect("expected a valid semver pattern"),
|
version: Version::parse(&tag_name)?,
|
||||||
download_url: String::from(browser_download_url),
|
download_url: browser_download_url.to_owned(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-3
@@ -1,7 +1,12 @@
|
|||||||
|
pub mod path {
|
||||||
use std::{env, path::PathBuf, sync::LazyLock};
|
use std::{env, path::PathBuf, sync::LazyLock};
|
||||||
|
pub static APP_STORAGE: LazyLock<PathBuf> = LazyLock::new(|| {
|
||||||
pub static APP_STORAGE_PATH: LazyLock<PathBuf> =
|
PathBuf::from(env::var("LOCALAPPDATA").unwrap_or_else(|_| String::from(".")))
|
||||||
LazyLock::new(|| PathBuf::from(env::var("LOCALAPPDATA").unwrap()).join("GTA Tools"));
|
.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 mod game {
|
||||||
pub const EXE_ENHANCED: &str = "GTA5_Enhanced.exe";
|
pub const EXE_ENHANCED: &str = "GTA5_Enhanced.exe";
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
use crate::util::consts::path;
|
||||||
|
use std::{fs::File, io::Write};
|
||||||
|
use strum::Display;
|
||||||
|
|
||||||
|
#[derive(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();
|
||||||
|
}
|
||||||
+6
-2
@@ -1,4 +1,4 @@
|
|||||||
use crate::util::codeberg;
|
use crate::{util::codeberg, util::log};
|
||||||
use semver::Version;
|
use semver::Version;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -11,7 +11,11 @@ pub struct Meta {
|
|||||||
impl Default for Meta {
|
impl Default for Meta {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let current_version = Version::parse(env!("CARGO_PKG_VERSION")).unwrap();
|
let current_version = Version::parse(env!("CARGO_PKG_VERSION")).unwrap();
|
||||||
let latest_release = codeberg::get_latest_release().unwrap_or_default();
|
let latest_release = codeberg::get_latest_release().unwrap_or_else(|why| {
|
||||||
|
let message = format!("failed to get latest codeberg release:\n{why}");
|
||||||
|
log::log(log::LogLevel::Error, &message);
|
||||||
|
codeberg::Release::default()
|
||||||
|
});
|
||||||
let newer_version_available = matches!(
|
let newer_version_available = matches!(
|
||||||
¤t_version.cmp_precedence(&latest_release.version),
|
¤t_version.cmp_precedence(&latest_release.version),
|
||||||
std::cmp::Ordering::Less
|
std::cmp::Ordering::Less
|
||||||
|
|||||||
@@ -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 serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
fs::{self, File},
|
fs::{self, File},
|
||||||
io::Write,
|
io::Write,
|
||||||
path::PathBuf,
|
|
||||||
sync::LazyLock,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static CONFIG_PATH: LazyLock<PathBuf> = LazyLock::new(|| APP_STORAGE_PATH.join("config.json"));
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct PersistentState {
|
pub struct PersistentState {
|
||||||
pub launcher: Platform,
|
pub launcher: Platform,
|
||||||
@@ -17,18 +13,13 @@ pub struct PersistentState {
|
|||||||
|
|
||||||
impl PersistentState {
|
impl PersistentState {
|
||||||
pub fn get() -> Option<Self> {
|
pub fn get() -> Option<Self> {
|
||||||
fs::read_to_string(CONFIG_PATH.as_path())
|
fs::read_to_string(path::APP_CONFIG.as_path())
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|config| serde_json::from_str::<Self>(&config).ok())
|
.and_then(|config| serde_json::from_str::<Self>(&config).ok())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set(&self) {
|
pub fn set(&self) {
|
||||||
let config_path = CONFIG_PATH.as_path();
|
let mut config_file = File::create(path::APP_CONFIG.as_path()).unwrap();
|
||||||
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();
|
let json = serde_json::to_string_pretty(&self).unwrap();
|
||||||
config_file.write_all(json.as_bytes()).unwrap();
|
config_file.write_all(json.as_bytes()).unwrap();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,3 +83,17 @@ pub fn is_elevated() -> bool {
|
|||||||
result.is_ok() && elevation.TokenIsElevated != 0
|
result.is_ok() && elevation.TokenIsElevated != 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_system_theme_dark() -> bool {
|
||||||
|
use winreg::RegKey;
|
||||||
|
let hkcu = RegKey::predef(winreg::enums::HKEY_CURRENT_USER);
|
||||||
|
let Ok(subkey) =
|
||||||
|
hkcu.open_subkey("Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize")
|
||||||
|
else {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
let Ok(dword): Result<u32, std::io::Error> = subkey.get_value("AppsUseLightTheme") else {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
dword != 1
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user