12 Commits
14 changed files with 182 additions and 110 deletions
+31
View File
@@ -0,0 +1,31 @@
name: Create release for pushed tag
on:
push:
tags:
- "*"
jobs:
release-on-tag:
runs-on: windows-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
- name: Create release
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ github.ref_name }}
run: |
if gh release view "$TAG" >/dev/null 2>&1; then
echo "A release already exists for $TAG, skipping"
else
cargo build --release
prev_tag=$(gh release list --limit 100 --json tagName --jq '.[0].tagName')
notes="**Full Changelog**: https://github.com/${{ github.repository }}/compare/${prev_tag}...${TAG}"
gh release create "$TAG" target/release/gta-tools.exe \
--title "$TAG" \
--notes "$notes"
fi
@@ -0,0 +1,43 @@
name: Create releases for existing tags
on:
workflow_dispatch:
jobs:
release-retroactively:
runs-on: windows-latest
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Get tags and create release for each
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mapfile -t tags < <(git tag -l)
echo "${#tags[@]} tags found."
prev_tag=""
for tag in "${tags[@]}"; do
if gh release view "$tag" >/dev/null 2>&1; then
echo "A release already exists for $tag, skipping"
else
echo "Proceeding with tag $tag"
git checkout "$tag"
cargo build --release
if [ -n "$prev_tag" ]; then
notes="**Full Changelog**: https://github.com/${{ github.repository }}/compare/${prev_tag}...${tag}"
else
notes="**Full Changelog**: https://github.com/${{ github.repository }}/commits/${tag}"
fi
gh release create "$tag" target/release/gta-tools.exe \
--title "$tag" \
--notes "$notes"
fi
prev_tag="$tag"
done
Generated
+1 -1
View File
@@ -825,7 +825,7 @@ dependencies = [
[[package]]
name = "gta-tools"
version = "0.8.0"
version = "0.9.0"
dependencies = [
"catppuccin-egui",
"chrono",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "gta-tools"
version = "0.8.0"
version = "0.9.0"
edition = "2024"
[dependencies]
+7 -4
View File
@@ -2,12 +2,15 @@
A toolset of convenient things for GTA V Online.
![](https://i.vgy.me/9j6ZSO.png) ![](https://i.vgy.me/xJgskM.png)
<picture>
<source srcset="https://i.vgy.me/M4sOHh.png" media="(prefers-color-scheme: dark)">
<img src="https://i.vgy.me/mpO9uc.png">
</picture>
## Installing
**Option 1** — <ins>Download</ins>
Download the latest release [here](https://codeberg.org/futile/gta-tools/releases/download/latest/gta-tools.exe) and place it somewhere convenient for you, such as Documents. You could then make a shortcut titled "GTA Tools", and pin it to taskbar or Start.
Download the latest release [here](https://github.com/elituf/gta-tools/releases/latest/download/gta-tools.exe) and place it somewhere convenient for you, such as Documents. You could then make a shortcut titled "GTA Tools", and pin it to taskbar or Start.
**Option 2** — <ins>Build from source</ins>
You will need the Rust toolchain, which can be obtained [here](https://rustup.rs). Follow the instructions of its installer. Once you have Rust installed, clone this repo and navigate to it. At this point, you should probably `git checkout x.x.x`, where `x.x.x` is the latest tag. You can then run `cargo build --release`. Once you do that, you can use the binary located at `.\target\release\gta-tools.exe` in the same way as **Option 1**.
@@ -16,7 +19,7 @@ You will need the Rust toolchain, which can be obtained [here](https://rustup.rs
Every feature of GTA Tools is Legacy/Enhanced-agnostic. Some functionality of GTA Tools requires administrator access. If necessary, GTA Tools can either be started as admin manually, or, the user can simply use the <kbd>Elevate</kbd> button to relaunch GTA Tools as admin.
It is recommended to always use an up-to-date version of GTA Tools from [releases](https://codeberg.org/futile/gta-tools/releases). You can also easily access the current latest release by going to the <kbd>About</kbd> page of GTA Tools and clicking the <kbd>↓</kbd> button near the version number.
It is recommended to always use an up-to-date version of GTA Tools from [releases](https://github.com/elituf/gta-tools/releases). You can also easily access the current latest release by going to the <kbd>About</kbd> page of GTA Tools and clicking the <kbd>↓</kbd> button near the version number.
#### Game
@@ -24,7 +27,7 @@ This section is quite simple.
There is a <kbd>Launch</kbd> feature, which will start your game on the chosen launcher and game version.
There is also a <kbd>Force close game</kbd> feature, which simply kills all game processes. This *does not* touch Rockstar Games Launcher or any other processes, only ones named `GTA5_Enhanced.exe` or `GTA5.exe`.
There is also a <kbd>Force close game</kbd> feature, which simply kills all game processes. This button requires a second press after the first one for confirmation. This *does not* touch Rockstar Games Launcher or any other processes, only ones named `GTA5_Enhanced.exe` or `GTA5.exe`.
#### Session
+9
View File
@@ -4,3 +4,12 @@ install:
cargo build --release
cp .\target\release\gta-tools.exe ~\.cargo\bin
cp .\target\release\gta-tools.exe ~\Documents
lint:
cargo clippy -- -W clippy::pedantic -W clippy::nursery -A clippy::cast_sign_loss -A clippy::cast_possible_truncation -A clippy::cast_possible_wrap
lint-full:
cargo clippy -- -W clippy::pedantic -W clippy::nursery -W clippy::unwrap_used
lint-unwraps:
cargo clippy -- -W clippy::unwrap_used
+1 -1
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,
+10 -14
View File
@@ -62,25 +62,21 @@ pub fn activate(game_handle: &mut HANDLE, system_info: &mut SystemInfo) -> Resul
let Some(pid) = get_gta_pid(system_info) else {
return Err(());
};
unsafe {
match OpenProcess(PROCESS_SUSPEND_RESUME, false, pid) {
Ok(handle) => *game_handle = handle,
Err(why) => {
let message = format!("failed to suspend game for empty session:\n{why}");
log::log(log::LogLevel::Error, &message);
return Err(());
}
match unsafe { OpenProcess(PROCESS_SUSPEND_RESUME, false, pid) } {
Ok(handle) => *game_handle = handle,
Err(why) => {
let message = format!("failed to suspend game for empty session:\n{why}");
log::log(log::LogLevel::Error, &message);
return Err(());
}
let _ = NtSuspendProcess(*game_handle);
}
let _ = unsafe { NtSuspendProcess(*game_handle) };
Ok(())
}
pub fn deactivate(game_handle: &mut HANDLE) {
unsafe {
if !game_handle.is_invalid() {
let _ = NtResumeProcess(*game_handle);
let _ = CloseHandle(*game_handle);
}
if !game_handle.is_invalid() {
let _ = unsafe { NtResumeProcess(*game_handle) };
let _ = unsafe { CloseHandle(*game_handle) };
}
}
+21 -25
View File
@@ -64,10 +64,8 @@ impl Default for GameNetworking {
impl Drop for GameNetworking {
fn drop(&mut self) {
unsafe {
if self.com_initialized {
CoUninitialize();
}
if self.com_initialized {
unsafe { CoUninitialize() };
}
}
}
@@ -79,44 +77,42 @@ impl GameNetworking {
return;
};
let policy: INetFwPolicy2 =
unsafe { CoCreateInstance(&NetFwPolicy2, None, CLSCTX_INPROC_SERVER).unwrap() };
let rules = unsafe { policy.Rules().unwrap() };
unsafe { CoCreateInstance(&NetFwPolicy2, None, CLSCTX_INPROC_SERVER) }.unwrap();
let rules = unsafe { policy.Rules() }.unwrap();
let exe_path = BSTR::from(exe_path.to_string_lossy().to_string());
for filter in [
(FILTER_NAME_IN, NET_FW_RULE_DIR_IN),
(FILTER_NAME_OUT, NET_FW_RULE_DIR_OUT),
] {
let _ = unsafe { rules.Remove(&BSTR::from(filter.0)) };
unsafe {
let rule: INetFwRule =
CoCreateInstance(&NetFwRule, None, CLSCTX_INPROC_SERVER).unwrap();
rule.SetName(&BSTR::from(filter.0)).unwrap();
rule.SetApplicationName(&exe_path).unwrap();
rule.SetDirection(filter.1).unwrap();
rule.SetEnabled(true.into()).unwrap();
rule.SetAction(NET_FW_ACTION_BLOCK).unwrap();
rule.SetProtocol(NET_FW_IP_PROTOCOL_ANY.0).unwrap();
rules.Add(&rule).unwrap();
}
let rule: INetFwRule =
unsafe { CoCreateInstance(&NetFwRule, None, CLSCTX_INPROC_SERVER) }.unwrap();
unsafe { rule.SetName(&BSTR::from(filter.0)) }.unwrap();
unsafe { rule.SetApplicationName(&exe_path) }.unwrap();
unsafe { rule.SetDirection(filter.1) }.unwrap();
unsafe { rule.SetEnabled(true.into()) }.unwrap();
unsafe { rule.SetAction(NET_FW_ACTION_BLOCK) }.unwrap();
unsafe { rule.SetProtocol(NET_FW_IP_PROTOCOL_ANY.0) }.unwrap();
unsafe { rules.Add(&rule) }.unwrap();
}
self.blocked_status = Self::is_blocked().into();
}
pub fn unblock_all(&mut self) {
let policy: INetFwPolicy2 =
unsafe { CoCreateInstance(&NetFwPolicy2, None, CLSCTX_INPROC_SERVER).unwrap() };
let rules = unsafe { policy.Rules().unwrap() };
unsafe { rules.Remove(&BSTR::from(FILTER_NAME_IN)).unwrap() };
unsafe { rules.Remove(&BSTR::from(FILTER_NAME_OUT)).unwrap() };
unsafe { CoCreateInstance(&NetFwPolicy2, None, CLSCTX_INPROC_SERVER) }.unwrap();
let rules = unsafe { policy.Rules() }.unwrap();
unsafe { rules.Remove(&BSTR::from(FILTER_NAME_IN)) }.unwrap();
unsafe { rules.Remove(&BSTR::from(FILTER_NAME_OUT)) }.unwrap();
self.blocked_status = Self::is_blocked().into();
}
fn is_blocked() -> bool {
let policy: INetFwPolicy2 =
unsafe { CoCreateInstance(&NetFwPolicy2, None, CLSCTX_INPROC_SERVER).unwrap() };
let rules = unsafe { policy.Rules().unwrap() };
let in_rule_exists = unsafe { rules.Item(&BSTR::from(FILTER_NAME_IN)).is_ok() };
let out_rule_exists = unsafe { rules.Item(&BSTR::from(FILTER_NAME_OUT)).is_ok() };
unsafe { CoCreateInstance(&NetFwPolicy2, None, CLSCTX_INPROC_SERVER) }.unwrap();
let rules = unsafe { policy.Rules() }.unwrap();
let in_rule_exists = unsafe { rules.Item(&BSTR::from(FILTER_NAME_IN)) }.is_ok();
let out_rule_exists = unsafe { rules.Item(&BSTR::from(FILTER_NAME_OUT)) }.is_ok();
in_rule_exists || out_rule_exists
}
+13 -12
View File
@@ -22,19 +22,17 @@ pub struct Launch {
pub fn launch(platform: &Platform, version: &LaunchVersion) {
match platform {
Platform::Steam => {
let steam_url = if *version == LaunchVersion::Enhanced {
"steam://run/3240220"
} else {
"steam://run/271590"
let steam_url = match version {
LaunchVersion::Enhanced => "steam://run/3240220",
LaunchVersion::Legacy => "steam://run/271590",
};
let _ = open::that_detached(steam_url);
}
Platform::Rockstar => {
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
let rockstar_url = if *version == LaunchVersion::Enhanced {
r"SOFTWARE\WOW6432Node\Rockstar Games\GTAV Enhanced"
} else {
r"SOFTWARE\WOW6432Node\Rockstar Games\Grand Theft Auto V"
let rockstar_url = match version {
LaunchVersion::Enhanced => r"SOFTWARE\WOW6432Node\Rockstar Games\GTAV Enhanced",
LaunchVersion::Legacy => r"SOFTWARE\WOW6432Node\Rockstar Games\Grand Theft Auto V",
};
let Ok(gta_v_enhanced) = hklm.open_subkey(rockstar_url) else {
return;
@@ -49,10 +47,13 @@ pub fn launch(platform: &Platform, version: &LaunchVersion) {
let _ = Command::new(play_gtav_path).spawn();
}
Platform::Epic => {
let epic_url = if *version == LaunchVersion::Enhanced {
"com.epicgames.launcher://apps/8769e24080ea413b8ebca3f1b8c50951?action=launch&silent=true"
} else {
"com.epicgames.launcher://apps/9d2d0eb64d5c44529cece33fe2a46482?action=launch&silent=true"
let epic_url = match version {
LaunchVersion::Enhanced => {
"com.epicgames.launcher://apps/8769e24080ea413b8ebca3f1b8c50951?action=launch&silent=true"
}
LaunchVersion::Legacy => {
"com.epicgames.launcher://apps/9d2d0eb64d5c44529cece33fe2a46482?action=launch&silent=true"
}
};
let _ = open::that_detached(epic_url);
}
+2 -2
View File
@@ -227,7 +227,7 @@ impl App {
ui.label("with ");
ui.scope(|ui| {
ui.style_mut().visuals.hyperlink_color = colours::RED;
ui.hyperlink_to("", "https://codeberg.org/futile/gta-tools");
ui.hyperlink_to("", "https://github.com/elituf/gta-tools");
});
ui.label(" from ");
ui.hyperlink_to("futile", "https://futile.eu");
@@ -244,7 +244,7 @@ impl App {
.on_hover_text("Go to current latest version.");
if button.clicked() {
let _ =
open::that("https://codeberg.org/futile/gta-tools/releases/latest");
open::that("https://github.com/elituf/gta-tools/releases/latest");
}
});
});
+4 -4
View File
@@ -21,10 +21,10 @@ impl App {
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);
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}\""));
+21 -24
View File
@@ -1,12 +1,13 @@
use std::{
os::windows::process::CommandExt,
ffi::{OsStr, OsString},
os::windows::{ffi::OsStringExt, process::CommandExt},
path::{Path, PathBuf},
process::Command,
};
use windows::{
Win32::System::{
Diagnostics::ToolHelp::{
CreateToolhelp32Snapshot, PROCESSENTRY32, Process32First, Process32Next,
CreateToolhelp32Snapshot, PROCESSENTRY32W, Process32FirstW, Process32NextW,
TH32CS_SNAPPROCESS,
},
Threading::{
@@ -20,7 +21,7 @@ use windows::{
#[derive(Clone, Debug)]
pub struct Process {
pid: u32,
name: String,
name: OsString,
exe: Option<PathBuf>,
}
@@ -29,7 +30,7 @@ impl Process {
self.pid
}
pub fn name(&self) -> &str {
pub fn name(&self) -> &OsStr {
&self.name
}
@@ -56,23 +57,23 @@ pub struct SystemInfo {
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 = PROCESSENTRY32 {
dwSize: size_of::<PROCESSENTRY32>() as u32,
let snapshot_handle = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) }.unwrap();
let mut process_entry = PROCESSENTRY32W {
dwSize: size_of::<PROCESSENTRY32W>() as u32,
..Default::default()
};
unsafe { Process32First(snapshot_handle, &raw mut process_entry).unwrap() };
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: c_char_arr_to_string(&process_entry.szExeFile),
name: wide_array_to_os_string(&process_entry.szExeFile),
exe: exe_full_path,
});
while unsafe { Process32Next(snapshot_handle, &raw mut process_entry) }.is_ok() {
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: c_char_arr_to_string(&process_entry.szExeFile),
name: wide_array_to_os_string(&process_entry.szExeFile),
exe: exe_full_path,
});
}
@@ -84,7 +85,7 @@ impl SystemInfo {
}
}
fn get_exe_full_path(process_entry: &PROCESSENTRY32) -> Option<PathBuf> {
fn get_exe_full_path(process_entry: &PROCESSENTRY32W) -> Option<PathBuf> {
let process_handle_result = unsafe {
OpenProcess(
PROCESS_QUERY_LIMITED_INFORMATION,
@@ -92,29 +93,25 @@ fn get_exe_full_path(process_entry: &PROCESSENTRY32) -> Option<PathBuf> {
process_entry.th32ProcessID,
)
};
let mut exename = [0u16; 260];
let mut dwsize = exename.len() as u32;
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(exename.as_mut_ptr()),
&raw mut dwsize,
PWSTR(exe_name.as_mut_ptr()),
&raw mut dw_size,
)
};
match image_name_result {
Ok(()) => Some(PathBuf::from(
unsafe { PWSTR(exename.as_mut_ptr()).to_string() }.unwrap(),
)),
Ok(()) => Some(PathBuf::from(wide_array_to_os_string(&exe_name))),
Err(_) => None,
}
})
}
fn c_char_arr_to_string(arr: &[i8]) -> String {
arr.iter()
.take_while(|&&b| b != 0)
.map(|&b| b as u8 as char)
.collect()
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])
}
+17 -21
View File
@@ -25,25 +25,21 @@ pub fn is_cursor_visible() -> bool {
cbSize: u32::try_from(std::mem::size_of::<CURSORINFO>()).unwrap(),
..Default::default()
};
unsafe {
GetCursorInfo(&raw 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) {
@@ -66,22 +62,22 @@ pub fn elevate(closing: ElevationExitMethod) {
pub fn is_elevated() -> bool {
let mut token: HANDLE = HANDLE::default();
unsafe {
if OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &raw mut token).is_err() {
return false;
}
let mut elevation = TOKEN_ELEVATION::default();
let mut size = u32::try_from(std::mem::size_of::<TOKEN_ELEVATION>()).unwrap();
let result = GetTokenInformation(
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,
);
CloseHandle(token).unwrap();
result.is_ok() && elevation.TokenIsElevated != 0
}
)
};
unsafe { CloseHandle(token) }.unwrap();
result.is_ok() && elevation.TokenIsElevated != 0
}
pub fn is_system_theme_dark() -> bool {