Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aeb99992bb
|
||
|
|
8ab6e14560
|
||
|
|
b9ebdfdb5c
|
||
|
|
2804812691
|
||
|
|
606fb7e191
|
||
|
|
e2a779b7f5
|
||
|
|
a8b04bb72b
|
||
|
|
c34cf187d5
|
||
|
|
f07421e75a
|
||
|
|
8b4a455dc1
|
||
|
|
a1300f8d76
|
||
|
|
9e623219a5
|
||
|
|
09e19b6e16
|
||
|
|
4498c45965
|
||
|
|
af8a5b1746
|
||
|
|
560d9659f4
|
||
|
|
bf086d0442
|
||
|
|
a6ea8ad8e2
|
||
|
|
4c00d1846b
|
||
|
|
e5e335f4f3
|
||
|
|
3ab5987b67
|
||
|
|
2030796337
|
||
|
|
2edb4f492a
|
||
|
|
114292d912
|
||
|
|
8adc1f49ad
|
||
|
|
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 | ||
|
|
b3158f71fb | ||
|
|
070e477fd4 | ||
|
|
7745bba10e | ||
|
|
8d7a0d12b9 | ||
|
|
b325ebb03c | ||
|
|
38c6402a12 | ||
|
|
2d42c0c3c4 | ||
|
|
a423721aee | ||
|
|
f3436c8ddc | ||
|
|
afcf0a87c0 | ||
|
|
73d610949f | ||
|
|
7f358d9adb | ||
|
|
7caa5421f7 | ||
|
|
ecac0d54aa | ||
|
|
a84401ca33 | ||
|
|
1bcd6b2c7d | ||
|
|
1dbdc0e4cb | ||
|
|
3610e44a08 | ||
|
|
f5fd22d35c | ||
|
|
022828b802 | ||
|
|
262d2d0720 | ||
|
|
2326edf97b | ||
|
|
301be82837 | ||
|
|
0d7dde929d | ||
|
|
3ab802fd56 | ||
|
|
53030cc5f7 | ||
|
|
383224c2fa | ||
|
|
575a6a4f1d | ||
|
|
17579de277 | ||
|
|
00727b9913 | ||
|
|
1f6fc22525 | ||
|
|
27b09be76e | ||
|
|
6f15e8c348 | ||
|
|
d294aa9861 | ||
|
|
f688f1c3c4 | ||
|
|
58ec9f6567 | ||
|
|
1c9ce2a4d3 | ||
|
|
8a5d6cf38c | ||
|
|
d789b0f62d | ||
|
|
6079a72f47 | ||
|
|
914ffe46d2 | ||
|
|
935703e1ab | ||
|
|
202554e03b | ||
|
|
da2b15a0b6 | ||
|
|
ccc27b1944 | ||
|
|
10168e84d1 | ||
|
|
e72964a9f8 | ||
|
|
18052d0c78 | ||
|
|
09f5a3895b | ||
|
|
89892ef592 | ||
|
|
61de3aeee3 | ||
|
|
4ed00e6ac3 | ||
|
|
332db0ea03 | ||
|
|
64d13e3964 | ||
|
|
feb3a793d9 | ||
|
|
585f454bc0 | ||
|
|
27524e855a | ||
|
|
0257834d04 | ||
|
|
64e94bf790 | ||
|
|
668a6eb38b | ||
|
|
0de79769d2 | ||
|
|
b79c5f62b3 |
@@ -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
|
/target
|
||||||
.vscode
|
.vscode
|
||||||
|
.idea
|
||||||
Generated
+758
-2238
File diff suppressed because it is too large
Load Diff
+26
-22
@@ -1,38 +1,42 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "gta-tools"
|
name = "gta-tools"
|
||||||
version = "0.4.2"
|
version = "0.10.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
license = "Apache-2.0"
|
||||||
|
authors = ["futile <git@futile.eu>"]
|
||||||
|
description = "A toolset of convenient things for GTA V Online."
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
catppuccin-egui = { version = "5.5.0", default-features = false, features = [
|
catppuccin-egui = { version = "5.6.0", default-features = false, features = [
|
||||||
"egui31",
|
"egui32",
|
||||||
] }
|
] }
|
||||||
chrono = "0.4.40"
|
eframe = { version = "0.32.2", default-features = false, features = [
|
||||||
dirs = "6.0.0"
|
"glow",
|
||||||
eframe = "0.31.1"
|
"default_fonts",
|
||||||
egui_extras = { version = "0.31.1", features = ["image"] }
|
] }
|
||||||
image = { version = "0.25.6", default-features = false, features = ["png"] }
|
egui_extras = { version = "0.32.2", default-features = false, features = [
|
||||||
|
"image",
|
||||||
|
"svg",
|
||||||
|
] }
|
||||||
|
humantime = "2.3.0"
|
||||||
|
image = { version = "0.25.8", default-features = false, features = ["png"] }
|
||||||
|
log = "0.4.28"
|
||||||
open = "5.3.2"
|
open = "5.3.2"
|
||||||
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 = { git = "https://github.com/GuillaumeGomez/sysinfo", rev = "bcbb7b2b389b8800ff457773f48b130e710458fc" }
|
windows = { version = "0.62.0", features = [
|
||||||
ureq = { version = "3.0.11", features = ["json"] }
|
"Win32_NetworkManagement_WindowsFirewall",
|
||||||
windows = { version = "0.61.1", features = [
|
"Win32_Security",
|
||||||
"Win32_UI_Input_KeyboardAndMouse",
|
"Win32_System_Com",
|
||||||
|
"Win32_System_Diagnostics_ToolHelp",
|
||||||
"Win32_System_Threading",
|
"Win32_System_Threading",
|
||||||
|
"Win32_UI_Input_KeyboardAndMouse",
|
||||||
"Win32_UI_Shell",
|
"Win32_UI_Shell",
|
||||||
"Win32_UI_WindowsAndMessaging",
|
"Win32_UI_WindowsAndMessaging",
|
||||||
"Win32_Security",
|
|
||||||
] }
|
] }
|
||||||
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"
|
tauri-winres = "0.3.3"
|
||||||
|
|
||||||
[profile.release]
|
|
||||||
strip = true
|
|
||||||
lto = true
|
|
||||||
codegen-units = 1
|
|
||||||
|
|||||||
@@ -1,4 +1,69 @@
|
|||||||
# GTA Tools
|
# GTA Tools
|
||||||
A toolset of convenient things for GTA 5 Online.
|
|
||||||
|
|
||||||

