55 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
22 changed files with 1332 additions and 1268 deletions
-31
View File
@@ -1,31 +0,0 @@
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
@@ -1,43 +0,0 @@
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
Generated
+903 -671
View File
File diff suppressed because it is too large Load Diff
+14 -18
View File
@@ -1,42 +1,38 @@
[package]
name = "gta-tools"
version = "0.11.0"
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.6.0", default-features = false, features = [
"egui32",
] }
eframe = { version = "0.32.2", default-features = false, features = [
anyhow = "1.0"
eframe = { version = "0.35", default-features = false, features = [
"glow",
"default_fonts",
] }
egui_extras = { version = "0.32.2", default-features = false, features = [
egui_extras = { version = "0.35", 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"
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.143"
strum = { version = "0.27.2", features = ["derive"] }
windows = { version = "0.62.0", features = [
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_Security",
"Win32_System_Com",
"Win32_System_Diagnostics_ToolHelp",
"Win32_System_Threading",
"Win32_UI_Input_KeyboardAndMouse",
"Win32_UI_Shell",
"Win32_UI_WindowsAndMessaging",
] }
winreg = "0.55.0"
winreg = "0.56"
[build-dependencies]
static_vcruntime = "2.0.0"
tauri-winres = "0.3.3"
static_vcruntime = "3.0.0"
tauri-winres = "0.3"
+34 -22
View File
@@ -2,68 +2,80 @@
A toolset of convenient things for GTA V Online.
<picture>
<source srcset="https://i.vgy.me/xt8EfK.png" media="(prefers-color-scheme: dark)">
<img src="https://i.vgy.me/lYlZnd.png">
</picture>
![](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://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.
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 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**.
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.
> [!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 is Legacy/Enhanced-agnostic. Some functionality of GTA Tools requires administrator access. If necessary, GTA Tools can either be started as admin manually, or, the user can simply use the <kbd>Elevate</kbd> button to relaunch GTA Tools as admin.
Every feature of GTA Tools works for both the Legacy and Enhanced versions of the game. GTA Tools requires administrator access.
#### 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.
<kbd>Launch</kbd> — Starts 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`.
<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.
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:
<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
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:
<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[^1]
* The mouse cursor is not currently visible[^2]
* None of the designated keys are currently being pressed[^2]
* The mouse cursor is not currently visible[^3]
#### 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
<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>
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
</details>
## Issues
<details>
<summary>How to do a replay glitch (save server method)</summary>
- It is possible that when not elevated (administrator), the <kbd>Force close game</kbd> and <kbd>Empty current session</kbd> features can fail due to being denied access to the game. This is not guaranteed to happen. If this does happen to you, I recommend always using GTA Tools in elevated mode. For convenience, you may check "Always start elevated" in the Settings tab.
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]: 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.
[^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.
-13
View File
@@ -1,13 +0,0 @@
### <ins>todo</ins>
- rewrite entire codebase
- add global hotkeys
### <ins>doing</ins>
- better error reporting
### <ins>done</ins>
- change `is_system_theme_dark` to `is_system_theme_light`
- fix weird changing colour on futile link in about
+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

+11
View File
@@ -6,6 +6,17 @@ fn main() {
.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();
+30 -39
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 windows::Win32::{
Foundation::{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,52 +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;
}
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;
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);
};
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
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)
}
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) };
}
pub fn deactivate(firewall: &Firewall) -> Result<()> {
firewall.remove(FILTER_NAME_EMPTY_SESSION_IN)?;
firewall.remove(FILTER_NAME_EMPTY_SESSION_OUT)?;
Ok(())
}
+51 -159
View File
@@ -1,208 +1,100 @@
use crate::{
gui::settings::BlockMethod,
util::{
consts::game::{EXE_ENHANCED, EXE_LEGACY},
firewall::{Firewall, RuleDirection, RuleMode, RuleProtocol},
system_info::SystemInfo,
},
};
use std::{
error::Error,
path::{Path, PathBuf},
time::{Duration, Instant},
};
use anyhow::Result;
use strum::{Display, EnumIter};
use windows::{
Win32::{
NetworkManagement::WindowsFirewall::{
INetFwPolicy2, INetFwRule, NET_FW_ACTION_BLOCK, NET_FW_IP_PROTOCOL_ANY,
NET_FW_RULE_DIR_OUT, NetFwPolicy2, NetFwRule,
},
System::Com::{
CLSCTX_INPROC_SERVER, COINIT_MULTITHREADED, CoCreateInstance, CoInitializeEx,
CoUninitialize,
},
},
core::BSTR,
};
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: if Self::is_save_server_blocked().unwrap() {
Self::is_save_server_blocked().unwrap().into()
blocked: if firewall.is_blocked(FILTER_NAME_SAVE_SERVER).unwrap() {
BlockedStatus::Server
} else if firewall.is_blocked(FILTER_NAME_EXE).unwrap() {
BlockedStatus::Executable
} else {
Self::is_exe_blocked().unwrap().into()
BlockedStatus::Unblocked
},
com_initialized: unsafe { CoInitializeEx(None, COINIT_MULTITHREADED) }.is_ok(),
timer: Instant::now(),
counting: false,
}
}
}
impl Drop for GameNetworking {
fn drop(&mut self) {
if self.com_initialized {
unsafe { CoUninitialize() };
}
}
}
enum Mode {
EntireGame(PathBuf),
SaveServer(String),
}
impl GameNetworking {
fn block_generic(&self, mode: Mode) -> Result<(), Box<dyn Error>> {
let policy: INetFwPolicy2 =
unsafe { CoCreateInstance(&NetFwPolicy2, None, CLSCTX_INPROC_SERVER) }?;
let rules = unsafe { policy.Rules() }?;
let filter_name = match mode {
Mode::EntireGame(_) => FILTER_NAME_EXE,
Mode::SaveServer(_) => FILTER_NAME_SAVE_SERVER,
};
unsafe { rules.Remove(&BSTR::from(filter_name)) }?;
let rule: INetFwRule = unsafe { CoCreateInstance(&NetFwRule, None, CLSCTX_INPROC_SERVER) }?;
unsafe { rule.SetName(&BSTR::from(filter_name)) }?;
match mode {
Mode::EntireGame(exe_path) => {
let exe_path = BSTR::from(exe_path.to_string_lossy().to_string());
unsafe { rule.SetApplicationName(&exe_path) }?;
}
Mode::SaveServer(save_server_ip) => {
unsafe { rule.SetRemoteAddresses(&BSTR::from(save_server_ip)) }?;
}
}
unsafe { rule.SetDirection(NET_FW_RULE_DIR_OUT) }?;
unsafe { rule.SetEnabled(true.into()) }?;
unsafe { rule.SetAction(NET_FW_ACTION_BLOCK) }?;
unsafe { rule.SetProtocol(NET_FW_IP_PROTOCOL_ANY.0) }?;
unsafe { rules.Add(&rule) }?;
Ok(())
}
fn unblock_generic(&self, filter_name: &str) -> Result<(), Box<dyn Error>> {
let policy: INetFwPolicy2 =
unsafe { CoCreateInstance(&NetFwPolicy2, None, CLSCTX_INPROC_SERVER) }?;
let rules = unsafe { policy.Rules() }?;
unsafe { rules.Remove(&BSTR::from(filter_name)) }?;
Ok(())
}
fn is_blocked_generic(filter_name: &str) -> Result<bool, Box<dyn Error>> {
let policy: INetFwPolicy2 =
unsafe { CoCreateInstance(&NetFwPolicy2, None, CLSCTX_INPROC_SERVER) }?;
let rules = unsafe { policy.Rules() }?;
let rule_exists = unsafe { rules.Item(&BSTR::from(filter_name)) }.is_ok();
Ok(rule_exists)
}
pub fn block_exe(&mut self, system_info: &mut SystemInfo) -> Result<(), Box<dyn Error>> {
let Some(exe_path) = get_game_exe_path(system_info) else {
self.blocked_status = BlockedStatus::Failed;
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(());
};
self.block_generic(Mode::EntireGame(exe_path.to_path_buf()))?;
self.blocked_status = Self::is_exe_blocked()?.into();
Ok(())
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) -> Result<(), Box<dyn Error>> {
self.unblock_generic(FILTER_NAME_EXE)?;
self.blocked_status = Self::is_exe_blocked()?.into();
Ok(())
pub fn unblock_exe(&mut self, firewall: &Firewall) -> Result<()> {
firewall
.remove(FILTER_NAME_EXE)
.inspect(|_| self.blocked = BlockedStatus::Unblocked)
}
fn is_exe_blocked() -> Result<bool, Box<dyn Error>> {
Self::is_blocked_generic(FILTER_NAME_EXE)
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 block_save_server(&mut self, save_server_ip: &str) -> Result<(), Box<dyn Error>> {
self.block_generic(Mode::SaveServer(save_server_ip.to_owned()))?;
self.blocked_status = Self::is_save_server_blocked()?.into();
Ok(())
pub fn unblock_save_server(&mut self, firewall: &Firewall) -> Result<()> {
firewall
.remove(FILTER_NAME_SAVE_SERVER)
.inspect(|_| self.blocked = BlockedStatus::Unblocked)
}
pub fn unblock_save_server(&mut self) -> Result<(), Box<dyn Error>> {
self.unblock_generic(FILTER_NAME_SAVE_SERVER)?;
self.blocked_status = Self::is_save_server_blocked()?.into();
Ok(())
}
pub fn is_save_server_blocked() -> Result<bool, Box<dyn Error>> {
Self::is_blocked_generic(FILTER_NAME_SAVE_SERVER)
}
pub fn reset_indicator_if_failed(&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_exe_blocked().unwrap().into();
}
}
pub fn ensure_not_both_blocked_simultaneously(&mut self, block_method: BlockMethod) {
pub fn ensure_block_exclusivity(
&mut self,
block_method: BlockMethod,
firewall: &Firewall,
) -> Result<()> {
match block_method {
BlockMethod::EntireGame => {
if Self::is_save_server_blocked().unwrap() {
// ignoring the return because if this is an error the user can just thug it out at that point
let _ = self.unblock_save_server();
self.blocked_status = Self::is_exe_blocked().unwrap().into();
if self.blocked == BlockedStatus::Server {
self.unblock_save_server(firewall)?;
}
}
BlockMethod::SaveServer => {
if Self::is_exe_blocked().unwrap() {
// ignoring the return because if this is an error the user can just thug it out at that point
let _ = self.unblock_exe();
self.blocked_status = Self::is_save_server_blocked().unwrap().into();
if self.blocked == BlockedStatus::Executable {
self.unblock_exe(firewall)?;
}
}
}
Ok(())
}
}
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())
}
+7 -6
View File
@@ -34,18 +34,19 @@ pub fn launch(platform: &Platform, version: &LaunchVersion) {
LaunchVersion::Enhanced => r"SOFTWARE\WOW6432Node\Rockstar Games\GTAV Enhanced",
LaunchVersion::Legacy => r"SOFTWARE\WOW6432Node\Rockstar Games\Grand Theft Auto V",
};
let Ok(gta_v_enhanced) = hklm.open_subkey(rockstar_url) else {
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");
// ignoring the return because if it errors that means GTA isn't installed via Rockstar
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 epic_url = match version {
+132 -124
View File
@@ -1,12 +1,13 @@
use crate::{
features,
features::{self, game_networking::BlockedStatus},
gui::{
settings::{BlockMethod, ROCKSTAR_SAVE_SERVER, Settings},
tools,
ui_ext::UiExt,
},
util::{
consts::{colours, game::WINDOW_TITLE},
consts::{colours, game::WINDOW_TITLE, path},
firewall::Firewall,
persistent_state::PersistentState,
system_info::SystemInfo,
win,
@@ -28,7 +29,6 @@ enum Stage {
#[derive(Debug)]
pub struct Flags {
pub elevated: bool,
pub debug: bool,
closing: bool,
}
@@ -36,7 +36,6 @@ pub struct Flags {
impl Default for Flags {
fn default() -> Self {
Self {
elevated: win::is_elevated(),
debug: false,
closing: false,
}
@@ -49,7 +48,7 @@ pub struct App {
stage: Stage,
pub flags: Flags,
pub system_info: SystemInfo,
game_handle: windows::Win32::Foundation::HANDLE,
firewall: Firewall,
pub anti_afk: features::anti_afk::AntiAfk,
empty_session: features::empty_session::EmptySession,
force_close: features::force_close::ForceClose,
@@ -58,48 +57,39 @@ 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| {
ui.build_menu(&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));
}
});
});
});
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) {
fn show_game_section(&mut self, ui: &mut egui::Ui) {
ui.header("Game");
ui.horizontal(|ui| {
if ui.button("Launch").clicked() {
@@ -112,23 +102,18 @@ impl App {
ui.build_menu(&mut self.launch.selected);
});
});
let force_close_button = ui.add_sized(
[108.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.system_info);
}
fn show_session_section(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) {
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()
&& features::empty_session::activate(
&mut self.game_handle,
&mut self.system_info,
)
&& features::empty_session::activate(&mut self.system_info, &self.firewall)
.unwrap()
{
self.empty_session.interval = Instant::now();
self.empty_session.disabled = true;
@@ -143,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!"
})
});
}
@@ -155,72 +140,86 @@ impl App {
}
}
fn show_network_section(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) {
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| {
ui.add_enabled_ui(self.flags.elevated, |ui| {
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(self.game_networking.blocked_status);
self.game_networking.reset_indicator_if_failed();
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 =>
{
match self.settings.block_method {
BlockMethod::EntireGame => {
self.game_networking
.block_exe(&mut self.system_info)
.unwrap();
}
BlockMethod::SaveServer => {
self.game_networking
.block_save_server(&self.settings.save_server_ip)
.unwrap();
}
}
colours::RED
}
if ui
.add_sized([button_width, 18.0], egui::Button::new("Unblock"))
.clicked()
BlockedStatus::Server
if self.settings.block_method == BlockMethod::SaveServer =>
{
match self.settings.block_method {
BlockMethod::EntireGame => {
self.game_networking.unblock_exe().unwrap();
}
BlockMethod::SaveServer => {
self.game_networking.unblock_save_server().unwrap();
}
}
colours::RED
}
_ => colours::GREEN,
});
})
.response
.on_disabled_hover_text("This requires administrator.\nUse the Elevate button.");
label
});
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) {
fn show_settings_stage(&mut self, ui: &mut egui::Ui) {
ui.collapsing("General", |ui| {
ui.checkbox(&mut self.settings.start_elevated, "Always start elevated");
ui.horizontal(|ui| {
let selection = self.settings.theme;
egui::ComboBox::from_id_salt("Theme")
@@ -229,7 +228,7 @@ impl App {
ui.build_menu(&mut self.settings.theme);
});
if selection != self.settings.theme {
catppuccin_egui::set_theme(ctx, self.settings.theme.into());
ui.set_theme(egui::Theme::from(self.settings.theme));
}
ui.label("Theme");
});
@@ -245,40 +244,44 @@ impl App {
});
});
ui.collapsing("Network", |ui| {
ui.add_enabled_ui(self.flags.elevated, |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| {
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");
self.game_networking
.ensure_not_both_blocked_simultaneously(self.settings.block_method);
});
ui.horizontal(|ui| {
ui.add_enabled_ui(
self.settings.block_method == BlockMethod::SaveServer,
|ui| {
ui.add(
egui::TextEdit::singleline(&mut self.settings.save_server_ip)
.char_limit(15)
.desired_width(92.0),
);
ui.label("Save server IP");
if ui.button("").clicked() {
self.settings.save_server_ip = String::from(ROCKSTAR_SAVE_SERVER);
}
},
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);
}
}
});
})
.response
.on_disabled_hover_text("This requires administrator.\nUse the Elevate button.");
};
});
ui.collapsing("Miscellaneous", |ui| {
if ui.button("Open storage path").clicked() {
open::that_detached(path::APP_STORAGE.as_path()).unwrap();
}
});
}
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| {
@@ -291,14 +294,17 @@ impl App {
ui.label(" from ");
ui.scope(|ui| {
ui.style_mut().visuals.hyperlink_color =
catppuccin_egui::Theme::from(self.settings.theme).text;
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.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://github.com/elituf/gta-tools").unwrap();
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)");
@@ -325,7 +331,9 @@ impl Drop for App {
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}");
}
}
}
+9 -14
View File
@@ -4,19 +4,13 @@ use crate::{
tools,
ui_ext::UiExt,
},
util::consts::{
game::{EXE_ENHANCED, EXE_LEGACY},
path,
},
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| {
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,
@@ -30,15 +24,16 @@ impl App {
ui.label(format!("focused: \"{current_title}\""));
});
ui.horizontal(|ui| {
ui.label("blocked_status");
egui::ComboBox::from_id_salt("blocked_status")
.selected_text(self.game_networking.blocked_status.to_string())
.show_ui(ui, |ui| {
ui.build_menu(&mut self.game_networking.blocked_status);
});
ui.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() {
catppuccin_egui::set_theme(ui.ctx(), self.settings.theme.into());
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| {
+10 -10
View File
@@ -3,36 +3,36 @@ use crate::{
app::{App, WINDOW_SIZE},
tools,
},
util::{persistent_state::PersistentState, win},
util::persistent_state::PersistentState,
};
use eframe::egui;
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>> {
// 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::new(App::default());
// load previously selected launch platform & settings from persistent state
if let Some(persistent_state) = PersistentState::get() {
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 {
win::elevate(win::ElevationExitMethod::Forced);
}
// refresh system info because it initializes with nothing
app.system_info.refresh();
// enable image loading support in egui
egui_extras::install_image_loaders(&cc.egui_ctx);
// set our initial theme, from earlier loaded settings. we set the egui theme
// 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());
// 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.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;
// 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();
+10 -17
View File
@@ -1,4 +1,5 @@
use crate::util::win;
use eframe::egui;
use serde::{Deserialize, Serialize};
use strum::{Display, EnumIter};
@@ -9,30 +10,24 @@ pub enum Theme {
#[default]
#[strum(to_string = "Auto")]
Auto,
#[strum(to_string = "Latte")]
Latte,
#[strum(to_string = "Frappe")]
Frappe,
#[strum(to_string = "Macchiato")]
Macchiato,
#[strum(to_string = "Mocha")]
Mocha,
#[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::Auto => {
if win::is_system_theme_light() {
catppuccin_egui::LATTE
egui::Theme::Light
} else {
catppuccin_egui::MOCHA
egui::Theme::Dark
}
}
Theme::Latte => catppuccin_egui::LATTE,
Theme::Frappe => catppuccin_egui::FRAPPE,
Theme::Macchiato => catppuccin_egui::MACCHIATO,
Theme::Mocha => catppuccin_egui::MOCHA,
Theme::Light => egui::Theme::Light,
Theme::Dark => egui::Theme::Dark,
}
}
}
@@ -55,7 +50,6 @@ pub enum BlockMethod {
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Settings {
pub start_elevated: bool,
pub theme: Theme,
pub launch_version: LaunchVersion,
pub save_server_ip: String,
@@ -65,7 +59,6 @@ pub struct Settings {
impl Default for Settings {
fn default() -> Self {
Self {
start_elevated: false,
theme: Theme::default(),
launch_version: LaunchVersion::default(),
block_method: BlockMethod::default(),
+4 -4
View File
@@ -11,12 +11,12 @@ pub fn load_icon() -> egui::IconData {
}
}
pub fn debug_keycombo_pressed(ctx: &egui::Context) -> bool {
ctx.input(|i| i.modifiers.all() && i.key_pressed(egui::Key::D))
pub fn debug_keycombo_pressed(ui: &egui::Ui) -> bool {
ui.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"))
+1
View File
@@ -1,5 +1,6 @@
pub mod consts;
pub mod countdown;
pub mod firewall;
pub mod logging;
pub mod persistent_state;
pub mod system_info;
-12
View File
@@ -15,20 +15,8 @@ pub mod game {
}
pub mod colours {
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,
}
}
}
}
+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,
}
+5 -5
View File
@@ -27,7 +27,7 @@ impl log::Log for Logger {
fn log(&self, record: &log::Record) {
if self.enabled(record.metadata()) {
self.log_to_stderr(record);
Self::log_to_stderr(record);
self.log_to_file(record);
}
}
@@ -42,9 +42,9 @@ impl Logger {
fn log_to_file(&self, record: &log::Record) {
let mut file = self.file.lock().unwrap();
write!(
writeln!(
file,
"[{}][{}]\n{}\n\n",
"[{}][{}] {}",
humantime::format_rfc3339_seconds(SystemTime::now()),
record.level(),
record.args()
@@ -53,9 +53,9 @@ impl Logger {
file.flush().unwrap();
}
fn log_to_stderr(&self, record: &log::Record) {
fn log_to_stderr(record: &log::Record) {
eprintln!(
"[{}][{}]\n{}",
"[{}][{}] {}",
humantime::format_rfc3339_seconds(SystemTime::now()),
record.level(),
record.args()
+10
View File
@@ -18,6 +18,8 @@ use windows::{
core::PWSTR,
};
use crate::util::consts::game::{EXE_ENHANCED, EXE_LEGACY};
#[derive(Clone, Debug)]
pub struct Process {
pid: u32,
@@ -80,6 +82,14 @@ impl SystemInfo {
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
}
+4 -57
View File
@@ -1,25 +1,10 @@
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(),
@@ -42,44 +27,6 @@ pub fn is_any_key_pressed(keys: &[VIRTUAL_KEY]) -> bool {
.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();
if unsafe { OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &raw mut token) }.is_err() {
return false;
}
let mut elevation = TOKEN_ELEVATION::default();
let mut size = u32::try_from(std::mem::size_of::<TOKEN_ELEVATION>()).unwrap();
let result = unsafe {
GetTokenInformation(
token,
TokenElevation,
Some((&raw mut elevation).cast()),
size,
&raw mut size,
)
};
unsafe { 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);