Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb27334f57
|
||
|
|
1e13905885
|
||
|
|
8fc73b2992
|
||
|
|
43b72995dd
|
||
|
|
d3473d46e3
|
||
|
|
1363f5b125
|
||
|
|
d15b8070ec | ||
|
|
efccd6ed50 | ||
|
|
5c73ee7bd5 | ||
|
|
445b63fa4c | ||
|
|
941868dcc8 | ||
|
|
762a3b9b26 | ||
|
|
0cb211c103 | ||
|
|
90b32c5c5a | ||
|
|
71c3c69ea2 | ||
|
|
b553e418f5 | ||
|
|
ab026c0bc6 | ||
|
|
32e21f58a1 | ||
|
|
b0c339cc5f | ||
|
|
c2584b5d01 | ||
|
|
3322f9582f | ||
|
|
5348a1d6ee | ||
|
|
05ce68cb47 | ||
|
|
f63d24ea43 | ||
|
|
356d5f17e5 | ||
|
|
bc5e80aaca | ||
|
|
e050231692 | ||
|
|
6e33536dfe | ||
|
|
c36a41f919 | ||
|
|
660e7a874f | ||
|
|
72abda7352 | ||
|
|
ebf704c6f4 | ||
|
|
5c5e6d60a1 | ||
|
|
98a6545b27 | ||
|
|
a9a965462a | ||
|
|
81b4b26f96 | ||
|
|
06df466d24 | ||
|
|
b8c444dc5b | ||
|
|
e848ca8d79 | ||
|
|
0a831580e2 | ||
|
|
10e69eaac3 | ||
|
|
9dbfbc4419 | ||
|
|
ef0b6de6ba | ||
|
|
e7470bd694 | ||
|
|
936f683385 |
@@ -0,0 +1,31 @@
|
||||
name: Create release for pushed tag
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
release-on-tag:
|
||||
runs-on: windows-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- name: Create release
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG: ${{ github.ref_name }}
|
||||
run: |
|
||||
if gh release view "$TAG" >/dev/null 2>&1; then
|
||||
echo "A release already exists for $TAG, skipping"
|
||||
else
|
||||
cargo build --release
|
||||
prev_tag=$(gh release list --limit 100 --json tagName --jq '.[0].tagName')
|
||||
notes="**Full Changelog**: https://github.com/${{ github.repository }}/compare/${prev_tag}...${TAG}"
|
||||
gh release create "$TAG" target/release/gta-tools.exe \
|
||||
--title "$TAG" \
|
||||
--notes "$notes"
|
||||
fi
|
||||
@@ -0,0 +1,43 @@
|
||||
name: Create releases for existing tags
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
release-retroactively:
|
||||
runs-on: windows-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Get tags and create release for each
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
mapfile -t tags < <(git tag -l)
|
||||
echo "${#tags[@]} tags found."
|
||||
prev_tag=""
|
||||
for tag in "${tags[@]}"; do
|
||||
if gh release view "$tag" >/dev/null 2>&1; then
|
||||
echo "A release already exists for $tag, skipping"
|
||||
else
|
||||
echo "Proceeding with tag $tag"
|
||||
git checkout "$tag"
|
||||
cargo build --release
|
||||
if [ -n "$prev_tag" ]; then
|
||||
notes="**Full Changelog**: https://github.com/${{ github.repository }}/compare/${prev_tag}...${tag}"
|
||||
else
|
||||
notes="**Full Changelog**: https://github.com/${{ github.repository }}/commits/${tag}"
|
||||
fi
|
||||
gh release create "$tag" target/release/gta-tools.exe \
|
||||
--title "$tag" \
|
||||
--notes "$notes"
|
||||
fi
|
||||
prev_tag="$tag"
|
||||
done
|
||||
@@ -1,2 +1,3 @@
|
||||
/target
|
||||
.vscode
|
||||
.idea
|
||||
Generated
+715
-632
File diff suppressed because it is too large
Load Diff
+16
-21
@@ -1,43 +1,38 @@
|
||||
[package]
|
||||
name = "gta-tools"
|
||||
version = "0.6.0"
|
||||
version = "0.9.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"
|
||||
|
||||
@@ -2,29 +2,32 @@
|
||||
|
||||
A toolset of convenient things for GTA V Online.
|
||||
|
||||