|
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://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
|
||||||
|
|
||||||
|
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 always use an up-to-date version of GTA Tools from [releases](https://github.com/elituf/gta-tools/releases). You can also easily access this repository by going to the <kbd>About</kbd> page of GTA Tools and clicking the GitHub button beside the version number.
|
||||||
|
|
||||||
|
#### Game
|
||||||
|
|
||||||
|
This section is quite simple.
|
||||||
|
|
||||||
|
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 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
|
||||||
|
|
||||||
|
This section also has two features.
|
||||||
|
|
||||||
|
The first being <kbd>Empty current session</kbd>, which "removes everyone" from your session by suspending your game for 10 seconds, and then resuming it. This method is exceedingly similar to the Resource Monitor method, but made convenient in one button. This can be useful in numerous ways:
|
||||||
|
* You are stuck loading into a session
|
||||||
|
* You are stuck loading into an interior
|
||||||
|
* You are about to get griefed while doing a freemode mission
|
||||||
|
* You want to quickly get a "new" session
|
||||||
|
|
||||||
|
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[^1]
|
||||||
|
* The mouse cursor is not currently visible[^2]
|
||||||
|
|
||||||
|
#### Network
|
||||||
|
|
||||||
|
The last section has a single feature, <kbd>Game's network access</kbd>, with two buttons, <kbd>Block</kbd> and <kbd>Unblock</kbd>, and a coloured indicator dot. This feature requires administrator, and blocks/unblocks GTA V's entire internet access using the Windows Firewall. The coloured indicator dot has these meanings:
|
||||||
|
* **Green** - the game is connected to the internet
|
||||||
|
* **Yellow** - the game could not be found running, and thus was not blocked
|
||||||
|
* **Red** - the game is blocked from the internet
|
||||||
|
|
||||||
|
This feature is primarily useful for **replay glitching**, which is an exploit that allows you to complete any heist/mission finale, get the money for it, and then be able to do it again right away. Here's how you can do a replay glitch:
|
||||||
|
1. Get to the finale of a heist/contract/mission series, such as **The Contract: Dr. Dre**
|
||||||
|
2. Complete the finale as normal
|
||||||
|
3. Right *after* the **HEIST/MISSION PASSED** screen you get when you complete a finale (basically, when your cut is being shown), <kbd>Block</kbd> the game's internet access
|
||||||
|
4. After a few seconds, you will be shown an error screen and kicked to the main menu
|
||||||
|
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
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="512"
|
||||||
|
height="512"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<g
|
||||||
|
id="layer1">
|
||||||
|
<circle
|
||||||
|
style="fill:#ffffff"
|
||||||
|
id="path1"
|
||||||
|
cx="256"
|
||||||
|
cy="256"
|
||||||
|
r="256" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 458 B |
@@ -1,7 +1,23 @@
|
|||||||
fn main() {
|
fn main() {
|
||||||
static_vcruntime::metabuild();
|
static_vcruntime::metabuild();
|
||||||
winresource::WindowsResource::new()
|
tauri_winres::WindowsResource::new()
|
||||||
|
.set("FileDescription", "GTA Tools")
|
||||||
|
.set("ProductName", "GTA Tools")
|
||||||
|
.set("LegalCopyright", "futile <git@futile.eu>")
|
||||||
|
.set_language(0x0009)
|
||||||
.set_icon("assets/icon.ico")
|
.set_icon("assets/icon.ico")
|
||||||
.compile()
|
.compile()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
embed_latest_git_hash();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn embed_latest_git_hash() {
|
||||||
|
let git_rev_parse = std::process::Command::new("git")
|
||||||
|
.args(["rev-parse", "--short=8", "HEAD"])
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
let git_hash = String::from_utf8(git_rev_parse.stdout).unwrap();
|
||||||
|
println!("cargo::rustc-env=LATEST_GIT_COMMIT_HASH={git_hash}");
|
||||||
|
println!("cargo::rerun-if-changed=.git/refs/heads/main");
|
||||||
|
println!("cargo::rerun-if-changed=.git/HEAD");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
set windows-shell := ["pwsh", "-NoLogo", "-NoProfileLoadTime", "-Command"]
|
|
||||||
|
|
||||||
install:
|
|
||||||
cargo build --release
|
|
||||||
cp .\target\release\gta-tools.exe ~\.cargo\bin
|
|
||||||
cp .\target\release\gta-tools.exe ~\Documents
|
|
||||||
+35
-29
@@ -1,13 +1,12 @@
|
|||||||
#![allow(clippy::cast_possible_truncation)]
|
use crate::util::{self, consts::game::WINDOW_TITLE};
|
||||||
|
|
||||||
use crate::util::{self, consts::GTA_WINDOW_TITLE};
|
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use windows::Win32::UI::Input::KeyboardAndMouse::{
|
use windows::Win32::UI::Input::KeyboardAndMouse::{
|
||||||
KEYBD_EVENT_FLAGS, MAP_VIRTUAL_KEY_TYPE, MapVirtualKeyW, VK_NUMPAD4, VK_NUMPAD6, keybd_event,
|
INPUT, INPUT_KEYBOARD, KEYBD_EVENT_FLAGS, KEYBDINPUT, KEYEVENTF_KEYUP, MAPVK_VK_TO_VSC,
|
||||||
|
MapVirtualKeyW, SendInput, VIRTUAL_KEY, VK_NUMPAD4, VK_NUMPAD6,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const INTERVAL: Duration = Duration::from_secs(60);
|
const INTERVAL: Duration = Duration::from_secs(60);
|
||||||
const PRESS_KEYS: [u8; 2] = [VK_NUMPAD4.0 as u8, VK_NUMPAD6.0 as u8];
|
const PRESS_KEYS: [VIRTUAL_KEY; 2] = [VK_NUMPAD4, VK_NUMPAD6];
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct AntiAfk {
|
pub struct AntiAfk {
|
||||||
@@ -25,32 +24,39 @@ impl Default for AntiAfk {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AntiAfk {
|
impl AntiAfk {
|
||||||
|
pub fn can_activate(&self) -> bool {
|
||||||
|
use util::win::{is_any_key_pressed, is_cursor_visible, is_window_focused};
|
||||||
|
is_window_focused(WINDOW_TITLE) && !is_any_key_pressed(&PRESS_KEYS) && !is_cursor_visible()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn should_activate(&self) -> bool {
|
||||||
|
self.enabled && self.interval.elapsed() >= INTERVAL
|
||||||
|
}
|
||||||
|
|
||||||
pub fn activate(&mut self) {
|
pub fn activate(&mut self) {
|
||||||
if can_activate() {
|
send(&PRESS_KEYS);
|
||||||
send(&PRESS_KEYS);
|
|
||||||
}
|
|
||||||
self.interval = Instant::now();
|
self.interval = Instant::now();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn can_activate() -> bool {
|
fn send(vk_codes: &[VIRTUAL_KEY]) {
|
||||||
use util::win::{is_any_key_pressed, is_cursor_visible, is_window_focused};
|
let mut inputs = Vec::new();
|
||||||
is_window_focused(GTA_WINDOW_TITLE) && !is_any_key_pressed(&PRESS_KEYS) && !is_cursor_visible()
|
for &vk_code in vk_codes {
|
||||||
}
|
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] {
|
||||||
pub fn send(vk_codes: &[u8]) {
|
let mut input = INPUT {
|
||||||
vk_codes.iter().for_each(|vk_code: &u8| unsafe {
|
r#type: INPUT_KEYBOARD,
|
||||||
keybd_event(
|
..Default::default()
|
||||||
*vk_code,
|
};
|
||||||
u8::try_from(MapVirtualKeyW(u32::from(*vk_code), MAP_VIRTUAL_KEY_TYPE(0))).unwrap(),
|
input.Anonymous.ki = KEYBDINPUT {
|
||||||
KEYBD_EVENT_FLAGS(0),
|
wVk: vk_code,
|
||||||
0,
|
wScan: scan_code,
|
||||||
);
|
dwFlags: event,
|
||||||
keybd_event(
|
time: 0,
|
||||||
*vk_code,
|
dwExtraInfo: 0,
|
||||||
u8::try_from(MapVirtualKeyW(u32::from(*vk_code), MAP_VIRTUAL_KEY_TYPE(0))).unwrap(),
|
};
|
||||||
KEYBD_EVENT_FLAGS(2),
|
inputs.push(input);
|
||||||
0,
|
}
|
||||||
);
|
}
|
||||||
});
|
unsafe { SendInput(&inputs, size_of::<INPUT>() as i32) };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
use crate::util::{
|
use crate::util::{
|
||||||
consts::{ENHANCED, LEGACY},
|
consts::game::{EXE_ENHANCED, EXE_LEGACY},
|
||||||
countdown::Countdown,
|
countdown::Countdown,
|
||||||
|
system_info::SystemInfo,
|
||||||
};
|
};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use sysinfo::System;
|
|
||||||
use windows::Win32::{
|
use windows::Win32::{
|
||||||
Foundation::{CloseHandle, HANDLE, NTSTATUS},
|
Foundation::{HANDLE, NTSTATUS},
|
||||||
System::Threading::{OpenProcess, PROCESS_SUSPEND_RESUME},
|
System::Threading::{OpenProcess, PROCESS_SUSPEND_RESUME},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const INTERVAL: Duration = Duration::from_secs(10);
|
const INTERVAL: Duration = Duration::from_secs(10);
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct EmptySession {
|
pub struct EmptySession {
|
||||||
@@ -28,40 +28,53 @@ impl Default for EmptySession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[link(name = "ntdll")]
|
impl EmptySession {
|
||||||
unsafe extern "system" {
|
pub fn run_timers(&mut self, game_handle: &mut HANDLE) {
|
||||||
pub unsafe fn NtSuspendProcess(ProcessHandle: HANDLE) -> NTSTATUS;
|
if self.disabled {
|
||||||
pub unsafe fn NtResumeProcess(ProcessHandle: HANDLE) -> NTSTATUS;
|
self.countdown.count();
|
||||||
}
|
} else {
|
||||||
|
self.countdown.reset();
|
||||||
fn get_gta_pid(sysinfo: &mut System) -> u32 {
|
}
|
||||||
sysinfo.refresh_all();
|
if self.interval.elapsed() >= INTERVAL {
|
||||||
if let Some((pid, _)) = sysinfo
|
deactivate(game_handle);
|
||||||
.processes()
|
self.disabled = false;
|
||||||
.iter()
|
|
||||||
.find(|(_, p)| p.name() == ENHANCED || p.name() == LEGACY)
|
|
||||||
{
|
|
||||||
return pid.as_u32();
|
|
||||||
}
|
|
||||||
u32::MAX
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn activate(handle: &mut HANDLE, sysinfo: &mut System) {
|
|
||||||
let pid = get_gta_pid(sysinfo);
|
|
||||||
if pid == u32::MAX {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
unsafe {
|
|
||||||
*handle = OpenProcess(PROCESS_SUSPEND_RESUME, false, pid).unwrap();
|
|
||||||
let _ = NtSuspendProcess(*handle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deactivate(handle: &mut HANDLE) {
|
|
||||||
unsafe {
|
|
||||||
if !handle.is_invalid() {
|
|
||||||
let _ = NtResumeProcess(*handle);
|
|
||||||
let _ = CloseHandle(*handle);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[link(name = "ntdll")]
|
||||||
|
unsafe extern "system" {
|
||||||
|
unsafe fn NtSuspendProcess(ProcessHandle: HANDLE) -> NTSTATUS;
|
||||||
|
unsafe fn NtResumeProcess(ProcessHandle: HANDLE) -> NTSTATUS;
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
.map(|p| p.pid())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn activate(game_handle: &mut HANDLE, system_info: &mut SystemInfo) -> bool {
|
||||||
|
let Some(pid) = get_gta_pid(system_info) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
match unsafe { OpenProcess(PROCESS_SUSPEND_RESUME, false, pid) } {
|
||||||
|
Ok(handle) => *game_handle = handle,
|
||||||
|
Err(why) => {
|
||||||
|
log::error!("failed to suspend game for empty session:\n{why}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unsafe { NtSuspendProcess(*game_handle) }.unwrap();
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deactivate(game_handle: &mut HANDLE) {
|
||||||
|
if !game_handle.is_invalid() {
|
||||||
|
// ignoring the return because this function behaves very weirdly
|
||||||
|
let _ = unsafe { NtResumeProcess(*game_handle) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+46
-18
@@ -1,40 +1,68 @@
|
|||||||
use crate::util::consts::{ENHANCED, LEGACY};
|
use crate::util::{
|
||||||
use std::time::Instant;
|
consts::game::{EXE_ENHANCED, EXE_LEGACY},
|
||||||
use sysinfo::System;
|
system_info::SystemInfo,
|
||||||
|
};
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
const INTERVAL: Duration = Duration::from_secs(3);
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ForceClose {
|
pub struct ForceClose {
|
||||||
pub button_text: String,
|
pub button_text: String,
|
||||||
pub prompting: bool,
|
timer: Instant,
|
||||||
pub interval: Instant,
|
counting: bool,
|
||||||
|
current_frame: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ForceClose {
|
impl Default for ForceClose {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
button_text: "Force close game".to_owned(),
|
button_text: "Force close game".to_owned(),
|
||||||
prompting: false,
|
timer: Instant::now(),
|
||||||
interval: Instant::now(),
|
counting: false,
|
||||||
|
current_frame: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ForceClose {
|
impl ForceClose {
|
||||||
pub fn prompting(&mut self) {
|
pub fn prompt(&mut self, force_close_button_clicked: bool, system_info: &mut SystemInfo) {
|
||||||
self.button_text = "Are you sure?".to_owned();
|
if force_close_button_clicked && !self.counting {
|
||||||
self.prompting = true;
|
self.button_text = "Are you sure?".to_owned();
|
||||||
self.interval = Instant::now();
|
self.timer = Instant::now();
|
||||||
|
self.counting = true;
|
||||||
|
self.current_frame = true;
|
||||||
|
}
|
||||||
|
if self.counting && self.timer.elapsed() >= INTERVAL {
|
||||||
|
self.reset();
|
||||||
|
} else if force_close_button_clicked && !self.current_frame {
|
||||||
|
activate(system_info);
|
||||||
|
self.reset();
|
||||||
|
}
|
||||||
|
self.finish_current_frame();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
*self = Self::default();
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn finish_current_frame(&mut self) {
|
||||||
|
if self.current_frame {
|
||||||
|
self.current_frame = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn activate(sysinfo: &mut System) {
|
fn activate(system_info: &mut SystemInfo) {
|
||||||
sysinfo.refresh_all();
|
system_info.refresh();
|
||||||
sysinfo
|
system_info
|
||||||
.processes()
|
.processes()
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(_, p)| p.name() == ENHANCED || p.name() == LEGACY)
|
.filter(|p| p.name() == EXE_ENHANCED || p.name() == EXE_LEGACY)
|
||||||
.for_each(|(_, p)| {
|
.for_each(|p| {
|
||||||
p.kill();
|
if !p.kill() {
|
||||||
|
log::error!("failed to force close game, probably due to access denied");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
sysinfo.refresh_all();
|
system_info.refresh();
|
||||||
}
|
}
|
||||||
|
|||||||
+132
-63
@@ -1,72 +1,141 @@
|
|||||||
#![allow(clippy::zombie_processes)]
|
use crate::util::{
|
||||||
|
consts::game::{EXE_ENHANCED, EXE_LEGACY},
|
||||||
|
system_info::SystemInfo,
|
||||||
|
};
|
||||||
|
use std::{
|
||||||
|
path::Path,
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
use strum::{Display, EnumIter};
|
||||||
|
use windows::{
|
||||||
|
Win32::{
|
||||||
|
NetworkManagement::WindowsFirewall::{
|
||||||
|
INetFwPolicy2, INetFwRule, NET_FW_ACTION_BLOCK, NET_FW_IP_PROTOCOL_ANY,
|
||||||
|
NET_FW_RULE_DIR_IN, NET_FW_RULE_DIR_OUT, NetFwPolicy2, NetFwRule,
|
||||||
|
},
|
||||||
|
System::Com::{
|
||||||
|
CLSCTX_INPROC_SERVER, COINIT_MULTITHREADED, CoCreateInstance, CoInitializeEx,
|
||||||
|
CoUninitialize,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
core::BSTR,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::util::consts::{ENHANCED, LEGACY};
|
const FILTER_NAME_IN: &str = "[GTA Tools] Block all inbound traffic for GTA V";
|
||||||
use std::{os::windows::process::CommandExt, path::Path, process::Command};
|
const FILTER_NAME_OUT: &str = "[GTA Tools] Block all outbound traffic for GTA V";
|
||||||
use sysinfo::System;
|
|
||||||
use windows::Win32::System::Threading::CREATE_NO_WINDOW;
|
|
||||||
|
|
||||||
const FILTER_NAME: &str = "[GTA Tools] Block all traffic for GTA V";
|
const INTERVAL: Duration = Duration::from_secs(3);
|
||||||
|
|
||||||
fn get_game_exe_path(sysinfo: &mut System) -> Option<&Path> {
|
#[derive(Clone, Copy, Debug, Display, PartialEq, Eq, EnumIter)]
|
||||||
sysinfo.refresh_all();
|
pub enum BlockedStatus {
|
||||||
if let Some((_, process)) = sysinfo
|
Blocked,
|
||||||
.processes()
|
Failed,
|
||||||
.iter()
|
Unblocked,
|
||||||
.find(|(_, p)| p.name() == ENHANCED || p.name() == LEGACY)
|
}
|
||||||
{
|
|
||||||
process.exe()
|
impl From<bool> for BlockedStatus {
|
||||||
} else {
|
fn from(value: bool) -> Self {
|
||||||
None
|
if value {
|
||||||
|
Self::Blocked
|
||||||
|
} else {
|
||||||
|
Self::Unblocked
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn block_all(sysinfo: &mut System) {
|
#[derive(Debug)]
|
||||||
let Some(exe_path) = get_game_exe_path(sysinfo) else {
|
pub struct GameNetworking {
|
||||||
return;
|
com_initialized: bool,
|
||||||
};
|
pub blocked_status: BlockedStatus,
|
||||||
let exe_path = exe_path.display();
|
timer: Instant,
|
||||||
Command::new("netsh")
|
counting: bool,
|
||||||
.args([
|
|
||||||
"advfirewall",
|
|
||||||
"firewall",
|
|
||||||
"add",
|
|
||||||
"rule",
|
|
||||||
&format!("name={FILTER_NAME}"),
|
|
||||||
"dir=out",
|
|
||||||
"action=block",
|
|
||||||
"protocol=ANY",
|
|
||||||
&format!("program={exe_path}"),
|
|
||||||
])
|
|
||||||
.creation_flags(CREATE_NO_WINDOW.0)
|
|
||||||
.spawn()
|
|
||||||
.unwrap();
|
|
||||||
Command::new("netsh")
|
|
||||||
.args([
|
|
||||||
"advfirewall",
|
|
||||||
"firewall",
|
|
||||||
"add",
|
|
||||||
"rule",
|
|
||||||
&format!("name={FILTER_NAME}"),
|
|
||||||
"dir=in",
|
|
||||||
"action=block",
|
|
||||||
"protocol=ANY",
|
|
||||||
&format!("program={exe_path}"),
|
|
||||||
])
|
|
||||||
.creation_flags(CREATE_NO_WINDOW.0)
|
|
||||||
.spawn()
|
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn unblock_all() {
|
impl Default for GameNetworking {
|
||||||
Command::new("netsh")
|
fn default() -> Self {
|
||||||
.args([
|
Self {
|
||||||
"advfirewall",
|
blocked_status: Self::is_blocked().into(),
|
||||||
"firewall",
|
com_initialized: unsafe { CoInitializeEx(None, COINIT_MULTITHREADED) }.is_ok(),
|
||||||
"delete",
|
timer: Instant::now(),
|
||||||
"rule",
|
counting: false,
|
||||||
&format!("name={FILTER_NAME}"),
|
}
|
||||||
])
|
}
|
||||||
.creation_flags(CREATE_NO_WINDOW.0)
|
}
|
||||||
.spawn()
|
|
||||||
.unwrap();
|
impl Drop for GameNetworking {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if self.com_initialized {
|
||||||
|
unsafe { CoUninitialize() };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GameNetworking {
|
||||||
|
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();
|
||||||
|
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),
|
||||||
|
] {
|
||||||
|
unsafe { rules.Remove(&BSTR::from(filter.0)) }.unwrap();
|
||||||
|
let rule: INetFwRule =
|
||||||
|
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();
|
||||||
|
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();
|
||||||
|
in_rule_exists || out_rule_exists
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn if_failed_return_to_boolean(&mut self) {
|
||||||
|
if self.blocked_status == BlockedStatus::Failed && !self.counting {
|
||||||
|
self.counting = true;
|
||||||
|
self.timer = Instant::now();
|
||||||
|
}
|
||||||
|
if self.blocked_status == BlockedStatus::Failed
|
||||||
|
&& self.counting
|
||||||
|
&& self.timer.elapsed() >= INTERVAL
|
||||||
|
{
|
||||||
|
self.counting = false;
|
||||||
|
self.blocked_status = Self::is_blocked().into();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
.and_then(|p| p.exe())
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-22
@@ -1,42 +1,40 @@
|
|||||||
|
use crate::gui::settings::LaunchVersion;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{fmt::Display, path::PathBuf, process::Command};
|
use std::{path::PathBuf, process::Command};
|
||||||
use strum::EnumIter;
|
use strum::{Display, EnumIter};
|
||||||
use winreg::{RegKey, enums::HKEY_LOCAL_MACHINE};
|
use winreg::{RegKey, enums::HKEY_LOCAL_MACHINE};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Default, Debug, PartialEq, Eq, Serialize, Deserialize, EnumIter)]
|
#[derive(Clone, Copy, Default, Debug, Display, PartialEq, Eq, Serialize, Deserialize, EnumIter)]
|
||||||
pub enum Platform {
|
pub enum Platform {
|
||||||
#[default]
|
#[default]
|
||||||
Steam,
|
Steam,
|
||||||
|
#[strum(to_string = "Rockstar Games")]
|
||||||
Rockstar,
|
Rockstar,
|
||||||
|
#[strum(to_string = "Epic Games")]
|
||||||
Epic,
|
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(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct Launch {
|
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 = match version {
|
||||||
|
LaunchVersion::Enhanced => "steam://run/3240220",
|
||||||
|
LaunchVersion::Legacy => "steam://run/271590",
|
||||||
|
};
|
||||||
|
open::that_detached(steam_url).unwrap();
|
||||||
}
|
}
|
||||||
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 = match version {
|
||||||
hklm.open_subkey(r"SOFTWARE\WOW6432Node\Rockstar Games\GTAV Enhanced")
|
LaunchVersion::Enhanced => r"SOFTWARE\WOW6432Node\Rockstar Games\GTAV Enhanced",
|
||||||
else {
|
LaunchVersion::Legacy => 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> =
|
||||||
@@ -46,12 +44,19 @@ pub fn launch(platform: &Platform) {
|
|||||||
};
|
};
|
||||||
let mut play_gtav_path = PathBuf::from(install_folder);
|
let mut play_gtav_path = PathBuf::from(install_folder);
|
||||||
play_gtav_path.push("PlayGTAV.exe");
|
play_gtav_path.push("PlayGTAV.exe");
|
||||||
|
// ignoring the return because if it errors that means GTA isn't installed via Rockstar
|
||||||
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 = match version {
|
||||||
"com.epicgames.launcher://apps/331226ba7c944720baa99103cb1fe80c?action=launch&silent=true",
|
LaunchVersion::Enhanced => {
|
||||||
);
|
"com.epicgames.launcher://apps/8769e24080ea413b8ebca3f1b8c50951?action=launch&silent=true"
|
||||||
|
}
|
||||||
|
LaunchVersion::Legacy => {
|
||||||
|
"com.epicgames.launcher://apps/9d2d0eb64d5c44529cece33fe2a46482?action=launch&silent=true"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
open::that_detached(epic_url).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
mod app;
|
pub mod app;
|
||||||
|
mod debug;
|
||||||
pub mod run;
|
pub mod run;
|
||||||
pub mod settings;
|
pub mod settings;
|
||||||
mod tools;
|
mod tools;
|
||||||
|
mod ui_ext;
|
||||||
+124
-204
@@ -1,92 +1,74 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
features::{
|
features,
|
||||||
self, anti_afk::AntiAfk, empty_session::EmptySession, force_close::ForceClose,
|
gui::{settings::Settings, tools, ui_ext::UiExt},
|
||||||
launch::Launch,
|
|
||||||
},
|
|
||||||
gui::{
|
|
||||||
settings::{self, Settings},
|
|
||||||
tools,
|
|
||||||
},
|
|
||||||
util::{
|
util::{
|
||||||
self,
|
consts::{colours, game::WINDOW_TITLE},
|
||||||
consts::{APP_STORAGE_PATH, ENHANCED, GTA_WINDOW_TITLE, LEGACY},
|
|
||||||
meta::Meta,
|
|
||||||
persistent_state::PersistentState,
|
persistent_state::PersistentState,
|
||||||
|
system_info::SystemInfo,
|
||||||
|
win,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
use strum::{Display, EnumIter};
|
||||||
|
|
||||||
pub const WINDOW_SIZE: [f32; 2] = [240.0, 240.0];
|
pub const WINDOW_SIZE: [f32; 2] = [240.0, 240.0];
|
||||||
|
|
||||||
#[derive(Debug, Default, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, Default, Display, PartialEq, Eq, EnumIter)]
|
||||||
pub enum Stage {
|
enum Stage {
|
||||||
#[default]
|
#[default]
|
||||||
Main,
|
Main,
|
||||||
Settings,
|
Settings,
|
||||||
About,
|
About,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::struct_excessive_bools)]
|
#[derive(Debug)]
|
||||||
#[derive(Debug, Default)]
|
|
||||||
pub struct Flags {
|
pub struct Flags {
|
||||||
pub elevated: bool,
|
pub elevated: bool,
|
||||||
debug: bool,
|
pub debug: bool,
|
||||||
closing: bool,
|
closing: bool,
|
||||||
current_frame: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
impl Default for Flags {
|
||||||
pub struct App {
|
|
||||||
meta: Meta,
|
|
||||||
pub settings: Settings,
|
|
||||||
stage: Stage,
|
|
||||||
pub flags: Flags,
|
|
||||||
pub sysinfo: sysinfo::System,
|
|
||||||
pub game_handle: windows::Win32::Foundation::HANDLE,
|
|
||||||
pub launch: Launch,
|
|
||||||
force_close: ForceClose,
|
|
||||||
empty_session: EmptySession,
|
|
||||||
anti_afk: AntiAfk,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for App {
|
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
meta: Meta::default(),
|
elevated: win::is_elevated(),
|
||||||
settings: Settings::default(),
|
debug: false,
|
||||||
stage: Stage::default(),
|
closing: false,
|
||||||
flags: Flags::default(),
|
|
||||||
sysinfo: sysinfo::System::new_all(),
|
|
||||||
game_handle: windows::Win32::Foundation::HANDLE::default(),
|
|
||||||
launch: Launch::default(),
|
|
||||||
force_close: ForceClose::default(),
|
|
||||||
empty_session: EmptySession::default(),
|
|
||||||
anti_afk: AntiAfk::default(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct App {
|
||||||
|
pub settings: Settings,
|
||||||
|
stage: Stage,
|
||||||
|
pub flags: Flags,
|
||||||
|
pub system_info: SystemInfo,
|
||||||
|
game_handle: windows::Win32::Foundation::HANDLE,
|
||||||
|
pub anti_afk: features::anti_afk::AntiAfk,
|
||||||
|
empty_session: features::empty_session::EmptySession,
|
||||||
|
force_close: features::force_close::ForceClose,
|
||||||
|
pub game_networking: features::game_networking::GameNetworking,
|
||||||
|
pub launch: features::launch::Launch,
|
||||||
|
}
|
||||||
|
|
||||||
impl eframe::App for App {
|
impl eframe::App for App {
|
||||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||||
catppuccin_egui::set_theme(ctx, self.settings.theme.into());
|
ctx.request_repaint_after(Duration::from_millis(100));
|
||||||
self.run_timers();
|
self.empty_session.run_timers(&mut self.game_handle);
|
||||||
egui::TopBottomPanel::bottom("bottom_panel")
|
egui::TopBottomPanel::bottom("bottom_panel")
|
||||||
.exact_height(25.0)
|
.exact_height(25.0)
|
||||||
.show(ctx, |ui| {
|
.show(ctx, |ui| {
|
||||||
ui.with_layout(egui::Layout::left_to_right(egui::Align::Center), |ui| {
|
ui.with_layout(egui::Layout::left_to_right(egui::Align::Center), |ui| {
|
||||||
ui.selectable_value(&mut self.stage, Stage::Main, "Main");
|
ui.build_menu(&mut self.stage);
|
||||||
ui.selectable_value(&mut self.stage, Stage::Settings, "Settings");
|
|
||||||
ui.selectable_value(&mut self.stage, Stage::About, "About");
|
|
||||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||||
let button = ui
|
let button = ui
|
||||||
.add_enabled(!self.flags.elevated, egui::Button::new("Elevate"))
|
.add_enabled(!self.flags.elevated, egui::Button::new("Elevate"))
|
||||||
.on_hover_text("Relaunch ourselves as administrator.")
|
.on_hover_text("Relaunch ourselves as administrator.")
|
||||||
.on_disabled_hover_text("We are already running elevated.");
|
.on_disabled_hover_text("We are already running elevated.");
|
||||||
if button.clicked() {
|
if button.clicked() {
|
||||||
util::win::elevate(util::win::ElevationExitMethod::Gentle(
|
win::elevate(win::ElevationExitMethod::Gentle(&mut self.flags.closing));
|
||||||
&mut self.flags.closing,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -95,53 +77,17 @@ impl eframe::App for App {
|
|||||||
egui::ScrollArea::vertical()
|
egui::ScrollArea::vertical()
|
||||||
.auto_shrink([false, true])
|
.auto_shrink([false, true])
|
||||||
.show(ui, |ui| match self.stage {
|
.show(ui, |ui| match self.stage {
|
||||||
Stage::Main => {
|
Stage::Main => self.show_main_stage(ctx, ui),
|
||||||
self.show_game(ctx, ui);
|
Stage::Settings => self.show_settings_stage(ctx, ui),
|
||||||
self.show_session(ctx, ui);
|
Stage::About => self.show_about_stage(ctx, ui),
|
||||||
self.show_network(ctx, ui);
|
|
||||||
}
|
|
||||||
Stage::Settings => self.show_settings(ctx, ui),
|
|
||||||
Stage::About => self.show_about(ctx, ui),
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
if tools::check_debug_keycombo_pressed(ctx) {
|
if tools::debug_keycombo_pressed(ctx) || tools::debug_viewport_close_pressed(ctx) {
|
||||||
self.flags.debug = !self.flags.debug;
|
self.flags.debug = !self.flags.debug;
|
||||||
}
|
}
|
||||||
if tools::check_debug_viewport_close_button_pressed(ctx) {
|
|
||||||
self.flags.debug = false;
|
|
||||||
}
|
|
||||||
if self.flags.debug {
|
if self.flags.debug {
|
||||||
let main_rect = ctx.input(|i| {
|
self.show_debug_viewport(ctx);
|
||||||
i.viewport()
|
|
||||||
.clone()
|
|
||||||
.outer_rect
|
|
||||||
.unwrap_or(egui::Rect::EVERYTHING)
|
|
||||||
});
|
|
||||||
let position = [main_rect.right(), main_rect.min.y];
|
|
||||||
ctx.show_viewport_immediate(
|
|
||||||
egui::ViewportId::from_hash_of("debug_viewport"),
|
|
||||||
egui::ViewportBuilder::default()
|
|
||||||
.with_title("GTA Tools Debug")
|
|
||||||
.with_minimize_button(false)
|
|
||||||
.with_maximize_button(false)
|
|
||||||
.with_inner_size(WINDOW_SIZE)
|
|
||||||
.with_position(position)
|
|
||||||
.with_icon(tools::load_icon()),
|
|
||||||
|ctx, _class| {
|
|
||||||
if tools::check_debug_keycombo_pressed(ctx) {
|
|
||||||
self.flags.debug = !self.flags.debug;
|
|
||||||
}
|
|
||||||
egui::CentralPanel::default().show(ctx, |ui| {
|
|
||||||
egui::ScrollArea::both()
|
|
||||||
.auto_shrink([false, true])
|
|
||||||
.show(ui, |ui| {
|
|
||||||
self.show_debug(ctx, ui);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
ctx.request_repaint_after(Duration::from_millis(100));
|
|
||||||
if self.flags.closing {
|
if self.flags.closing {
|
||||||
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
||||||
}
|
}
|
||||||
@@ -149,49 +95,39 @@ impl eframe::App for App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
fn show_game(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) {
|
fn show_game_section(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) {
|
||||||
tools::header(ui, "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);
|
||||||
}
|
}
|
||||||
tools::build_combo_box::<features::launch::Platform>(
|
egui::ComboBox::from_id_salt("Launch")
|
||||||
ui,
|
.selected_text(self.launch.selected.to_string())
|
||||||
&mut self.launch.selected,
|
.width(120.0)
|
||||||
"Launch",
|
.show_ui(ui, |ui| {
|
||||||
);
|
ui.build_menu(&mut self.launch.selected);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
let force_close_button = ui.add_sized(
|
let force_close_button = ui.add_sized(
|
||||||
[104.0, 0.0],
|
[108.0, 0.0],
|
||||||
egui::Button::new(&self.force_close.button_text),
|
egui::Button::new(&self.force_close.button_text),
|
||||||
);
|
);
|
||||||
if force_close_button.clicked() && !self.force_close.prompting {
|
self.force_close
|
||||||
self.force_close.prompting();
|
.prompt(force_close_button.clicked(), &mut self.system_info);
|
||||||
self.flags.current_frame = true;
|
|
||||||
}
|
|
||||||
if self.force_close.prompting
|
|
||||||
&& self.force_close.interval.elapsed() <= Duration::from_secs(3)
|
|
||||||
{
|
|
||||||
if force_close_button.clicked() && !self.flags.current_frame {
|
|
||||||
features::force_close::activate(&mut self.sysinfo);
|
|
||||||
self.force_close = ForceClose::default();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
self.force_close = ForceClose::default();
|
|
||||||
}
|
|
||||||
if self.flags.current_frame {
|
|
||||||
self.flags.current_frame = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_session(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) {
|
fn show_session_section(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) {
|
||||||
tools::header(ui, "Session");
|
ui.header("Session");
|
||||||
ui.add_enabled_ui(!self.empty_session.disabled, |ui| {
|
ui.add_enabled_ui(!self.empty_session.disabled, |ui| {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
if ui.button("Empty current session").clicked() {
|
if ui.button("Empty current session").clicked()
|
||||||
|
&& features::empty_session::activate(
|
||||||
|
&mut self.game_handle,
|
||||||
|
&mut self.system_info,
|
||||||
|
)
|
||||||
|
{
|
||||||
self.empty_session.interval = Instant::now();
|
self.empty_session.interval = Instant::now();
|
||||||
self.empty_session.disabled = true;
|
self.empty_session.disabled = true;
|
||||||
features::empty_session::activate(&mut self.game_handle, &mut self.sysinfo);
|
|
||||||
}
|
}
|
||||||
ui.label(&self.empty_session.countdown.i_string);
|
ui.label(&self.empty_session.countdown.i_string);
|
||||||
});
|
});
|
||||||
@@ -202,7 +138,7 @@ impl App {
|
|||||||
if self.anti_afk.enabled {
|
if self.anti_afk.enabled {
|
||||||
ui.add_space(8.0);
|
ui.add_space(8.0);
|
||||||
ui.add_enabled_ui(false, |ui| {
|
ui.add_enabled_ui(false, |ui| {
|
||||||
ui.label(if util::win::is_window_focused(GTA_WINDOW_TITLE) {
|
ui.label(if win::is_window_focused(WINDOW_TITLE) {
|
||||||
"GTA is focused."
|
"GTA is focused."
|
||||||
} else {
|
} else {
|
||||||
"GTA is not focused!"
|
"GTA is not focused!"
|
||||||
@@ -210,34 +146,42 @@ impl App {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if self.anti_afk.enabled && self.anti_afk.interval.elapsed() >= features::anti_afk::INTERVAL
|
if self.anti_afk.can_activate() && self.anti_afk.should_activate() {
|
||||||
{
|
|
||||||
self.anti_afk.activate();
|
self.anti_afk.activate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_network(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) {
|
fn show_network_section(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) {
|
||||||
tools::header(ui, "Network");
|
ui.header("Network");
|
||||||
egui::Frame::new()
|
egui::Frame::new()
|
||||||
.outer_margin(egui::vec2(0.0, -2.0))
|
.outer_margin(egui::vec2(0.0, -2.0))
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
let response = ui.add_enabled_ui(self.flags.elevated, |ui| {
|
let response = ui.add_enabled_ui(self.flags.elevated, |ui| {
|
||||||
let label = ui.label("Game's network access");
|
let label = ui.horizontal(|ui| {
|
||||||
|
let label = ui.label("Game's network access");
|
||||||
|
ui.add_space(1.0);
|
||||||
|
ui.create_indicator_dot(self.game_networking.blocked_status)
|
||||||
|
.on_hover_text(
|
||||||
|
"This turns yellow if GTA Tools\ncannot find your game.",
|
||||||
|
);
|
||||||
|
self.game_networking.if_failed_return_to_boolean();
|
||||||
|
label
|
||||||
|
});
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
let available_width = label.rect.width();
|
let available_width = label.inner.rect.width();
|
||||||
let spacing = ui.spacing().item_spacing.x;
|
let spacing = ui.spacing().item_spacing.x;
|
||||||
let button_width = (available_width - spacing) / 2.0;
|
let button_width = (available_width - spacing) / 2.0;
|
||||||
if ui
|
if ui
|
||||||
.add_sized([button_width, 18.0], egui::Button::new("Block"))
|
.add_sized([button_width, 18.0], egui::Button::new("Block"))
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
features::game_networking::block_all(&mut self.sysinfo);
|
self.game_networking.block_all(&mut self.system_info);
|
||||||
}
|
}
|
||||||
if ui
|
if ui
|
||||||
.add_sized([button_width, 18.0], egui::Button::new("Unblock"))
|
.add_sized([button_width, 18.0], egui::Button::new("Unblock"))
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
features::game_networking::unblock_all();
|
self.game_networking.unblock_all();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -247,34 +191,61 @@ impl App {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_about(&self, _ctx: &egui::Context, ui: &mut egui::Ui) {
|
fn show_main_stage(&mut self, ctx: &egui::Context, ui: &mut egui::Ui) {
|
||||||
|
self.show_game_section(ctx, ui);
|
||||||
|
self.show_session_section(ctx, ui);
|
||||||
|
self.show_network_section(ctx, 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| {
|
||||||
|
let selection = self.settings.theme;
|
||||||
|
ui.label("Theme");
|
||||||
|
egui::ComboBox::from_id_salt("Theme")
|
||||||
|
.selected_text(self.settings.theme.to_string())
|
||||||
|
.show_ui(ui, |ui| {
|
||||||
|
ui.build_menu(&mut self.settings.theme);
|
||||||
|
});
|
||||||
|
if selection != self.settings.theme {
|
||||||
|
catppuccin_egui::set_theme(ctx, self.settings.theme.into());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ui.checkbox(&mut self.settings.start_elevated, "Always start elevated");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_about_stage(&self, _ctx: &egui::Context, ui: &mut egui::Ui) {
|
||||||
ui.with_layout(egui::Layout::bottom_up(egui::Align::Center), |ui| {
|
ui.with_layout(egui::Layout::bottom_up(egui::Align::Center), |ui| {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
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.hyperlink_to("❤", "https://codeberg.org/futile/gta-tools");
|
ui.scope(|ui| {
|
||||||
|
ui.style_mut().visuals.override_text_color = Some(colours::RED);
|
||||||
|
ui.label("❤");
|
||||||
|
});
|
||||||
ui.label(" from ");
|
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.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||||
ui.label(format!(
|
ui.style_mut().spacing.button_padding = egui::Vec2::new(4.0, 0.0);
|
||||||
"v{} {}",
|
if ui.button("").on_hover_text("View source code").clicked() {
|
||||||
self.meta.current_version,
|
open::that("https://github.com/elituf/gta-tools").unwrap();
|
||||||
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")))
|
||||||
|
.on_hover_text(
|
||||||
|
egui::RichText::new(env!("LATEST_GIT_COMMIT_HASH")).monospace(),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
ui.add(egui::Image::new(egui::include_image!(
|
ui.add(egui::Image::new(egui::include_image!(
|
||||||
@@ -282,68 +253,17 @@ impl App {
|
|||||||
)));
|
)));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_settings(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) {
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.label("Theme");
|
|
||||||
tools::build_combo_box::<settings::Theme>(ui, &mut self.settings.theme, "Theme");
|
|
||||||
});
|
|
||||||
ui.checkbox(&mut self.settings.start_elevated, "Always start elevated");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn show_debug(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) {
|
|
||||||
if ui.button("open storage path").clicked() {
|
|
||||||
open::that_detached(APP_STORAGE_PATH.as_path()).unwrap();
|
|
||||||
}
|
|
||||||
ui.collapsing("anti afk", |ui| {
|
|
||||||
ui.label(format!(
|
|
||||||
"timer: {}",
|
|
||||||
self.anti_afk.interval.elapsed().as_secs()
|
|
||||||
));
|
|
||||||
ui.label(format!(
|
|
||||||
"can activate: {}",
|
|
||||||
features::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() == ENHANCED || p.name() == 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:#?}")));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run_timers(&mut self) {
|
|
||||||
if self.empty_session.disabled {
|
|
||||||
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(&mut self.game_handle);
|
|
||||||
self.empty_session.disabled = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for App {
|
impl Drop for App {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
// save any persistent state to config file
|
// save any persistent state to config file
|
||||||
let persistent_state = PersistentState {
|
PersistentState {
|
||||||
launcher: self.launch.selected,
|
launcher: self.launch.selected,
|
||||||
|
anti_afk_enabled: self.anti_afk.enabled,
|
||||||
settings: self.settings.clone(),
|
settings: self.settings.clone(),
|
||||||
};
|
}
|
||||||
persistent_state.set();
|
.set();
|
||||||
// make sure we are not suspending game
|
// make sure we are not suspending game
|
||||||
features::empty_session::deactivate(&mut self.game_handle);
|
features::empty_session::deactivate(&mut self.game_handle);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
use crate::{
|
||||||
|
gui::{
|
||||||
|
app::{App, WINDOW_SIZE},
|
||||||
|
tools,
|
||||||
|
ui_ext::UiExt,
|
||||||
|
},
|
||||||
|
util::consts::{
|
||||||
|
game::{EXE_ENHANCED, EXE_LEGACY},
|
||||||
|
path,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use eframe::egui;
|
||||||
|
|
||||||
|
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.scope(|ui| {
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::{
|
||||||
|
GetForegroundWindow, GetWindowTextW,
|
||||||
|
};
|
||||||
|
let current_title = {
|
||||||
|
let mut buffer = [0; 512];
|
||||||
|
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}\""));
|
||||||
|
});
|
||||||
|
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("system info", |ui| {
|
||||||
|
if ui.button("refresh").clicked() {
|
||||||
|
self.system_info.refresh();
|
||||||
|
}
|
||||||
|
let pid = self
|
||||||
|
.system_info
|
||||||
|
.processes()
|
||||||
|
.iter()
|
||||||
|
.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:#?}")));
|
||||||
|
}
|
||||||
|
|
||||||
|
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 builder = egui::ViewportBuilder::default()
|
||||||
|
.with_title("GTA Tools Debug")
|
||||||
|
.with_minimize_button(false)
|
||||||
|
.with_maximize_button(false)
|
||||||
|
.with_inner_size(WINDOW_SIZE)
|
||||||
|
.with_position([main.right() - 12.0, main.min.y])
|
||||||
|
.with_icon(tools::load_icon());
|
||||||
|
ctx.show_viewport_immediate(
|
||||||
|
egui::ViewportId::from_hash_of("debug_viewport"),
|
||||||
|
builder,
|
||||||
|
|ctx, _class| {
|
||||||
|
if tools::debug_keycombo_pressed(ctx) {
|
||||||
|
self.flags.debug = !self.flags.debug;
|
||||||
|
}
|
||||||
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
|
egui::ScrollArea::both()
|
||||||
|
.auto_shrink([false, true])
|
||||||
|
.show(ui, |ui| self.add_debug_viewport_contents(ui));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+33
-29
@@ -3,54 +3,58 @@ 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};
|
|
||||||
|
|
||||||
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 = chrono::Local::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, false);
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::unnecessary_wraps)]
|
|
||||||
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>> {
|
||||||
std::panic::set_hook(Box::new(panic_hook));
|
// 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
|
||||||
if let Some(persistent_state) = PersistentState::get() {
|
if let Some(persistent_state) = PersistentState::get() {
|
||||||
app.launch.selected = persistent_state.launcher;
|
persistent_state.apply_to(&mut app);
|
||||||
app.settings = persistent_state.settings;
|
|
||||||
}
|
}
|
||||||
let elevated = util::win::is_elevated();
|
// check if we're elevated. if not, and the user wants an elevated launch - relaunch elevated
|
||||||
if app.settings.start_elevated && !elevated {
|
if !app.flags.elevated && app.settings.start_elevated {
|
||||||
util::win::elevate(util::win::ElevationExitMethod::Forced);
|
win::elevate(win::ElevationExitMethod::Forced);
|
||||||
}
|
}
|
||||||
app.flags.elevated = elevated;
|
// 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);
|
egui_extras::install_image_loaders(&cc.egui_ctx);
|
||||||
cc.egui_ctx.style_mut(|style| {
|
// 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());
|
||||||
|
// apply some global styling that we like
|
||||||
|
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;
|
||||||
|
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();
|
let mut fonts = egui::FontDefinitions::default();
|
||||||
fonts.font_data.insert(
|
fonts.font_data.insert(
|
||||||
"Ubuntu-Regular".to_owned(),
|
"Inter 18pt Light".to_owned(),
|
||||||
egui::FontData::from_static(include_bytes!("../../assets/Ubuntu-Regular.ttf")).into(),
|
egui::FontData::from_static(include_bytes!("../../assets/Inter_18pt-Light.ttf")).into(),
|
||||||
|
);
|
||||||
|
fonts
|
||||||
|
.families
|
||||||
|
.get_mut(&egui::FontFamily::Proportional)
|
||||||
|
.unwrap()
|
||||||
|
.insert(0, "Inter 18pt Light".to_owned());
|
||||||
|
fonts.font_data.insert(
|
||||||
|
"Inter 18pt Regular".to_owned(),
|
||||||
|
egui::FontData::from_static(include_bytes!("../../assets/Inter_18pt-Regular.ttf")).into(),
|
||||||
);
|
);
|
||||||
fonts.families.insert(
|
fonts.families.insert(
|
||||||
egui::FontFamily::Name("Ubuntu-Regular".into()),
|
egui::FontFamily::Name("Inter 18pt Regular".into()),
|
||||||
vec!["Ubuntu-Regular".to_owned()],
|
vec!["Inter 18pt Regular".to_owned()],
|
||||||
);
|
);
|
||||||
cc.egui_ctx.set_fonts(fonts);
|
cc.egui_ctx.set_fonts(fonts);
|
||||||
|
// finally return the App
|
||||||
Ok(app)
|
Ok(app)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+26
-32
@@ -1,19 +1,39 @@
|
|||||||
|
use crate::util::win;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fmt::Display;
|
use strum::{Display, EnumIter};
|
||||||
use strum::EnumIter;
|
|
||||||
|
|
||||||
#[allow(clippy::enum_variant_names)]
|
#[derive(Clone, Copy, Debug, Default, Display, PartialEq, Eq, Serialize, Deserialize, EnumIter)]
|
||||||
#[derive(Clone, Copy, Debug, 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")]
|
||||||
CatppuccinLatte,
|
CatppuccinLatte,
|
||||||
|
#[strum(to_string = "Catppuccin Frappe")]
|
||||||
CatppuccinFrappe,
|
CatppuccinFrappe,
|
||||||
|
#[strum(to_string = "Catppuccin Macchiato")]
|
||||||
CatppuccinMacchiato,
|
CatppuccinMacchiato,
|
||||||
|
#[strum(to_string = "Catppuccin Mocha")]
|
||||||
CatppuccinMocha,
|
CatppuccinMocha,
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
||||||
@@ -22,35 +42,9 @@ impl From<Theme> for catppuccin_egui::Theme {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Theme {
|
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
let x = match self {
|
|
||||||
Self::CatppuccinLatte => "Catppuccin Latte",
|
|
||||||
Self::CatppuccinFrappe => "Catppuccin Frappe",
|
|
||||||
Self::CatppuccinMacchiato => "Catppuccin Macchiato",
|
|
||||||
Self::CatppuccinMocha => "Catppuccin Mocha",
|
|
||||||
};
|
|
||||||
write!(f, "{x}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Theme {
|
|
||||||
pub fn to_catppuccin(self) -> catppuccin_egui::Theme {
|
|
||||||
self.into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
||||||
pub struct Settings {
|
pub 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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+2
-25
@@ -11,34 +11,11 @@ pub fn load_icon() -> egui::IconData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn header(ui: &mut egui::Ui, text: &str) {
|
pub fn debug_keycombo_pressed(ctx: &egui::Context) -> bool {
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.label(egui::RichText::new(text).font(egui::FontId::new(
|
|
||||||
12.5,
|
|
||||||
egui::FontFamily::Name("Ubuntu-Regular".into()),
|
|
||||||
)));
|
|
||||||
ui.add(egui::Separator::default().horizontal());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn build_combo_box<E>(ui: &mut egui::Ui, current_value: &mut E, label: impl std::hash::Hash)
|
|
||||||
where
|
|
||||||
E: strum::IntoEnumIterator + std::fmt::Display + std::cmp::PartialEq + Copy,
|
|
||||||
{
|
|
||||||
egui::ComboBox::from_id_salt(label)
|
|
||||||
.selected_text(current_value.to_string())
|
|
||||||
.show_ui(ui, |ui| {
|
|
||||||
E::iter().for_each(|v| {
|
|
||||||
ui.selectable_value(current_value, v, v.to_string());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn check_debug_keycombo_pressed(ctx: &egui::Context) -> bool {
|
|
||||||
ctx.input(|i| i.modifiers.all() && i.key_pressed(egui::Key::D))
|
ctx.input(|i| i.modifiers.all() && i.key_pressed(egui::Key::D))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn check_debug_viewport_close_button_pressed(ctx: &egui::Context) -> bool {
|
pub fn debug_viewport_close_pressed(ctx: &egui::Context) -> bool {
|
||||||
ctx.input(|i| {
|
ctx.input(|i| {
|
||||||
i.raw
|
i.raw
|
||||||
.viewports
|
.viewports
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
use eframe::egui;
|
||||||
|
|
||||||
|
pub trait UiExt {
|
||||||
|
fn header(&mut self, text: &str);
|
||||||
|
fn build_menu<E>(&mut self, current_value: &mut E)
|
||||||
|
where
|
||||||
|
E: strum::IntoEnumIterator + std::fmt::Display + std::cmp::PartialEq + Copy;
|
||||||
|
fn create_indicator_dot(&mut self, colour: impl Into<egui::Color32>) -> egui::Response;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UiExt for egui::Ui {
|
||||||
|
fn header(&mut self, text: &str) {
|
||||||
|
self.horizontal(|ui| {
|
||||||
|
ui.label(egui::RichText::new(text).font(egui::FontId::new(
|
||||||
|
12.5,
|
||||||
|
egui::FontFamily::Name("Inter 18pt Regular".into()),
|
||||||
|
)));
|
||||||
|
ui.add(egui::Separator::default().horizontal());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_menu<E>(&mut self, current_value: &mut E)
|
||||||
|
where
|
||||||
|
E: strum::IntoEnumIterator + std::fmt::Display + std::cmp::PartialEq + Copy,
|
||||||
|
{
|
||||||
|
E::iter().for_each(|variant| {
|
||||||
|
self.selectable_value(current_value, variant, variant.to_string());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_indicator_dot(&mut self, colour: impl Into<egui::Color32>) -> egui::Response {
|
||||||
|
self.add(
|
||||||
|
egui::Image::new(egui::include_image!("../../assets/circle.svg"))
|
||||||
|
.max_size([4.0, 4.0].into())
|
||||||
|
.tint(colour),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+18
@@ -4,6 +4,24 @@ mod features;
|
|||||||
mod gui;
|
mod gui;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
|
fn init_storage() {
|
||||||
|
if !util::consts::path::APP_STORAGE.exists() {
|
||||||
|
std::fs::create_dir_all(util::consts::path::APP_STORAGE.as_path()).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn panic_hook(panic_info: &std::panic::PanicHookInfo<'_>) {
|
||||||
|
let backtrace = std::backtrace::Backtrace::capture();
|
||||||
|
let mut message = format!("{panic_info}");
|
||||||
|
if backtrace.status() == std::backtrace::BacktraceStatus::Captured {
|
||||||
|
message += &format!("\nstack backtrace:\n{backtrace}");
|
||||||
|
}
|
||||||
|
log::error!("{message}");
|
||||||
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
init_storage();
|
||||||
|
util::logging::Logger::init(log::LevelFilter::Info);
|
||||||
|
std::panic::set_hook(Box::new(panic_hook));
|
||||||
gui::run::run();
|
gui::run::run();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
pub mod codeberg;
|
|
||||||
pub mod consts;
|
pub mod consts;
|
||||||
pub mod countdown;
|
pub mod countdown;
|
||||||
pub mod meta;
|
pub mod logging;
|
||||||
pub mod persistent_state;
|
pub mod persistent_state;
|
||||||
|
pub mod system_info;
|
||||||
pub mod win;
|
pub mod win;
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
use semver::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";
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Release {
|
|
||||||
pub version: Version,
|
|
||||||
pub download_url: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Release {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
version: Version::parse(env!("CARGO_PKG_VERSION")).unwrap(),
|
|
||||||
download_url: String::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_latest_release() -> Option<Release> {
|
|
||||||
let request_url = format!("{CODEBERG_ENDPOINT_ROOT}/repos/futile/gta-tools/releases/latest");
|
|
||||||
let ureq: ureq::Agent = ureq::Agent::config_builder()
|
|
||||||
.user_agent(APP_USER_AGENT)
|
|
||||||
.build()
|
|
||||||
.into();
|
|
||||||
let mut response = ureq.get(request_url).call().ok()?;
|
|
||||||
let json = response.body_mut().read_json::<serde_json::Value>().ok()?;
|
|
||||||
let tag_name = json["tag_name"].as_str()?;
|
|
||||||
let browser_download_url = json["assets"][0]["browser_download_url"].as_str()?;
|
|
||||||
Some(Release {
|
|
||||||
version: Version::parse(tag_name).expect("expected a valid semver pattern"),
|
|
||||||
download_url: String::from(browser_download_url),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
+31
-6
@@ -1,9 +1,34 @@
|
|||||||
use std::{path::PathBuf, sync::LazyLock};
|
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 const ENHANCED: &str = "GTA5_Enhanced.exe";
|
pub mod game {
|
||||||
pub const LEGACY: &str = "GTA5.exe";
|
pub const EXE_ENHANCED: &str = "GTA5_Enhanced.exe";
|
||||||
|
pub const EXE_LEGACY: &str = "GTA5.exe";
|
||||||
|
pub const WINDOW_TITLE: &str = "Grand Theft Auto V";
|
||||||
|
}
|
||||||
|
|
||||||
pub const GTA_WINDOW_TITLE: &str = "Grand Theft Auto V";
|
pub mod colours {
|
||||||
|
use crate::features::game_networking::BlockedStatus;
|
||||||
|
use eframe::egui;
|
||||||
|
|
||||||
pub static APP_STORAGE_PATH: LazyLock<PathBuf> =
|
pub const RED: egui::Color32 = egui::Color32::from_rgb(255, 96, 96);
|
||||||
LazyLock::new(|| dirs::config_local_dir().unwrap().join("GTA Tools"));
|
pub const YELLOW: egui::Color32 = egui::Color32::from_rgb(255, 255, 96);
|
||||||
|
pub const GREEN: egui::Color32 = egui::Color32::from_rgb(96, 255, 96);
|
||||||
|
|
||||||
|
impl From<BlockedStatus> for egui::Color32 {
|
||||||
|
fn from(value: BlockedStatus) -> Self {
|
||||||
|
match value {
|
||||||
|
BlockedStatus::Blocked => RED,
|
||||||
|
BlockedStatus::Failed => YELLOW,
|
||||||
|
BlockedStatus::Unblocked => GREEN,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ use std::time::{Duration, Instant};
|
|||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Countdown {
|
pub struct Countdown {
|
||||||
pub i: u64,
|
i: u64,
|
||||||
pub i_original: u64,
|
i_original: u64,
|
||||||
pub i_string: String,
|
pub i_string: String,
|
||||||
pub interval: Instant,
|
interval: Instant,
|
||||||
pub first_count: bool,
|
first_count: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Countdown {
|
impl Countdown {
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
use std::{
|
||||||
|
fs::File,
|
||||||
|
io::Write,
|
||||||
|
sync::{LazyLock, Mutex},
|
||||||
|
time::SystemTime,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct Logger {
|
||||||
|
file: Mutex<File>,
|
||||||
|
}
|
||||||
|
|
||||||
|
static LOGGER: LazyLock<Logger> = LazyLock::new(|| Logger {
|
||||||
|
file: File::options()
|
||||||
|
.create(true)
|
||||||
|
.append(true)
|
||||||
|
.open(crate::util::consts::path::APP_LOG.as_path())
|
||||||
|
.unwrap()
|
||||||
|
.into(),
|
||||||
|
});
|
||||||
|
|
||||||
|
impl log::Log for Logger {
|
||||||
|
fn enabled(&self, metadata: &log::Metadata) -> bool {
|
||||||
|
metadata.level() <= log::Level::Info
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&self) {}
|
||||||
|
|
||||||
|
fn log(&self, record: &log::Record) {
|
||||||
|
if self.enabled(record.metadata()) {
|
||||||
|
self.log_to_stderr(record);
|
||||||
|
self.log_to_file(record);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Logger {
|
||||||
|
pub fn init(level: log::LevelFilter) {
|
||||||
|
log::set_logger(&*LOGGER)
|
||||||
|
.map(|()| log::set_max_level(level))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn log_to_file(&self, record: &log::Record) {
|
||||||
|
let mut file = self.file.lock().unwrap();
|
||||||
|
write!(
|
||||||
|
file,
|
||||||
|
"[{}][{}]\n{}\n\n",
|
||||||
|
humantime::format_rfc3339_seconds(SystemTime::now()),
|
||||||
|
record.level(),
|
||||||
|
record.args()
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
file.flush().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn log_to_stderr(&self, record: &log::Record) {
|
||||||
|
eprintln!(
|
||||||
|
"[{}][{}]\n{}",
|
||||||
|
humantime::format_rfc3339_seconds(SystemTime::now()),
|
||||||
|
record.level(),
|
||||||
|
record.args()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,35 +1,42 @@
|
|||||||
use crate::{features::launch::Platform, gui::settings::Settings, util::consts::APP_STORAGE_PATH};
|
use crate::{
|
||||||
|
features::launch::Platform,
|
||||||
|
gui::{app, 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,
|
||||||
|
pub anti_afk_enabled: bool,
|
||||||
pub settings: Settings,
|
pub settings: Settings,
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn apply_to(self, app: &mut app::App) {
|
||||||
|
let Self {
|
||||||
|
launcher,
|
||||||
|
anti_afk_enabled,
|
||||||
|
settings,
|
||||||
|
} = self;
|
||||||
|
app.launch.selected = launcher;
|
||||||
|
app.anti_afk.enabled = anti_afk_enabled;
|
||||||
|
app.settings = settings;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 const 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])
|
||||||
|
}
|
||||||
+34
-25
@@ -4,7 +4,7 @@ use windows::{
|
|||||||
Security::{GetTokenInformation, TOKEN_ELEVATION, TOKEN_QUERY, TokenElevation},
|
Security::{GetTokenInformation, TOKEN_ELEVATION, TOKEN_QUERY, TokenElevation},
|
||||||
System::Threading::{GetCurrentProcess, OpenProcessToken},
|
System::Threading::{GetCurrentProcess, OpenProcessToken},
|
||||||
UI::{
|
UI::{
|
||||||
Input::KeyboardAndMouse::GetAsyncKeyState,
|
Input::KeyboardAndMouse::{GetAsyncKeyState, VIRTUAL_KEY},
|
||||||
Shell::ShellExecuteW,
|
Shell::ShellExecuteW,
|
||||||
WindowsAndMessaging::{
|
WindowsAndMessaging::{
|
||||||
CURSOR_SHOWING, CURSORINFO, GetCursorInfo, GetForegroundWindow, GetWindowTextW,
|
CURSOR_SHOWING, CURSORINFO, GetCursorInfo, GetForegroundWindow, GetWindowTextW,
|
||||||
@@ -25,26 +25,21 @@ pub fn is_cursor_visible() -> bool {
|
|||||||
cbSize: u32::try_from(std::mem::size_of::<CURSORINFO>()).unwrap(),
|
cbSize: u32::try_from(std::mem::size_of::<CURSORINFO>()).unwrap(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
unsafe {
|
unsafe { GetCursorInfo(&raw mut ci) }.unwrap();
|
||||||
GetCursorInfo(&mut ci).unwrap();
|
|
||||||
}
|
|
||||||
ci.flags == CURSOR_SHOWING
|
ci.flags == CURSOR_SHOWING
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::cast_sign_loss)]
|
|
||||||
pub fn is_window_focused(target_title: &str) -> bool {
|
pub fn is_window_focused(target_title: &str) -> bool {
|
||||||
let mut buffer = [0; 512];
|
let mut buffer = [0; 512];
|
||||||
unsafe {
|
let hwnd = unsafe { GetForegroundWindow() };
|
||||||
let hwnd = GetForegroundWindow();
|
let length = unsafe { GetWindowTextW(hwnd, &mut buffer) };
|
||||||
let length = GetWindowTextW(hwnd, &mut buffer);
|
let current_title = String::from_utf16_lossy(&buffer[..length as usize]);
|
||||||
let current_title = String::from_utf16_lossy(&buffer[..length as usize]);
|
current_title == target_title
|
||||||
current_title == target_title
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_any_key_pressed(keys: &[u8]) -> bool {
|
pub fn is_any_key_pressed(keys: &[VIRTUAL_KEY]) -> bool {
|
||||||
keys.iter()
|
keys.iter()
|
||||||
.any(|&key| unsafe { (GetAsyncKeyState(i32::from(key)) & i16::MIN) != 0 })
|
.any(|&key| unsafe { GetAsyncKeyState(i32::from(key.0)) } & i16::MIN != 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn elevate(closing: ElevationExitMethod) {
|
pub fn elevate(closing: ElevationExitMethod) {
|
||||||
@@ -67,20 +62,34 @@ pub fn elevate(closing: ElevationExitMethod) {
|
|||||||
|
|
||||||
pub fn is_elevated() -> bool {
|
pub fn is_elevated() -> bool {
|
||||||
let mut token: HANDLE = HANDLE::default();
|
let mut token: HANDLE = HANDLE::default();
|
||||||
unsafe {
|
if unsafe { OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &raw mut token) }.is_err() {
|
||||||
if OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token).is_err() {
|
return false;
|
||||||
return false;
|
}
|
||||||
}
|
let mut elevation = TOKEN_ELEVATION::default();
|
||||||
let mut elevation = TOKEN_ELEVATION::default();
|
let mut size = u32::try_from(std::mem::size_of::<TOKEN_ELEVATION>()).unwrap();
|
||||||
let mut size = u32::try_from(std::mem::size_of::<TOKEN_ELEVATION>()).unwrap();
|
let result = unsafe {
|
||||||
let result = GetTokenInformation(
|
GetTokenInformation(
|
||||||
token,
|
token,
|
||||||
TokenElevation,
|
TokenElevation,
|
||||||
Some((&raw mut elevation).cast()),
|
Some((&raw mut elevation).cast()),
|
||||||
size,
|
size,
|
||||||
&mut size,
|
&raw mut size,
|
||||||
);
|
)
|
||||||
CloseHandle(token).unwrap();
|
};
|
||||||
result.is_ok() && elevation.TokenIsElevated != 0
|
unsafe { CloseHandle(token) }.unwrap();
|
||||||
}
|
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