160 Commits
Author SHA1 Message Date
futile fce32eab52 update readme 2026-07-11 08:01:25 +01:00
futile a70f868120 update readme 2026-07-11 07:50:57 +01:00
futile 9bc7ef4177 update readme 2026-07-11 07:41:23 +01:00
futile 6cb70e87af 0.12.0 2026-07-11 07:31:00 +01:00
futile 393588d8d0 update readme 2026-07-11 06:53:18 +01:00
futile f22040d8e3 update view source code url 2026-07-11 06:52:15 +01:00
futile 937d775f63 delete github workflows 2026-07-11 04:39:23 +01:00
futile 709b17d729 update screenshots 2026-07-11 04:36:52 +01:00
futile 9a18ea1576 fix width of save server ip box 2026-07-11 04:30:49 +01:00
futile 96c4fd80a4 optimise circle.svg, for some reason 2026-06-28 17:47:39 +01:00
futile 47b647784e revert "update logo"
This reverts commit 890ae36590.
2026-06-28 17:34:02 +01:00
futile 3cf1d708a8 update egui & co, and rip out catppuccin_egui (garbage) 2026-06-28 17:30:43 +01:00
futile e8df86c250 update dependencies 2026-06-28 16:33:21 +01:00
futile 40bb3e2cc2 update text a lil 2026-06-28 16:24:09 +01:00
futile 7c5e64774c fuck that file 2026-06-28 16:22:21 +01:00
futile a02293a518 update readme 2026-06-28 16:14:56 +01:00
futile 1f296c3880 remove conditional elevation 2026-06-28 16:14:49 +01:00
futile 054c86e2d2 show reset button only when save server ip is not the default value 2026-06-28 15:38:19 +01:00
futile eed72cdec9 firewall: save a to_string call 2026-01-16 16:10:49 +00:00
Sapphireandfutile 749bf9a7a7 cargo clippy 2025-12-12 22:54:00 +00:00
Sapphireandfutile f1f71e7533 Upload artifact 2025-12-12 22:54:00 +00:00
Sapphireandfutile 27eeb48044 Add CI 2025-12-12 22:54:00 +00:00
Sapphire 60f1868334 Use anyhow for Result type 2025-12-12 03:40:33 -06:00
futile 44e5997847 update todo 2025-12-12 09:39:56 +00:00
Sapphireandfutile 0384eee42c Improve logging 2025-12-12 09:36:21 +00:00
Sapphireandfutile 89a320fd0f logging: remove padding newlines 2025-12-12 09:36:21 +00:00
Sapphireandfutile 58fd6b7e9c game_networking: turn blocked into enum
Allows us to keep track of what's blocked without querying rules constantly.
2025-12-12 09:36:21 +00:00
futileandSapphire 53fd0112c2 apply review suggestions
Co-authored-by: Sapphire <imsapphire0@gmail.com>
2025-12-12 03:52:40 -05:00
futileandSapphire 23fd4b12f9 use Firewall in game_networking 2025-12-12 03:52:40 -05:00
futileandSapphire e0a5821431 add is_blocked method to Firewall 2025-12-12 03:52:40 -05:00
futileandSapphire 50b5388f8a use Firewall in empty_session 2025-12-12 03:52:40 -05:00
futileandSapphire d185ab1a57 add Firewall to App 2025-12-12 03:52:40 -05:00
futileandSapphire 4205a332a7 add firewall helper util 2025-12-12 03:52:40 -05:00
futileandSapphire ade95b5c65 empty session with firewall instead of suspend 2025-12-12 03:52:40 -05:00
futileandSapphire 54985d350a initialize COM in app_creator instead of local to module 2025-12-12 03:52:40 -05:00
futileandSapphire 97e1446996 move get_game_exe_path to be a method on SystemInfo 2025-12-12 03:52:40 -05:00
futile 746b36dfc0 add panic button (useful) 2025-12-12 05:08:56 +00:00
futile 200b92cd48 update todo 2025-12-11 05:58:39 +00:00
futile fb7e1f7864 apply (some) clippy suggestions 2025-12-11 02:44:40 +00:00
futile b0afa9de1a update todo 2025-12-11 02:35:19 +00:00
futile 890ae36590 update logo 2025-12-11 02:34:10 +00:00
futile 89eec435ff update static_vcruntime 2025-12-08 18:30:40 +00:00
futileandSapphire 2611dbfd3e only show save server ip input box sometimes
Co-authored-by: Sapphire <imsapphire0@gmail.com>
2025-12-01 07:34:21 +00:00
futile eafc9e1d10 update todo 2025-11-26 22:43:48 +00:00
futile 0e92c9908d update todo 2025-11-24 12:18:08 +00:00
futile 8ffc5efb8d bump tauri-winres 2025-11-22 14:20:31 +00:00
futile bd8f70ed5f update todo 2025-11-22 11:30:38 +00:00
futile 9227533303 update readme 2025-11-22 10:06:23 +00:00
futile 4b522d9b3f move "open storage path" feature from debug to settings 2025-11-22 09:47:44 +00:00
futile d393bbfc37 better error handling and variable name changes in launch 2025-11-22 09:39:14 +00:00
futile 7190dc05fe rename ensure_not_both_blocked_simultaneously to ensure_block_exclusivity 2025-11-22 09:27:03 +00:00
futile 23b6c879ae better error handling of block exclusivity 2025-11-22 09:27:02 +00:00
futile 660a478fbb greatly simplify blocked status by removing temporary failure state 2025-11-22 09:27:02 +00:00
futile 28486174f2 create block/unblock buttons more idiomatically 2025-11-22 09:27:01 +00:00
futile 0f65c76055 remove size limit of force close button, as it could be too large on various scaling options (why was it even sized) 2025-11-22 09:26:40 +00:00
futile 8943e61f3a 0.11.0 2025-11-21 12:08:02 +00:00
futile ed0a828fd4 ensure all filters do not use the same name (oops) 2025-11-21 11:56:52 +00:00
futile 96ef494443 let's check in this todo for fun 2025-11-21 11:21:17 +00:00
futile 9b5340a6c5 apply clippy suggestions 2025-11-21 11:19:24 +00:00
futile 74132ec322 heavily modify game_networking & fully implement save server blocking 2025-11-21 11:09:18 +00:00
futile a58606c91d nicen up game_networking 2025-11-21 10:01:41 +00:00
futile 47adf42a48 add settings support for alternative replay glitching 2025-11-20 17:46:58 +00:00
futile 0fb587027c nicen up settings 2025-11-20 16:02:03 +00:00
futile 0424b7b114 fix weird changing colour on futile link in about 2025-11-20 14:16:24 +00:00
futile e89c3acc06 change is_system_theme_dark to is_system_theme_light 2025-11-20 14:10:26 +00:00
futile ab64c29935 update readme 2025-11-08 13:54:42 +00:00
futile e216445c7f update readme screenshots 2025-11-07 12:48:05 +00:00
futile aeb99992bb 0.10.0 2025-11-07 12:26:12 +00:00
futile 8ab6e14560 show git commit hash as monospace 2025-11-07 11:59:57 +00:00
futile b9ebdfdb5c update force close game button width to fit new font 2025-11-07 11:56:26 +00:00
futile 2804812691 replace Ubuntu fonts with Inter 2025-11-07 11:51:38 +00:00
futile 606fb7e191 update readme 2025-11-07 11:37:13 +00:00
futile e2a779b7f5 redo links in about stage a bit 2025-11-07 11:35:56 +00:00
futile a8b04bb72b move colour constants from gui::colours to util::consts::colours 2025-11-07 00:34:42 +00:00
futile c34cf187d5 ensure persistent_state is easier and more infallible to add fields to 2025-11-06 23:55:04 +00:00
futile f07421e75a use newer syntax for cargo instructions in build.rs 2025-11-06 08:53:09 +00:00
futile 8b4a455dc1 make empty_session::activate simply return a bool 2025-11-06 08:45:50 +00:00
futile a1300f8d76 lowkey improve logging 2025-11-06 08:41:53 +00:00
futile 9e623219a5 shorten some paths in main.rs 2025-11-06 05:07:49 +00:00
futile 09e19b6e16 remove hardly useful "windows compilation only" warning 2025-11-06 05:06:57 +00:00
futile 4498c45965 apply clippy suggestion 2025-11-06 04:20:40 +00:00
futile af8a5b1746 persist anti afk toggle 2025-11-06 04:17:03 +00:00
futile 560d9659f4 make git hash embedding more reliable 2025-11-06 03:06:25 +00:00
futile bf086d0442 amend comment 2025-11-06 02:38:13 +00:00
futile a6ea8ad8e2 ignore return of NtResumeProcess in empty_session::deactivate to reduce crashing 2025-11-06 02:37:26 +00:00
futile 4c00d1846b add git commit hash when hovering version info 2025-11-05 00:33:17 +00:00
futile e5e335f4f3 apply clippy suggestion 2025-11-05 00:10:39 +00:00
futile 3ab5987b67 use more unwraps instead of let _ = 2025-11-05 00:08:53 +00:00
futile 2030796337 add description to Cargo.toml after making sure tauri-winres doesn't use it 2025-11-04 23:08:59 +00:00
futile 2edb4f492a get rid of justfile it sucks 2025-11-04 23:08:31 +00:00
futile 114292d912 use tauri-winres instead of winresource & specify some more fields in the windows resource in build.rs 2025-11-04 22:56:03 +00:00
futile 8adc1f49ad add license and author information to Cargo.toml 2025-11-04 20:15:09 +00:00
futile bb27334f57 0.9.0 2025-11-03 07:43:40 +00:00
futile 1e13905885 update codebase for github 2025-11-03 07:13:00 +00:00
futile 8fc73b2992 update readme for github 2025-11-03 06:56:28 +00:00
futile 43b72995dd create github workflow to build and release on tag push 2025-11-03 06:42:22 +00:00
futile d3473d46e3 create github workflow to rebuild and rerelease all existing tags 2025-11-03 05:26:46 +00:00
futile 1363f5b125 use match instead of if for LaunchVersion checks 2025-10-25 09:35:31 +01:00
futile d15b8070ec improve strings in util::system_info 2025-10-11 03:32:39 +01:00
futile efccd6ed50 update readme 2025-09-13 16:58:52 +01:00
futile 5c73ee7bd5 improve (tighten) scoping of unsafe blocks 2025-09-13 16:50:44 +01:00
futile 445b63fa4c small system_info improvements 2025-09-13 16:39:16 +01:00
futile 941868dcc8 use &[c_char] instead of &[i8] in c_char_arr_to_string 2025-09-13 12:02:19 +01:00
futile 762a3b9b26 update readme 2025-09-13 00:34:48 +01:00
futile 0cb211c103 0.8.0 2025-09-12 05:17:47 +01:00
futile 90b32c5c5a this couldn't hurt to be create_dir_all instead of create_dir 2025-09-12 05:12:11 +01:00
futile 71c3c69ea2 a few clippy fixes 2025-09-12 05:04:12 +01:00
futile b553e418f5 reduce clutter in system_info 2025-09-12 01:21:56 +01:00
futile ab026c0bc6 adapt codebase to own, new system info impl 2025-09-12 01:07:10 +01:00
futile 32e21f58a1 create a clone of sysinfo focused on only our needs 2025-09-12 00:52:19 +01:00
futile b0c339cc5f update readme 2025-09-09 14:09:17 +01:00
futile c2584b5d01 reimplement "update checking" in the form of a simple button that goes to latest release 2025-09-09 14:07:46 +01:00
futile 3322f9582f update readme 2025-09-09 03:16:00 +01:00
futile 5348a1d6ee rip out update checking (too unreliable) 2025-09-09 03:15:01 +01:00
futile 05ce68cb47 update readme 2025-09-09 03:03:36 +01:00
futile f63d24ea43 improve reliability and logging when running into game access denied issues 2025-09-09 03:03:32 +01:00
futile 356d5f17e5 me when i don't smoke the drugs that make you write retarded code 2025-09-08 14:32:36 +01:00
futile bc5e80aaca update readme 2025-09-06 21:49:44 +01:00
futile e050231692 update readme 2025-09-06 12:26:38 +01:00
futile 6e33536dfe 0.7.0 2025-09-06 12:11:39 +01:00
futile c36a41f919 remove extra release optimizations to save sanity on compile times 2025-09-06 12:11:20 +01:00
futile 660e7a874f improve reliability of panic hook by moving it to main and always creating its storage path 2025-09-06 12:10:38 +01:00
futile 72abda7352 partial fix some weird colouring that was introduced in egui 0.32 (?) 2025-09-05 17:12:38 +01:00
futile ebf704c6f4 update readme to reflect prior changes 2025-09-05 16:10:47 +01:00
futile 5c5e6d60a1 add option to launch legacy version of the game & fix epic games launch urls 2025-09-05 16:09:24 +01:00
futile 98a6545b27 update dependencies i think 2025-09-05 14:26:27 +01:00
futile a9a965462a random small fixes 2025-09-05 13:24:16 +01:00
futile 81b4b26f96 small grammar fix on non-windows compilation error 2025-09-04 09:44:07 +01:00
futile 06df466d24 switch back to ureq from nyquest for http client
note: even if this raises binary size by a significant amount, it should in theory be more reliable, so it is a worthy sacrifice.
2025-09-04 07:41:54 +01:00
futile b8c444dc5b move theme refresh button to debug menu 2025-09-04 06:21:27 +01:00
futile e848ca8d79 GET OUT 2025-09-04 05:36:00 +01:00
futile 0a831580e2 use reworked logging to log errors in util::meta::default 2025-05-16 10:10:16 +01:00
futile 10e69eaac3 rework (error) logging 2025-05-16 10:03:48 +01:00
futile 9dbfbc4419 rework storage paths a bit 2025-05-13 09:40:45 +01:00
futile ef0b6de6ba improve reliability of util::codeberg::get_latest_release 2025-05-13 09:40:18 +01:00
futile e7470bd694 update readme 2025-05-12 08:00:25 +01:00
futile 936f683385 update readme 2025-05-12 07:50:00 +01:00
futile b3158f71fb 0.6.0 2025-05-10 07:04:59 +01:00
futile 070e477fd4 derive default on some shit instead of manually 2025-05-10 06:57:26 +01:00
futile 7745bba10e improve styling in relation to windows theme 2025-05-10 06:44:58 +01:00
futile 8d7a0d12b9 don't force capture backtrace in panic hook & otherwise improve gui::run 2025-05-04 18:36:32 +01:00
futile b325ebb03c clippy stuff 2025-05-04 18:21:55 +01:00
futile 38c6402a12 split ui of App::show_debug_viewport into App::add_debug_viewport_contents 2025-05-04 18:01:50 +01:00
futile 2d42c0c3c4 improve util::codeberg 2025-05-04 17:52:50 +01:00
futile a423721aee bump nyquest and stop using .json() workaround 2025-05-04 17:52:06 +01:00
futile f3436c8ddc 0.5.4 2025-05-03 13:12:47 +01:00
futile afcf0a87c0 get sysinfo from crates again, instead of git repo 2025-05-03 13:12:14 +01:00
futile 73d610949f use nyquest instead of ureq for http client 2025-05-03 13:10:40 +01:00
futile 7f358d9adb change how module roots are organized 2025-05-01 11:23:27 +01:00
futile 7caa5421f7 eliminate chrono dependency by using unix timestamp in panic logs 2025-04-30 01:36:17 +01:00
futile ecac0d54aa eliminate dirs dependency by checking LOCALAPPDATA envvar instead 2025-04-30 00:53:27 +01:00
futile a84401ca33 fail compilation if compiling outside of windows 2025-04-30 00:48:44 +01:00
futile 1bcd6b2c7d fix duplicate inputs bug in features::anti_afk::send 2025-04-26 05:47:10 +01:00
futile 1dbdc0e4cb remove redundant method to_color32 2025-04-26 04:42:29 +01:00
futile 3610e44a08 create trait UiExt 2025-04-26 04:37:10 +01:00
futile f5fd22d35c redesign if_failed_return_to_unblocked to if_failed_return_to_boolean 2025-04-26 04:23:37 +01:00
futile 022828b802 update readme 2025-04-26 04:07:39 +01:00
futile 262d2d0720 update readme 2025-04-25 17:18:38 +00:00
futile 2326edf97b update readme with a guide 2025-04-24 23:43:52 +01:00
futile 301be82837 update screenshot in readme 2025-04-24 13:54:39 +01:00
34 changed files with 2157 additions and 1791 deletions
+1
View File
@@ -1,2 +1,3 @@
/target
.vscode
.idea
Generated
+1151 -1127
View File
File diff suppressed because it is too large Load Diff
+23 -29
View File
@@ -1,44 +1,38 @@
[package]
name = "gta-tools"
version = "0.5.3"
version = "0.12.0"
edition = "2024"
license = "Apache-2.0"
authors = ["futile <git@futile.eu>"]
description = "A toolset of convenient things for GTA V Online."
[dependencies]
catppuccin-egui = { version = "5.5.0", default-features = false, features = [
"egui31",
] }
chrono = { version = "0.4.40", default-features = false, features = ["clock"] }
dirs = "6.0.0"
eframe = { version = "0.31.1", default-features = false, features = [
anyhow = "1.0"
eframe = { version = "0.35", default-features = false, features = [
"glow",
"default_fonts",
] }
egui_extras = { version = "0.31.1", default-features = false, features = [
egui_extras = { version = "0.35", default-features = false, features = [
"image",
"svg",
] }
image = { version = "0.25.6", 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 = { git = "https://github.com/GuillaumeGomez/sysinfo", rev = "bcbb7b2b389b8800ff457773f48b130e710458fc" }
ureq = { version = "3.0.11", features = ["json"] }
windows = { version = "0.61.1", features = [
"Win32_UI_Input_KeyboardAndMouse",
"Win32_System_Threading",
"Win32_UI_Shell",
"Win32_UI_WindowsAndMessaging",
"Win32_Security",
humantime = "2.3.0"
image = { version = "0.25", default-features = false, features = ["png"] }
log = "0.4"
open = "5"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
strum = { version = "0.28", features = ["derive"] }
windows = { version = "0.62", features = [
"Win32_NetworkManagement_WindowsFirewall",
"Win32_System_Com",
"Win32_System_Diagnostics_ToolHelp",
"Win32_System_Threading",
"Win32_UI_Input_KeyboardAndMouse",
"Win32_UI_WindowsAndMessaging",
] }
winreg = "0.55.0"
winreg = "0.56"
[build-dependencies]
static_vcruntime = "2.0.0"
winresource = "0.1.20"
[profile.release]
lto = true
codegen-units = 1
static_vcruntime = "3.0.0"
tauri-winres = "0.3"
+71 -3
View File
@@ -2,12 +2,80 @@
A toolset of convenient things for GTA V Online.
![](https://i.vgy.me/maLh8W.png)
![](https://i.vgy.me/HVBklM.png)
![](https://i.vgy.me/yzRGjG.png)
> [!NOTE]
> I have been forced to move this project from [GitHub](https://github.com/elituf/gta-tools) to [here](https://gitlab.com/futile/gta-tools). Sorry for the inconvenience. From now on you may always find the project source code via the redirect at [futile.eu/gta-tools](https://futile.eu/gta-tools).
## 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://gitlab.com/futile/gta-tools/-/releases/permalink/latest/downloads/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**.
You will need Rust, which you can get [here](https://rustup.rs). Then, 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**.
> [!NOTE]
> It is recommended to always use an up-to-date version of GTA Tools from [releases](https://gitlab.com/futile/gta-tools/-/releases). You can also easily access this repository by going to the <kbd>About</kbd> page of GTA Tools and clicking the <kbd>git</kbd> button beside the version number.
## Guide
Every feature of GTA Tools works for both the Legacy and Enhanced versions of the game. GTA Tools requires administrator access.
#### Game
This section is quite simple.
<kbd>Launch</kbd> — Starts your game on the chosen launcher and game version.
<kbd>Force close game</kbd> — Kills all game processes. The 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.
<kbd>Empty current session</kbd> — Clears your session of players[^1] by blocking the game's network access for 10 seconds, then unblocking it. 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
<kbd>Anti AFK</kbd> — 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]
#### Network
<kbd>Game's network access</kbd> — Blocks/unblocks GTA V's entire internet access using the Windows Firewall. This feature can also be changed to <kbd>Rockstar save server access</kbd> via the **Network** dropdown of the settings. When in this mode, instead of blocking the entire game's network, it will only block the Rockstar save server at the IP `192.81.241.171`. The IP can also be changed in the same settings, if ever needed.
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.
<details>
<summary>How to do a replay glitch</summary>
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
</details>
<details>
<summary>How to do a replay glitch (save server method)</summary>
1. Get to the finale of a heist/contract/mission series, such as **The Contract: Dr. Dre**
2. At any point before you complete the finale, <kbd>Block</kbd> the save server access (for example, at the beginning of the mission)
3. Complete the finale as normal
4. After loading into online, exit to story mode
5. At this point, <kbd>Unblock</kbd> the save server access and load back into online
6. Finally, force a save by changing your outfit, and you should receive the finale cut, but also be able to go play the finale again immediately
</details>
[^1]: Technically, it just makes you lose connection to the session. But it looks like everyone left you instead.
[^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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -23
View File
@@ -1,23 +1 @@
<?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>
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><circle style="fill:#fff" cx="256" cy="256" r="256"/></svg>

Before

Width:  |  Height:  |  Size: 458 B

After

Width:  |  Height:  |  Size: 147 B

+28 -1
View File
@@ -1,7 +1,34 @@
fn main() {
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_manifest(
r#"<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
</assembly>"#,
)
.compile()
.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");
}
-6
View File
@@ -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
+2 -2
View File
@@ -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,
@@ -57,6 +57,6 @@ fn send(vk_codes: &[VIRTUAL_KEY]) {
};
inputs.push(input);
}
unsafe { SendInput(&inputs, size_of::<INPUT>() as i32) };
}
unsafe { SendInput(&inputs, size_of::<INPUT>() as i32) };
}
+32 -42
View File
@@ -1,13 +1,14 @@
use crate::util::{
consts::game::{EXE_ENHANCED, EXE_LEGACY},
countdown::Countdown,
firewall::{Firewall, RuleDirection, RuleMode, RuleProtocol},
system_info::SystemInfo,
};
use anyhow::Result;
use std::time::{Duration, Instant};
use sysinfo::System;
use windows::Win32::{
Foundation::{CloseHandle, HANDLE, NTSTATUS},
System::Threading::{OpenProcess, PROCESS_SUSPEND_RESUME},
};
const FILTER_NAME_EMPTY_SESSION_IN: &str = "[GTA Tools] Block inbound UDP traffic for all of GTA V";
const FILTER_NAME_EMPTY_SESSION_OUT: &str =
"[GTA Tools] Block outbound UDP traffic for all of GTA V";
const INTERVAL: Duration = Duration::from_secs(10);
@@ -29,53 +30,42 @@ impl Default for EmptySession {
}
impl EmptySession {
pub fn run_timers(&mut self, game_handle: &mut HANDLE) {
pub fn run_timers(&mut self, firewall: &Firewall) -> Result<()> {
if self.disabled {
self.countdown.count();
} else {
self.countdown.reset();
}
if self.interval.elapsed() >= INTERVAL {
deactivate(game_handle);
deactivate(firewall)?;
self.disabled = false;
}
Ok(())
}
}
#[link(name = "ntdll")]
unsafe extern "system" {
unsafe fn NtSuspendProcess(ProcessHandle: HANDLE) -> NTSTATUS;
unsafe fn NtResumeProcess(ProcessHandle: HANDLE) -> NTSTATUS;
pub fn activate(system_info: &mut SystemInfo, firewall: &Firewall) -> Result<bool> {
let Some(exe_path) = system_info.get_game_exe_path() else {
log::info!("wasn't able to find game exe");
return Ok(false);
};
firewall.add(
FILTER_NAME_EMPTY_SESSION_IN,
RuleMode::Executable(exe_path.to_path_buf()),
RuleDirection::In,
RuleProtocol::Udp,
)?;
firewall.add(
FILTER_NAME_EMPTY_SESSION_OUT,
RuleMode::Executable(exe_path.to_path_buf()),
RuleDirection::Out,
RuleProtocol::Udp,
)?;
Ok(true)
}
fn get_gta_pid(sysinfo: &mut System) -> u32 {
sysinfo.refresh_all();
if let Some((pid, _)) = sysinfo
.processes()
.iter()
.find(|(_, p)| p.name() == EXE_ENHANCED || p.name() == EXE_LEGACY)
{
return pid.as_u32();
}
u32::MAX
}
pub fn activate(game_handle: &mut HANDLE, sysinfo: &mut System) {
let pid = get_gta_pid(sysinfo);
if pid == u32::MAX {
return;
}
unsafe {
*game_handle = OpenProcess(PROCESS_SUSPEND_RESUME, false, pid).unwrap();
let _ = NtSuspendProcess(*game_handle);
}
}
pub fn deactivate(game_handle: &mut HANDLE) {
unsafe {
if !game_handle.is_invalid() {
let _ = NtResumeProcess(*game_handle);
let _ = CloseHandle(*game_handle);
}
}
pub fn deactivate(firewall: &Firewall) -> Result<()> {
firewall.remove(FILTER_NAME_EMPTY_SESSION_IN)?;
firewall.remove(FILTER_NAME_EMPTY_SESSION_OUT)?;
Ok(())
}
+16 -12
View File
@@ -1,6 +1,8 @@
use crate::util::consts::game::{EXE_ENHANCED, EXE_LEGACY};
use crate::util::{
consts::game::{EXE_ENHANCED, EXE_LEGACY},
system_info::SystemInfo,
};
use std::time::{Duration, Instant};
use sysinfo::System;
const INTERVAL: Duration = Duration::from_secs(3);
@@ -24,7 +26,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 +36,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();
@@ -44,21 +46,23 @@ impl ForceClose {
*self = Self::default();
}
fn finish_current_frame(&mut self) {
const fn finish_current_frame(&mut self) {
if self.current_frame {
self.current_frame = false;
}
}
}
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::error!("failed to force close game, probably due to access denied");
}
});
sysinfo.refresh_all();
system_info.refresh();
}
+75 -122
View File
@@ -1,147 +1,100 @@
use crate::util::consts::game::{EXE_ENHANCED, EXE_LEGACY};
use std::{
path::Path,
time::{Duration, Instant},
};
use strum::{Display, EnumIter};
use sysinfo::System;
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,
},
use crate::{
gui::settings::BlockMethod,
util::{
firewall::{Firewall, RuleDirection, RuleMode, RuleProtocol},
system_info::SystemInfo,
},
core::BSTR,
};
use anyhow::Result;
use strum::{Display, EnumIter};
const FILTER_NAME_IN: &str = "[GTA Tools] Block all inbound traffic for GTA V";
const FILTER_NAME_OUT: &str = "[GTA Tools] Block all outbound traffic for GTA V";
const FILTER_NAME_EXE: &str = "[GTA Tools] Block outbound traffic for all of GTA V";
const FILTER_NAME_SAVE_SERVER: &str = "[GTA Tools] Block outbound traffic to Rockstar save server";
const INTERVAL: Duration = Duration::from_secs(3);
#[derive(Clone, Copy, Debug, Display, PartialEq, Eq, EnumIter)]
#[derive(Clone, Copy, Debug, Default, Display, EnumIter, PartialEq)]
pub enum BlockedStatus {
Blocked,
Failed,
#[default]
Unblocked,
}
impl From<bool> for BlockedStatus {
fn from(value: bool) -> Self {
if value {
Self::Blocked
} else {
Self::Unblocked
}
}
Server,
Executable,
}
#[derive(Debug)]
pub struct GameNetworking {
com_initialized: bool,
pub blocked_status: BlockedStatus,
timer: Instant,
counting: bool,
pub blocked: BlockedStatus,
}
impl Default for GameNetworking {
fn default() -> Self {
let firewall = Firewall::default();
Self {
blocked_status: Self::is_blocked().into(),
com_initialized: unsafe { CoInitializeEx(None, COINIT_MULTITHREADED) }.is_ok(),
timer: Instant::now(),
counting: false,
}
}
}
impl Drop for GameNetworking {
fn drop(&mut self) {
unsafe {
if self.com_initialized {
CoUninitialize();
}
blocked: if firewall.is_blocked(FILTER_NAME_SAVE_SERVER).unwrap() {
BlockedStatus::Server
} else if firewall.is_blocked(FILTER_NAME_EXE).unwrap() {
BlockedStatus::Executable
} else {
BlockedStatus::Unblocked
},
}
}
}
impl GameNetworking {
pub fn block_all(&mut self, sysinfo: &mut System) {
let Some(exe_path) = get_game_exe_path(sysinfo) else {
self.blocked_status = BlockedStatus::Failed;
return;
pub fn block_exe(&mut self, system_info: &mut SystemInfo, firewall: &Firewall) -> Result<()> {
let Some(exe_path) = system_info.get_game_exe_path() else {
log::warn!("Unable to find game executable path.");
return Ok(());
};
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),
] {
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();
firewall
.add(
FILTER_NAME_EXE,
RuleMode::Executable(exe_path.to_path_buf()),
RuleDirection::Out,
RuleProtocol::Any,
)
.inspect(|_| self.blocked = BlockedStatus::Executable)
}
pub fn unblock_exe(&mut self, firewall: &Firewall) -> Result<()> {
firewall
.remove(FILTER_NAME_EXE)
.inspect(|_| self.blocked = BlockedStatus::Unblocked)
}
pub fn block_save_server(&mut self, save_server_ip: &str, firewall: &Firewall) -> Result<()> {
firewall
.add(
FILTER_NAME_SAVE_SERVER,
RuleMode::Address(save_server_ip.to_owned()),
RuleDirection::Out,
RuleProtocol::Any,
)
.inspect(|_| self.blocked = BlockedStatus::Server)
}
pub fn unblock_save_server(&mut self, firewall: &Firewall) -> Result<()> {
firewall
.remove(FILTER_NAME_SAVE_SERVER)
.inspect(|_| self.blocked = BlockedStatus::Unblocked)
}
pub fn ensure_block_exclusivity(
&mut self,
block_method: BlockMethod,
firewall: &Firewall,
) -> Result<()> {
match block_method {
BlockMethod::EntireGame => {
if self.blocked == BlockedStatus::Server {
self.unblock_save_server(firewall)?;
}
}
BlockMethod::SaveServer => {
if self.blocked == BlockedStatus::Executable {
self.unblock_exe(firewall)?;
}
}
}
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_unblocked(&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 = BlockedStatus::Unblocked;
}
}
}
fn get_game_exe_path(sysinfo: &mut System) -> Option<&Path> {
sysinfo.refresh_all();
if let Some((_, process)) = sysinfo
.processes()
.iter()
.find(|(_, p)| p.name() == EXE_ENHANCED || p.name() == EXE_LEGACY)
{
process.exe()
} else {
None
Ok(())
}
}
+27 -12
View File
@@ -1,3 +1,4 @@
use crate::gui::settings::LaunchVersion;
use serde::{Deserialize, Serialize};
use std::{path::PathBuf, process::Command};
use strum::{Display, EnumIter};
@@ -18,31 +19,45 @@ 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",
};
open::that_detached(steam_url).unwrap();
}
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_key) = hklm.open_subkey(rockstar_url) else {
return;
};
let Ok(install_folder): Result<String, std::io::Error> =
gta_v_enhanced.get_value("InstallFolder")
gta_key.get_value("InstallFolder")
else {
return;
};
let mut play_gtav_path = PathBuf::from(install_folder);
play_gtav_path.push("PlayGTAV.exe");
let _ = Command::new(play_gtav_path).spawn();
let mut exe_path = PathBuf::from(install_folder);
exe_path.push("PlayGTAV.exe");
if let Err(why) = Command::new(exe_path).spawn() {
log::warn!("Couldn't launch GTA 5 via Rockstar Games Launcher: {why}");
}
}
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"
}
};
open::that_detached(epic_url).unwrap();
}
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
mod app;
mod colours;
pub mod app;
mod debug;
pub mod run;
pub mod settings;
mod tools;
mod ui_ext;
+192 -111
View File
@@ -1,7 +1,17 @@
use crate::{
features,
gui::{settings::Settings, tools},
util::{consts::game::WINDOW_TITLE, meta::Meta, persistent_state::PersistentState, win},
features::{self, game_networking::BlockedStatus},
gui::{
settings::{BlockMethod, ROCKSTAR_SAVE_SERVER, Settings},
tools,
ui_ext::UiExt,
},
util::{
consts::{colours, game::WINDOW_TITLE, path},
firewall::Firewall,
persistent_state::PersistentState,
system_info::SystemInfo,
win,
},
};
use eframe::egui;
use std::time::{Duration, Instant};
@@ -19,7 +29,6 @@ enum Stage {
#[derive(Debug)]
pub struct Flags {
pub elevated: bool,
pub debug: bool,
closing: bool,
}
@@ -27,7 +36,6 @@ pub struct Flags {
impl Default for Flags {
fn default() -> Self {
Self {
elevated: win::is_elevated(),
debug: false,
closing: false,
}
@@ -36,12 +44,11 @@ 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,
game_handle: windows::Win32::Foundation::HANDLE,
pub system_info: SystemInfo,
firewall: Firewall,
pub anti_afk: features::anti_afk::AntiAfk,
empty_session: features::empty_session::EmptySession,
force_close: features::force_close::ForceClose,
@@ -50,76 +57,66 @@ pub struct App {
}
impl eframe::App for App {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
ctx.request_repaint_after(Duration::from_millis(100));
self.empty_session.run_timers(&mut self.game_handle);
egui::TopBottomPanel::bottom("bottom_panel")
.exact_height(25.0)
.show(ctx, |ui| {
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
ui.request_repaint_after(Duration::from_millis(100));
self.empty_session.run_timers(&self.firewall).unwrap();
egui::Panel::bottom("bottom_panel")
.exact_size(25.0)
.show(ui, |ui| {
ui.with_layout(egui::Layout::left_to_right(egui::Align::Center), |ui| {
tools::build_menu::<Stage>(ui, &mut self.stage);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let button = ui
.add_enabled(!self.flags.elevated, egui::Button::new("Elevate"))
.on_hover_text("Relaunch ourselves as administrator.")
.on_disabled_hover_text("We are already running elevated.");
if button.clicked() {
win::elevate(win::ElevationExitMethod::Gentle(&mut self.flags.closing));
}
});
ui.build_menu(&mut self.stage);
});
});
egui::CentralPanel::default().show(ctx, |ui| {
egui::CentralPanel::default().show(ui, |ui| {
egui::ScrollArea::vertical()
.auto_shrink([false, true])
.show(ui, |ui| match self.stage {
Stage::Main => self.show_main_stage(ctx, ui),
Stage::Settings => self.show_settings_stage(ctx, ui),
Stage::About => self.show_about_stage(ctx, ui),
Stage::Main => self.show_main_stage(ui),
Stage::Settings => self.show_settings_stage(ui),
Stage::About => self.show_about_stage(ui),
});
});
if tools::debug_keycombo_pressed(ctx) || tools::debug_viewport_close_pressed(ctx) {
if tools::debug_keycombo_pressed(ui) || tools::debug_viewport_close_pressed(ui) {
self.flags.debug = !self.flags.debug;
}
if self.flags.debug {
self.show_debug_viewport(ctx);
self.show_debug_viewport(ui);
}
if self.flags.closing {
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
ui.send_viewport_cmd(egui::ViewportCommand::Close);
}
}
}
impl App {
fn show_game_section(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) {
tools::header(ui, "Game");
fn show_game_section(&mut self, ui: &mut egui::Ui) {
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())
.width(120.0)
.show_ui(ui, |ui| {
tools::build_menu(ui, &mut self.launch.selected);
ui.build_menu(&mut self.launch.selected);
});
});
let force_close_button = ui.add_sized(
[104.0, 0.0],
egui::Button::new(&self.force_close.button_text),
);
let force_close_button = ui.button(&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) {
tools::header(ui, "Session");
fn show_session_section(&mut self, 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.system_info, &self.firewall)
.unwrap()
{
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);
});
@@ -131,9 +128,9 @@ impl App {
ui.add_space(8.0);
ui.add_enabled_ui(false, |ui| {
ui.label(if win::is_window_focused(WINDOW_TITLE) {
"GTA is focused."
"Game is focused."
} else {
"GTA is not focused!"
"Game is not focused!"
})
});
}
@@ -143,98 +140,179 @@ impl App {
}
}
fn show_network_section(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) {
tools::header(ui, "Network");
fn show_network_section(&mut self, ui: &mut egui::Ui) {
ui.header("Network");
egui::Frame::new()
.outer_margin(egui::vec2(0.0, -2.0))
.show(ui, |ui| {
let response = ui.add_enabled_ui(self.flags.elevated, |ui| {
let label = ui.horizontal(|ui| {
let label = ui.label("Game's network access");
ui.add_space(1.0);
ui.add(
egui::Image::new(egui::include_image!("../../assets/circle.svg"))
.max_size([4.0, 4.0].into())
.tint(self.game_networking.blocked_status.to_color32()),
)
.on_hover_text("This turns yellow if GTA Tools\ncannot find your game.");
self.game_networking.if_failed_return_to_unblocked();
label
});
ui.horizontal(|ui| {
let available_width = label.inner.rect.width();
let spacing = ui.spacing().item_spacing.x;
let button_width = (available_width - spacing) / 2.0;
if ui
.add_sized([button_width, 18.0], egui::Button::new("Block"))
.clicked()
let label = ui.horizontal(|ui| {
let label = match self.settings.block_method {
BlockMethod::EntireGame => ui.label("Game's network access"),
BlockMethod::SaveServer => ui.label("Rockstar save server access"),
};
ui.add_space(1.0);
ui.create_indicator_dot(match self.game_networking.blocked {
BlockedStatus::Executable
if self.settings.block_method == BlockMethod::EntireGame =>
{
self.game_networking.block_all(&mut self.sysinfo);
colours::RED
}
if ui
.add_sized([button_width, 18.0], egui::Button::new("Unblock"))
.clicked()
BlockedStatus::Server
if self.settings.block_method == BlockMethod::SaveServer =>
{
self.game_networking.unblock_all();
colours::RED
}
_ => colours::GREEN,
});
label
});
response.response.on_disabled_hover_text(
"This requires administrator.\nUse the Elevate button.",
ui.allocate_ui_with_layout(
egui::vec2(label.inner.rect.width(), 0.0),
egui::Layout::top_down(egui::Align::Min),
|ui| {
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
if ui.button("Block").clicked() {
match self.settings.block_method {
BlockMethod::EntireGame => {
self.game_networking
.block_exe(&mut self.system_info, &self.firewall)
.unwrap();
}
BlockMethod::SaveServer => {
self.game_networking
.block_save_server(
&self.settings.save_server_ip,
&self.firewall,
)
.unwrap();
}
}
}
});
columns[1].vertical_centered_justified(|ui| {
if ui.button("Unblock").clicked() {
match self.settings.block_method {
BlockMethod::EntireGame => {
self.game_networking
.unblock_exe(&self.firewall)
.unwrap();
}
BlockMethod::SaveServer => {
self.game_networking
.unblock_save_server(&self.firewall)
.unwrap();
}
}
}
});
});
},
);
});
}
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_main_stage(&mut self, ui: &mut egui::Ui) {
self.show_game_section(ui);
self.show_session_section(ui);
self.show_network_section(ui);
}
fn show_settings_stage(&mut self, ctx: &egui::Context, ui: &mut egui::Ui) {
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| {
tools::build_menu(ui, &mut self.settings.theme);
fn show_settings_stage(&mut self, ui: &mut egui::Ui) {
ui.collapsing("General", |ui| {
ui.horizontal(|ui| {
let selection = self.settings.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 {
ui.set_theme(egui::Theme::from(self.settings.theme));
}
ui.label("Theme");
});
});
ui.collapsing("Game", |ui| {
ui.horizontal(|ui| {
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.label("Launch version");
});
});
ui.collapsing("Network", |ui| {
ui.horizontal(|ui| {
egui::ComboBox::from_id_salt("Block method")
.selected_text(self.settings.block_method.to_string())
.show_ui(ui, |ui| {
ui.build_menu(&mut self.settings.block_method);
});
ui.label("Block method");
if let Err(why) = self
.game_networking
.ensure_block_exclusivity(self.settings.block_method, &self.firewall)
{
log::warn!("Couldn't ensure block exclusivity: {why}");
}
});
if self.settings.block_method == BlockMethod::SaveServer {
ui.horizontal(|ui| {
ui.add(
egui::TextEdit::singleline(&mut self.settings.save_server_ip)
.char_limit(15)
.desired_width(100.0),
);
ui.label("Save server IP");
if self.settings.save_server_ip != Settings::default().save_server_ip {
if ui.button("").clicked() {
self.settings.save_server_ip = String::from(ROCKSTAR_SAVE_SERVER);
}
}
});
if selection != self.settings.theme {
catppuccin_egui::set_theme(ctx, self.settings.theme.into());
};
});
ui.collapsing("Miscellaneous", |ui| {
if ui.button("Open storage path").clicked() {
open::that_detached(path::APP_STORAGE.as_path()).unwrap();
}
});
ui.checkbox(&mut self.settings.start_elevated, "Always start elevated");
}
fn show_about_stage(&self, _ctx: &egui::Context, ui: &mut egui::Ui) {
fn show_about_stage(&self, ui: &mut egui::Ui) {
ui.with_layout(egui::Layout::bottom_up(egui::Align::Center), |ui| {
ui.horizontal(|ui| {
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 0.0;
ui.label("with ");
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.hyperlink_to("futile", "http://futile.eu");
ui.scope(|ui| {
ui.style_mut().visuals.hyperlink_color =
egui::Theme::from(self.settings.theme)
.default_style()
.visuals
.text_color();
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();
ui.style_mut().spacing.button_padding = egui::Vec2::new(4.0, 0.0);
if ui.button("").on_hover_text("View source code").clicked() {
open::that("https://futile.eu/gta-tools").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!(
@@ -249,10 +327,13 @@ impl Drop for App {
// save any persistent state to config file
PersistentState {
launcher: self.launch.selected,
anti_afk_enabled: self.anti_afk.enabled,
settings: self.settings.clone(),
}
.set();
// make sure we are not suspending game
features::empty_session::deactivate(&mut self.game_handle);
// make sure we are not network blocking game
if let Err(why) = features::empty_session::deactivate(&self.firewall) {
log::error!("couldn't deactivate empty session: {why}");
}
}
}
-22
View File
@@ -1,22 +0,0 @@
use crate::features::game_networking::BlockedStatus;
use eframe::egui;
pub const RED: egui::Color32 = egui::Color32::from_rgb(255, 96, 96);
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,
}
}
}
impl BlockedStatus {
pub fn to_color32(self) -> egui::Color32 {
self.into()
}
}
+52 -61
View File
@@ -2,15 +2,62 @@ use crate::{
gui::{
app::{App, WINDOW_SIZE},
tools,
ui_ext::UiExt,
},
util::consts::{
APP_STORAGE_PATH,
game::{EXE_ENHANCED, EXE_LEGACY},
},
util::consts::game::{EXE_ENHANCED, EXE_LEGACY},
};
use eframe::egui;
impl App {
fn add_debug_viewport_contents(&mut self, ui: &mut egui::Ui) {
ui.collapsing("misc", |ui| {
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");
egui::ComboBox::from_id_salt("blocked")
.selected_text(self.game_networking.blocked.to_string())
.show_ui(ui, |ui| ui.build_menu(&mut self.game_networking.blocked));
});
if ui.add(egui::Button::new("force refresh theme")).clicked() {
ui.set_theme(egui::Theme::from(self.settings.theme));
}
if ui.button("panic button").clicked() {
panic!("this is the panic button");
}
});
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()
@@ -30,63 +77,7 @@ impl App {
egui::CentralPanel::default().show(ctx, |ui| {
egui::ScrollArea::both()
.auto_shrink([false, true])
.show(ui, |ui| {
ui.collapsing("misc", |ui| {
if ui.button("open storage path").clicked() {
open::that_detached(APP_STORAGE_PATH.as_path()).unwrap();
}
ui.checkbox(
&mut self.meta.newer_version_available,
"spoof new version available",
)
.on_hover_text("(this could already be checked if\nthere actually IS a new version)");
ui.scope(|ui| {
use windows::Win32::UI::WindowsAndMessaging::{GetForegroundWindow, GetWindowTextW};
let mut buffer = [0; 512];
let current_title = unsafe {
let hwnd = GetForegroundWindow();
let length = GetWindowTextW(hwnd, &mut buffer);
String::from_utf16_lossy(&buffer[..length as usize])
};
ui.label(format!("focused: \"{current_title}\""));
});
ui.horizontal(|ui| {
ui.label("blocked_status");
egui::ComboBox::from_id_salt("blocked_status")
.selected_text(&self.game_networking.blocked_status.to_string())
.show_ui(ui, |ui| {
tools::build_menu(ui, &mut self.game_networking.blocked_status);
});
});
});
ui.collapsing("anti afk", |ui| {
ui.label(format!(
"timer: {}",
self.anti_afk.interval.elapsed().as_secs()
));
ui.label(format!(
"can activate: {}",
self.anti_afk.can_activate()
));
});
ui.collapsing("sysinfo", |ui| {
if ui.button("refresh all").clicked() {
self.sysinfo.refresh_all();
}
let pid = self
.sysinfo
.processes()
.iter()
.find(|(_, p)| p.name() == EXE_ENHANCED || p.name() == EXE_LEGACY)
.map_or_else(
|| "no pid found!".to_owned(),
|(pid, _)| pid.as_u32().to_string(),
);
ui.label(format!("gta pid: {pid}"));
});
ui.collapsing("app state", |ui| ui.label(format!("{self:#?}")));
});
.show(ui, |ui| self.add_debug_viewport_contents(ui));
});
},
);
+28 -33
View File
@@ -3,60 +3,55 @@ use crate::{
app::{App, WINDOW_SIZE},
tools,
},
util::{self, consts::APP_STORAGE_PATH, persistent_state::PersistentState},
util::persistent_state::PersistentState,
};
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();
}
use windows::Win32::System::Com::{COINIT_APARTMENTTHREADED, CoInitializeEx};
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 COM just in case
if let Err(e) = unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED) }.ok() {
log::error!("couldn't initialize COM: {e}");
}
// 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() {
app.launch.selected = persistent_state.launcher;
app.settings = persistent_state.settings;
persistent_state.apply_to(&mut app);
}
// check if we're elevated. if not, and the user wants an elevated launch - relaunch elevated
if !app.flags.elevated && app.settings.start_elevated {
util::win::elevate(util::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
catppuccin_egui::set_theme(&cc.egui_ctx, app.settings.theme.into());
// set our initial theme, from earlier loaded settings. we set the egui theme here to work
// around system theme based switching of the egui style
cc.egui_ctx.set_theme(egui::Theme::from(app.settings.theme));
// apply some global styling that we like
cc.egui_ctx.style_mut(|style| {
cc.egui_ctx.all_styles_mut(|style| {
style.spacing.item_spacing = egui::vec2(4.0, 4.0);
style.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();
fonts.font_data.insert(
"Ubuntu-Regular".to_owned(),
egui::FontData::from_static(include_bytes!("../../assets/Ubuntu-Regular.ttf")).into(),
"Inter 18pt Light".to_owned(),
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(
egui::FontFamily::Name("Ubuntu-Regular".into()),
vec!["Ubuntu-Regular".to_owned()],
egui::FontFamily::Name("Inter 18pt Regular".into()),
vec!["Inter 18pt Regular".to_owned()],
);
cc.egui_ctx.set_fonts(fonts);
// finally return the App
+45 -17
View File
@@ -1,40 +1,68 @@
use crate::util::win;
use eframe::egui;
use serde::{Deserialize, Serialize};
use strum::{Display, EnumIter};
#[derive(Clone, Copy, Debug, Display, PartialEq, Eq, Serialize, Deserialize, EnumIter)]
pub const ROCKSTAR_SAVE_SERVER: &str = "192.81.241.171";
#[derive(Clone, Copy, Debug, Default, Display, PartialEq, Eq, Serialize, Deserialize, EnumIter)]
pub enum Theme {
#[strum(to_string = "Catppuccin Latte")]
CatppuccinLatte,
#[strum(to_string = "Catppuccin Frappe")]
CatppuccinFrappe,
#[strum(to_string = "Catppuccin Macchiato")]
CatppuccinMacchiato,
#[strum(to_string = "Catppuccin Mocha")]
CatppuccinMocha,
#[default]
#[strum(to_string = "Auto")]
Auto,
#[strum(to_string = "Light")]
Light,
#[strum(to_string = "Dark")]
Dark,
}
impl From<Theme> for catppuccin_egui::Theme {
impl From<Theme> for egui::Theme {
fn from(val: Theme) -> Self {
match val {
Theme::CatppuccinLatte => catppuccin_egui::LATTE,
Theme::CatppuccinFrappe => catppuccin_egui::FRAPPE,
Theme::CatppuccinMacchiato => catppuccin_egui::MACCHIATO,
Theme::CatppuccinMocha => catppuccin_egui::MOCHA,
Theme::Auto => {
if win::is_system_theme_light() {
egui::Theme::Light
} else {
egui::Theme::Dark
}
}
Theme::Light => egui::Theme::Light,
Theme::Dark => egui::Theme::Dark,
}
}
}
#[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 BlockMethod {
#[default]
#[strum(to_string = "Entire game")]
EntireGame,
#[strum(to_string = "Save server")]
SaveServer,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Settings {
pub theme: Theme,
pub start_elevated: bool,
pub launch_version: LaunchVersion,
pub save_server_ip: String,
pub block_method: BlockMethod,
}
impl Default for Settings {
fn default() -> Self {
Self {
theme: Theme::CatppuccinMocha,
start_elevated: false,
theme: Theme::default(),
launch_version: LaunchVersion::default(),
block_method: BlockMethod::default(),
save_server_ip: String::from(ROCKSTAR_SAVE_SERVER),
}
}
}
+4 -23
View File
@@ -11,31 +11,12 @@ pub fn load_icon() -> egui::IconData {
}
}
pub fn header(ui: &mut egui::Ui, text: &str) {
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 debug_keycombo_pressed(ui: &egui::Ui) -> bool {
ui.input(|i| i.modifiers.all() && i.key_pressed(egui::Key::D))
}
pub fn build_menu<E>(ui: &mut egui::Ui, current_value: &mut E)
where
E: strum::IntoEnumIterator + std::fmt::Display + std::cmp::PartialEq + Copy,
{
E::iter().for_each(|variant| {
ui.selectable_value(current_value, variant, variant.to_string());
});
}
pub fn debug_keycombo_pressed(ctx: &egui::Context) -> bool {
ctx.input(|i| i.modifiers.all() && i.key_pressed(egui::Key::D))
}
pub fn debug_viewport_close_pressed(ctx: &egui::Context) -> bool {
ctx.input(|i| {
pub fn debug_viewport_close_pressed(ui: &egui::Ui) -> bool {
ui.input(|i| {
i.raw
.viewports
.get(&egui::ViewportId::from_hash_of("debug_viewport"))
+38
View File
@@ -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),
)
}
}
+20
View File
@@ -4,6 +4,26 @@ mod features;
mod gui;
mod util;
use std::fmt::Write;
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 {
write!(message, "\nstack backtrace:\n{backtrace}").unwrap();
}
log::error!("{message}");
}
fn main() {
init_storage();
util::logging::Logger::init(log::LevelFilter::Info);
std::panic::set_hook(Box::new(panic_hook));
gui::run::run();
}
+3 -2
View File
@@ -1,6 +1,7 @@
mod codeberg;
pub mod consts;
pub mod countdown;
pub mod meta;
pub mod firewall;
pub mod logging;
pub mod persistent_state;
pub mod system_info;
pub mod win;
-35
View File
@@ -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),
})
}
+16 -4
View File
@@ -1,10 +1,22 @@
use std::{path::PathBuf, sync::LazyLock};
pub static APP_STORAGE_PATH: LazyLock<PathBuf> =
LazyLock::new(|| dirs::config_local_dir().unwrap().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";
pub const EXE_LEGACY: &str = "GTA5.exe";
pub const WINDOW_TITLE: &str = "Grand Theft Auto V";
}
pub mod colours {
use eframe::egui;
pub const RED: egui::Color32 = egui::Color32::from_rgb(255, 96, 96);
pub const GREEN: egui::Color32 = egui::Color32::from_rgb(96, 255, 96);
}
+96
View File
@@ -0,0 +1,96 @@
use anyhow::Result;
use std::path::PathBuf;
use windows::{
Win32::{
NetworkManagement::WindowsFirewall::{
INetFwPolicy2, INetFwRule, NET_FW_ACTION_BLOCK, NET_FW_IP_PROTOCOL_ANY,
NET_FW_IP_PROTOCOL_TCP, NET_FW_IP_PROTOCOL_UDP, NET_FW_RULE_DIR_IN,
NET_FW_RULE_DIR_OUT, NetFwPolicy2, NetFwRule,
},
System::Com::{CLSCTX_INPROC_SERVER, CoCreateInstance},
},
core::BSTR,
};
#[derive(Debug)]
pub struct Firewall {
policy: INetFwPolicy2,
}
impl Default for Firewall {
fn default() -> Self {
Self {
policy: unsafe { CoCreateInstance(&NetFwPolicy2, None, CLSCTX_INPROC_SERVER) }.unwrap(),
}
}
}
impl Firewall {
pub fn add(
&self,
name: &str,
mode: RuleMode,
direction: RuleDirection,
protocol: RuleProtocol,
) -> Result<()> {
let add_rule = || {
let rules = unsafe { self.policy.Rules() }?;
unsafe { rules.Remove(&BSTR::from(name)) }?;
let rule: INetFwRule =
unsafe { CoCreateInstance(&NetFwRule, None, CLSCTX_INPROC_SERVER) }?;
unsafe { rule.SetName(&BSTR::from(name)) }?;
match mode {
RuleMode::Executable(exe) => {
unsafe { rule.SetApplicationName(&BSTR::from(exe.to_string_lossy().as_ref())) }?
}
RuleMode::Address(ip) => unsafe { rule.SetRemoteAddresses(&BSTR::from(ip)) }?,
}
match direction {
RuleDirection::In => unsafe { rule.SetDirection(NET_FW_RULE_DIR_IN) }?,
RuleDirection::Out => unsafe { rule.SetDirection(NET_FW_RULE_DIR_OUT) }?,
}
unsafe { rule.SetEnabled(true.into()) }?;
unsafe { rule.SetAction(NET_FW_ACTION_BLOCK) }?;
match protocol {
RuleProtocol::Any => unsafe { rule.SetProtocol(NET_FW_IP_PROTOCOL_ANY.0) }?,
RuleProtocol::Tcp => unsafe { rule.SetProtocol(NET_FW_IP_PROTOCOL_TCP.0) }?,
RuleProtocol::Udp => unsafe { rule.SetProtocol(NET_FW_IP_PROTOCOL_UDP.0) }?,
}
unsafe { rules.Add(&rule) }?;
Ok(())
};
add_rule().inspect_err(|e| log::warn!("Failed to add rule '{name}': {e}"))
}
pub fn remove(&self, name: &str) -> Result<()> {
let remove_rule = || {
let rules = unsafe { self.policy.Rules() }?;
unsafe { rules.Remove(&BSTR::from(name)) }?;
Ok(())
};
remove_rule().inspect_err(|e| log::warn!("Failed to remove rule '{name}': {e}"))
}
pub fn is_blocked(&self, name: &str) -> Result<bool> {
let rules = unsafe { self.policy.Rules() }?;
let rule_exists = unsafe { rules.Item(&BSTR::from(name)) }.is_ok();
Ok(rule_exists)
}
}
pub enum RuleMode {
Executable(PathBuf),
Address(String),
}
pub enum RuleDirection {
In,
Out,
}
pub enum RuleProtocol {
Any,
#[allow(unused)]
Tcp,
Udp,
}
+64
View File
@@ -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();
writeln!(
file,
"[{}][{}] {}",
humantime::format_rfc3339_seconds(SystemTime::now()),
record.level(),
record.args()
)
.unwrap();
file.flush().unwrap();
}
fn log_to_stderr(record: &log::Record) {
eprintln!(
"[{}][{}] {}",
humantime::format_rfc3339_seconds(SystemTime::now()),
record.level(),
record.args()
);
}
}
-25
View File
@@ -1,25 +0,0 @@
use crate::util::codeberg;
use semver::Version;
#[derive(Debug)]
pub struct Meta {
pub current_version: Version,
pub latest_release: codeberg::Release,
pub newer_version_available: bool,
}
impl Default for Meta {
fn default() -> Self {
let current_version = Version::parse(env!("CARGO_PKG_VERSION")).unwrap();
let latest_release = codeberg::get_latest_release().unwrap_or_default();
let newer_version_available = matches!(
&current_version.cmp_precedence(&latest_release.version),
std::cmp::Ordering::Less
);
Self {
current_version,
latest_release,
newer_version_available,
}
}
}
+19 -12
View File
@@ -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 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,
pub anti_afk_enabled: bool,
pub settings: Settings,
}
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();
}
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;
}
}
+127
View File
@@ -0,0 +1,127 @@
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,
};
use crate::util::consts::game::{EXE_ENHANCED, EXE_LEGACY};
#[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 get_game_exe_path(&mut self) -> Option<&Path> {
self.refresh();
self.processes()
.iter()
.find(|p| p.name() == EXE_ENHANCED || p.name() == EXE_LEGACY)
.and_then(|p| p.exe())
}
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])
}
+24 -65
View File
@@ -1,85 +1,44 @@
use windows::{
Win32::{
Foundation::{CloseHandle, HANDLE},
Security::{GetTokenInformation, TOKEN_ELEVATION, TOKEN_QUERY, TokenElevation},
System::Threading::{GetCurrentProcess, OpenProcessToken},
UI::{
Input::KeyboardAndMouse::{GetAsyncKeyState, VIRTUAL_KEY},
Shell::ShellExecuteW,
WindowsAndMessaging::{
CURSOR_SHOWING, CURSORINFO, GetCursorInfo, GetForegroundWindow, GetWindowTextW,
SW_NORMAL,
},
},
use windows::Win32::UI::{
Input::KeyboardAndMouse::{GetAsyncKeyState, VIRTUAL_KEY},
WindowsAndMessaging::{
CURSOR_SHOWING, CURSORINFO, GetCursorInfo, GetForegroundWindow, GetWindowTextW,
},
core::{HSTRING, PCWSTR},
};
pub enum ElevationExitMethod<'a> {
Gentle(&'a mut bool),
Forced,
}
pub fn is_cursor_visible() -> bool {
let mut ci = CURSORINFO {
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 current_title = String::from_utf16_lossy(&buffer[..length as usize]);
current_title == target_title
}
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) {
let exe = std::env::current_exe().unwrap();
unsafe {
ShellExecuteW(
None,
&HSTRING::from("runas"),
&HSTRING::from(exe.as_path()),
PCWSTR::null(),
PCWSTR::null(),
SW_NORMAL,
);
}
match closing {
ElevationExitMethod::Gentle(closing) => *closing = true,
ElevationExitMethod::Forced => std::process::exit(0),
}
}
pub fn is_elevated() -> bool {
let mut token: HANDLE = HANDLE::default();
unsafe {
if OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &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(
token,
TokenElevation,
Some((&raw mut elevation).cast()),
size,
&mut size,
);
CloseHandle(token).unwrap();
result.is_ok() && elevation.TokenIsElevated != 0
}
pub fn is_system_theme_light() -> 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(apps_use_light_theme): Result<u32, std::io::Error> =
subkey.get_value("AppsUseLightTheme")
else {
return true;
};
apps_use_light_theme == 1
}