|
||||
<picture>
|
||||
<source srcset="https://i.vgy.me/M4sOHh.png" media="(prefers-color-scheme: dark)">
|
||||
<img src="https://i.vgy.me/mpO9uc.png">
|
||||
</picture>
|
||||
|
||||
## Installing
|
||||
|
||||
**Option 1** — <ins>Download</ins>
|
||||
Download the latest release [here](https://codeberg.org/futile/gta-tools/releases/download/latest/gta-tools.exe) and place it somewhere convenient for you, such as Documents. You could then make a shortcut titled "GTA Tools", and pin it to taskbar or Start.
|
||||
Download the latest release [here](https://github.com/elituf/gta-tools/releases/latest/download/gta-tools.exe) and place it somewhere convenient for you, such as Documents. You could then make a shortcut titled "GTA Tools", and pin it to taskbar or Start.
|
||||
|
||||
**Option 2** — <ins>Build from source</ins>
|
||||
You will need the Rust toolchain, which can be obtained [here](https://rustup.rs). Follow the instructions of its installer. Once you have Rust installed, clone this repo and navigate to it. At this point, you should probably `git checkout x.x.x`, where `x.x.x` is the latest tag. You can then run `cargo build --release`. Once you do that, you can use the binary located at `.\target\release\gta-tools.exe` in the same way as **Option 1**.
|
||||
|
||||
## 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://github.com/elituf/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`.
|
||||
There is also a <kbd>Force close game</kbd> feature, which simply kills all game processes. This button requires a second press after the first one for confirmation. This *does not* touch Rockstar Games Launcher or any other processes, only ones named `GTA5_Enhanced.exe` or `GTA5.exe`.
|
||||
|
||||
#### Session
|
||||
|
||||
@@ -38,8 +41,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 +59,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.
|
||||
|
||||
@@ -4,3 +4,12 @@ install:
|
||||
cargo build --release
|
||||
cp .\target\release\gta-tools.exe ~\.cargo\bin
|
||||
cp .\target\release\gta-tools.exe ~\Documents
|
||||
|
||||
lint:
|
||||
cargo clippy -- -W clippy::pedantic -W clippy::nursery -A clippy::cast_sign_loss -A clippy::cast_possible_truncation -A clippy::cast_possible_wrap
|
||||
|
||||
lint-full:
|
||||
cargo clippy -- -W clippy::pedantic -W clippy::nursery -W clippy::unwrap_used
|
||||
|
||||
lint-unwraps:
|
||||
cargo clippy -- -W clippy::unwrap_used
|
||||
@@ -42,7 +42,7 @@ impl AntiAfk {
|
||||
fn send(vk_codes: &[VIRTUAL_KEY]) {
|
||||
let mut inputs = Vec::new();
|
||||
for &vk_code in vk_codes {
|
||||
let scan_code = unsafe { MapVirtualKeyW(u32::from(vk_code.0), MAPVK_VK_TO_VSC) as u16 };
|
||||
let scan_code = unsafe { MapVirtualKeyW(u32::from(vk_code.0), MAPVK_VK_TO_VSC) } as u16;
|
||||
for event in [KEYBD_EVENT_FLAGS(0), KEYEVENTF_KEYUP] {
|
||||
let mut input = INPUT {
|
||||
r#type: INPUT_KEYBOARD,
|
||||
|
||||
@@ -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,34 +49,34 @@ 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(());
|
||||
};
|
||||
match unsafe { 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(());
|
||||
}
|
||||
unsafe {
|
||||
*game_handle = OpenProcess(PROCESS_SUSPEND_RESUME, false, pid).unwrap();
|
||||
let _ = NtSuspendProcess(*game_handle);
|
||||
}
|
||||
let _ = unsafe { NtSuspendProcess(*game_handle) };
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn deactivate(game_handle: &mut HANDLE) {
|
||||
unsafe {
|
||||
if !game_handle.is_invalid() {
|
||||
let _ = NtResumeProcess(*game_handle);
|
||||
let _ = CloseHandle(*game_handle);
|
||||
}
|
||||
let _ = unsafe { NtResumeProcess(*game_handle) };
|
||||
let _ = unsafe { CloseHandle(*game_handle) };
|
||||
}
|
||||
}
|
||||
|
||||
+19
-11
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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::{
|
||||
@@ -62,59 +64,55 @@ impl Default for GameNetworking {
|
||||
|
||||
impl Drop for GameNetworking {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
if self.com_initialized {
|
||||
CoUninitialize();
|
||||
}
|
||||
unsafe { CoUninitialize() };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
let policy: INetFwPolicy2 =
|
||||
unsafe { CoCreateInstance(&NetFwPolicy2, None, CLSCTX_INPROC_SERVER).unwrap() };
|
||||
let rules = unsafe { policy.Rules().unwrap() };
|
||||
unsafe { CoCreateInstance(&NetFwPolicy2, None, CLSCTX_INPROC_SERVER) }.unwrap();
|
||||
let rules = unsafe { policy.Rules() }.unwrap();
|
||||
let exe_path = BSTR::from(exe_path.to_string_lossy().to_string());
|
||||
for filter in [
|
||||
(FILTER_NAME_IN, NET_FW_RULE_DIR_IN),
|
||||
(FILTER_NAME_OUT, NET_FW_RULE_DIR_OUT),
|
||||
] {
|
||||
let _ = unsafe { rules.Remove(&BSTR::from(filter.0)) };
|
||||
unsafe {
|
||||
let rule: INetFwRule =
|
||||
CoCreateInstance(&NetFwRule, None, CLSCTX_INPROC_SERVER).unwrap();
|
||||
rule.SetName(&BSTR::from(filter.0)).unwrap();
|
||||
rule.SetApplicationName(&exe_path).unwrap();
|
||||
rule.SetDirection(filter.1).unwrap();
|
||||
rule.SetEnabled(true.into()).unwrap();
|
||||
rule.SetAction(NET_FW_ACTION_BLOCK).unwrap();
|
||||
rule.SetProtocol(NET_FW_IP_PROTOCOL_ANY.0).unwrap();
|
||||
rules.Add(&rule).unwrap();
|
||||
}
|
||||
unsafe { CoCreateInstance(&NetFwRule, None, CLSCTX_INPROC_SERVER) }.unwrap();
|
||||
unsafe { rule.SetName(&BSTR::from(filter.0)) }.unwrap();
|
||||
unsafe { rule.SetApplicationName(&exe_path) }.unwrap();
|
||||
unsafe { rule.SetDirection(filter.1) }.unwrap();
|
||||
unsafe { rule.SetEnabled(true.into()) }.unwrap();
|
||||
unsafe { rule.SetAction(NET_FW_ACTION_BLOCK) }.unwrap();
|
||||
unsafe { rule.SetProtocol(NET_FW_IP_PROTOCOL_ANY.0) }.unwrap();
|
||||
unsafe { rules.Add(&rule) }.unwrap();
|
||||
}
|
||||
self.blocked_status = Self::is_blocked().into();
|
||||
}
|
||||
|
||||
pub fn unblock_all(&mut self) {
|
||||
let policy: INetFwPolicy2 =
|
||||
unsafe { CoCreateInstance(&NetFwPolicy2, None, CLSCTX_INPROC_SERVER).unwrap() };
|
||||
let rules = unsafe { policy.Rules().unwrap() };
|
||||
unsafe { rules.Remove(&BSTR::from(FILTER_NAME_IN)).unwrap() };
|
||||
unsafe { rules.Remove(&BSTR::from(FILTER_NAME_OUT)).unwrap() };
|
||||
unsafe { CoCreateInstance(&NetFwPolicy2, None, CLSCTX_INPROC_SERVER) }.unwrap();
|
||||
let rules = unsafe { policy.Rules() }.unwrap();
|
||||
unsafe { rules.Remove(&BSTR::from(FILTER_NAME_IN)) }.unwrap();
|
||||
unsafe { rules.Remove(&BSTR::from(FILTER_NAME_OUT)) }.unwrap();
|
||||
self.blocked_status = Self::is_blocked().into();
|
||||
}
|
||||
|
||||
fn is_blocked() -> bool {
|
||||
let policy: INetFwPolicy2 =
|
||||
unsafe { CoCreateInstance(&NetFwPolicy2, None, CLSCTX_INPROC_SERVER).unwrap() };
|
||||
let rules = unsafe { policy.Rules().unwrap() };
|
||||
let in_rule_exists = unsafe { rules.Item(&BSTR::from(FILTER_NAME_IN)).is_ok() };
|
||||
let out_rule_exists = unsafe { rules.Item(&BSTR::from(FILTER_NAME_OUT)).is_ok() };
|
||||
unsafe { CoCreateInstance(&NetFwPolicy2, None, CLSCTX_INPROC_SERVER) }.unwrap();
|
||||
let rules = unsafe { policy.Rules() }.unwrap();
|
||||
let in_rule_exists = unsafe { rules.Item(&BSTR::from(FILTER_NAME_IN)) }.is_ok();
|
||||
let out_rule_exists = unsafe { rules.Item(&BSTR::from(FILTER_NAME_OUT)) }.is_ok();
|
||||
in_rule_exists || out_rule_exists
|
||||
}
|
||||
|
||||
@@ -133,15 +131,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())
|
||||
}
|
||||
|
||||
+21
-8
@@ -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,22 @@ 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 = match version {
|
||||
LaunchVersion::Enhanced => "steam://run/3240220",
|
||||
LaunchVersion::Legacy => "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 = match version {
|
||||
LaunchVersion::Enhanced => r"SOFTWARE\WOW6432Node\Rockstar Games\GTAV Enhanced",
|
||||
LaunchVersion::Legacy => 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 +47,15 @@ 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 = match version {
|
||||
LaunchVersion::Enhanced => {
|
||||
"com.epicgames.launcher://apps/8769e24080ea413b8ebca3f1b8c50951?action=launch&silent=true"
|
||||
}
|
||||
LaunchVersion::Legacy => {
|
||||
"com.epicgames.launcher://apps/9d2d0eb64d5c44529cece33fe2a46482?action=launch&silent=true"
|
||||
}
|
||||
};
|
||||
let _ = open::that_detached(epic_url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+40
-37
@@ -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.hyperlink_to("❤", "https://codeberg.org/futile/gta-tools");
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().visuals.hyperlink_color = colours::RED;
|
||||
ui.hyperlink_to("❤", "https://github.com/elituf/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://github.com/elituf/gta-tools/releases/latest");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
ui.add(egui::Image::new(egui::include_image!(
|
||||
|
||||
+14
-19
@@ -5,8 +5,8 @@ use crate::{
|
||||
ui_ext::UiExt,
|
||||
},
|
||||
util::consts::{
|
||||
APP_STORAGE_PATH,
|
||||
game::{EXE_ENHANCED, EXE_LEGACY},
|
||||
path,
|
||||
},
|
||||
};
|
||||
use eframe::egui;
|
||||
@@ -15,21 +15,16 @@ 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,
|
||||
};
|
||||
let current_title = {
|
||||
let mut buffer = [0; 512];
|
||||
let current_title = unsafe {
|
||||
let hwnd = GetForegroundWindow();
|
||||
let length = GetWindowTextW(hwnd, &mut buffer);
|
||||
let hwnd = unsafe { GetForegroundWindow() };
|
||||
let length = unsafe { GetWindowTextW(hwnd, &mut buffer) };
|
||||
String::from_utf16_lossy(&buffer[..length as usize])
|
||||
};
|
||||
ui.label(format!("focused: \"{current_title}\""));
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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";
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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!(
|
||||
¤t_version.cmp_precedence(&latest_release.version),
|
||||
std::cmp::Ordering::Less
|
||||
);
|
||||
Self {
|
||||
current_version,
|
||||
latest_release,
|
||||
newer_version_available,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
use std::{
|
||||
ffi::{OsStr, OsString},
|
||||
os::windows::{ffi::OsStringExt, process::CommandExt},
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
use windows::{
|
||||
Win32::System::{
|
||||
Diagnostics::ToolHelp::{
|
||||
CreateToolhelp32Snapshot, PROCESSENTRY32W, Process32FirstW, Process32NextW,
|
||||
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: OsString,
|
||||
exe: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl Process {
|
||||
pub fn pid(&self) -> u32 {
|
||||
self.pid
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &OsStr {
|
||||
&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 = PROCESSENTRY32W {
|
||||
dwSize: size_of::<PROCESSENTRY32W>() as u32,
|
||||
..Default::default()
|
||||
};
|
||||
unsafe { Process32FirstW(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: wide_array_to_os_string(&process_entry.szExeFile),
|
||||
exe: exe_full_path,
|
||||
});
|
||||
while unsafe { Process32NextW(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: wide_array_to_os_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: &PROCESSENTRY32W) -> Option<PathBuf> {
|
||||
let process_handle_result = unsafe {
|
||||
OpenProcess(
|
||||
PROCESS_QUERY_LIMITED_INFORMATION,
|
||||
false,
|
||||
process_entry.th32ProcessID,
|
||||
)
|
||||
};
|
||||
process_handle_result.map_or(None, |process_handle| {
|
||||
let mut exe_name = [0u16; 260];
|
||||
let mut dw_size = exe_name.len() as u32;
|
||||
let image_name_result = unsafe {
|
||||
QueryFullProcessImageNameW(
|
||||
process_handle,
|
||||
PROCESS_NAME_WIN32,
|
||||
PWSTR(exe_name.as_mut_ptr()),
|
||||
&raw mut dw_size,
|
||||
)
|
||||
};
|
||||
match image_name_result {
|
||||
Ok(()) => Some(PathBuf::from(wide_array_to_os_string(&exe_name))),
|
||||
Err(_) => None,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn wide_array_to_os_string(wide: &[u16]) -> OsString {
|
||||
let null_pos = wide.iter().position(|&x| x == 0).unwrap_or(wide.len());
|
||||
OsString::from_wide(&wide[..null_pos])
|
||||
}
|
||||
+11
-15
@@ -25,25 +25,21 @@ pub fn is_cursor_visible() -> bool {
|
||||
cbSize: u32::try_from(std::mem::size_of::<CURSORINFO>()).unwrap(),
|
||||
..Default::default()
|
||||
};
|
||||
unsafe {
|
||||
GetCursorInfo(&mut ci).unwrap();
|
||||
}
|
||||
unsafe { GetCursorInfo(&raw mut ci) }.unwrap();
|
||||
ci.flags == CURSOR_SHOWING
|
||||
}
|
||||
|
||||
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 hwnd = unsafe { GetForegroundWindow() };
|
||||
let length = unsafe { 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: &[VIRTUAL_KEY]) -> bool {
|
||||
keys.iter()
|
||||
.any(|&key| unsafe { (GetAsyncKeyState(i32::from(key.0)) & i16::MIN) != 0 })
|
||||
.any(|&key| unsafe { GetAsyncKeyState(i32::from(key.0)) } & i16::MIN != 0)
|
||||
}
|
||||
|
||||
pub fn elevate(closing: ElevationExitMethod) {
|
||||
@@ -66,22 +62,22 @@ 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 unsafe { OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &raw 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(
|
||||
let result = unsafe {
|
||||
GetTokenInformation(
|
||||
token,
|
||||
TokenElevation,
|
||||
Some((&raw mut elevation).cast()),
|
||||
size,
|
||||
&mut size,
|
||||
);
|
||||
CloseHandle(token).unwrap();
|
||||
&raw mut size,
|
||||
)
|
||||
};
|
||||
unsafe { CloseHandle(token) }.unwrap();
|
||||
result.is_ok() && elevation.TokenIsElevated != 0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_system_theme_dark() -> bool {
|
||||
|
||||
Reference in New Issue
Block a user