Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d7dde929d | ||
|
|
3ab802fd56 | ||
|
|
53030cc5f7 | ||
|
|
383224c2fa | ||
|
|
575a6a4f1d | ||
|
|
17579de277 | ||
|
|
00727b9913 | ||
|
|
1f6fc22525 | ||
|
|
27b09be76e | ||
|
|
6f15e8c348 | ||
|
|
d294aa9861 | ||
|
|
f688f1c3c4 | ||
|
|
58ec9f6567 | ||
|
|
1c9ce2a4d3 | ||
|
|
8a5d6cf38c | ||
|
|
d789b0f62d | ||
|
|
6079a72f47 | ||
|
|
914ffe46d2 | ||
|
|
935703e1ab | ||
|
|
202554e03b | ||
|
|
da2b15a0b6 | ||
|
|
ccc27b1944 | ||
|
|
10168e84d1 | ||
|
|
e72964a9f8 | ||
|
|
18052d0c78 | ||
|
|
09f5a3895b | ||
|
|
89892ef592 | ||
|
|
61de3aeee3 | ||
|
|
4ed00e6ac3 | ||
|
|
332db0ea03 | ||
|
|
64d13e3964 | ||
|
|
feb3a793d9 | ||
|
|
585f454bc0 | ||
|
|
27524e855a | ||
|
|
0257834d04 | ||
|
|
64e94bf790 | ||
|
|
668a6eb38b | ||
|
|
0de79769d2 | ||
|
|
b79c5f62b3 |
Generated
+216
-1488
File diff suppressed because it is too large
Load Diff
+11
-5
@@ -1,16 +1,22 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "gta-tools"
|
name = "gta-tools"
|
||||||
version = "0.4.2"
|
version = "0.5.3"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
catppuccin-egui = { version = "5.5.0", default-features = false, features = [
|
catppuccin-egui = { version = "5.5.0", default-features = false, features = [
|
||||||
"egui31",
|
"egui31",
|
||||||
] }
|
] }
|
||||||
chrono = "0.4.40"
|
chrono = { version = "0.4.40", default-features = false, features = ["clock"] }
|
||||||
dirs = "6.0.0"
|
dirs = "6.0.0"
|
||||||
eframe = "0.31.1"
|
eframe = { version = "0.31.1", default-features = false, features = [
|
||||||
egui_extras = { version = "0.31.1", features = ["image"] }
|
"glow",
|
||||||
|
"default_fonts",
|
||||||
|
] }
|
||||||
|
egui_extras = { version = "0.31.1", default-features = false, features = [
|
||||||
|
"image",
|
||||||
|
"svg",
|
||||||
|
] }
|
||||||
image = { version = "0.25.6", default-features = false, features = ["png"] }
|
image = { version = "0.25.6", default-features = false, features = ["png"] }
|
||||||
open = "5.3.2"
|
open = "5.3.2"
|
||||||
semver = "1.0.26"
|
semver = "1.0.26"
|
||||||
@@ -25,6 +31,7 @@ windows = { version = "0.61.1", features = [
|
|||||||
"Win32_UI_Shell",
|
"Win32_UI_Shell",
|
||||||
"Win32_UI_WindowsAndMessaging",
|
"Win32_UI_WindowsAndMessaging",
|
||||||
"Win32_Security",
|
"Win32_Security",
|
||||||
|
"Win32_NetworkManagement_WindowsFirewall",
|
||||||
] }
|
] }
|
||||||
winreg = "0.55.0"
|
winreg = "0.55.0"
|
||||||
|
|
||||||
@@ -33,6 +40,5 @@ static_vcruntime = "2.0.0"
|
|||||||
winresource = "0.1.20"
|
winresource = "0.1.20"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
strip = true
|
|
||||||
lto = true
|
lto = true
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
|
|||||||
@@ -1,4 +1,13 @@
|
|||||||
# GTA Tools
|
# GTA Tools
|
||||||
A toolset of convenient things for GTA 5 Online.
|
|
||||||
|
|
||||||

|
A toolset of convenient things for GTA V Online.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
**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**.
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="512"
|
||||||
|
height="512"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<g
|
||||||
|
id="layer1">
|
||||||
|
<circle
|
||||||
|
style="fill:#ffffff"
|
||||||
|
id="path1"
|
||||||
|
cx="256"
|
||||||
|
cy="256"
|
||||||
|
r="256" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 458 B |
+34
-28
@@ -1,13 +1,12 @@
|
|||||||
#![allow(clippy::cast_possible_truncation)]
|
use crate::util::{self, consts::game::WINDOW_TITLE};
|
||||||
|
|
||||||
use crate::util::{self, consts::GTA_WINDOW_TITLE};
|
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use windows::Win32::UI::Input::KeyboardAndMouse::{
|
use windows::Win32::UI::Input::KeyboardAndMouse::{
|
||||||
KEYBD_EVENT_FLAGS, MAP_VIRTUAL_KEY_TYPE, MapVirtualKeyW, VK_NUMPAD4, VK_NUMPAD6, keybd_event,
|
INPUT, INPUT_KEYBOARD, KEYBD_EVENT_FLAGS, KEYBDINPUT, KEYEVENTF_KEYUP, MAPVK_VK_TO_VSC,
|
||||||
|
MapVirtualKeyW, SendInput, VIRTUAL_KEY, VK_NUMPAD4, VK_NUMPAD6,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const INTERVAL: Duration = Duration::from_secs(60);
|
const INTERVAL: Duration = Duration::from_secs(60);
|
||||||
const PRESS_KEYS: [u8; 2] = [VK_NUMPAD4.0 as u8, VK_NUMPAD6.0 as u8];
|
const PRESS_KEYS: [VIRTUAL_KEY; 2] = [VK_NUMPAD4, VK_NUMPAD6];
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct AntiAfk {
|
pub struct AntiAfk {
|
||||||
@@ -25,32 +24,39 @@ impl Default for AntiAfk {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AntiAfk {
|
impl AntiAfk {
|
||||||
pub fn activate(&mut self) {
|
pub fn can_activate(&self) -> bool {
|
||||||
if can_activate() {
|
use util::win::{is_any_key_pressed, is_cursor_visible, is_window_focused};
|
||||||
send(&PRESS_KEYS);
|
is_window_focused(WINDOW_TITLE) && !is_any_key_pressed(&PRESS_KEYS) && !is_cursor_visible()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn should_activate(&self) -> bool {
|
||||||
|
self.enabled && self.interval.elapsed() >= INTERVAL
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn activate(&mut self) {
|
||||||
|
send(&PRESS_KEYS);
|
||||||
self.interval = Instant::now();
|
self.interval = Instant::now();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn can_activate() -> bool {
|
fn send(vk_codes: &[VIRTUAL_KEY]) {
|
||||||
use util::win::{is_any_key_pressed, is_cursor_visible, is_window_focused};
|
let mut inputs = Vec::new();
|
||||||
is_window_focused(GTA_WINDOW_TITLE) && !is_any_key_pressed(&PRESS_KEYS) && !is_cursor_visible()
|
for &vk_code in vk_codes {
|
||||||
|
let scan_code = unsafe { MapVirtualKeyW(u32::from(vk_code.0), MAPVK_VK_TO_VSC) as u16 };
|
||||||
|
for event in [KEYBD_EVENT_FLAGS(0), KEYEVENTF_KEYUP] {
|
||||||
|
let mut input = INPUT {
|
||||||
|
r#type: INPUT_KEYBOARD,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
input.Anonymous.ki = KEYBDINPUT {
|
||||||
|
wVk: vk_code,
|
||||||
|
wScan: scan_code,
|
||||||
|
dwFlags: event,
|
||||||
|
time: 0,
|
||||||
|
dwExtraInfo: 0,
|
||||||
|
};
|
||||||
|
inputs.push(input);
|
||||||
|
}
|
||||||
|
unsafe { SendInput(&inputs, size_of::<INPUT>() as i32) };
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send(vk_codes: &[u8]) {
|
|
||||||
vk_codes.iter().for_each(|vk_code: &u8| unsafe {
|
|
||||||
keybd_event(
|
|
||||||
*vk_code,
|
|
||||||
u8::try_from(MapVirtualKeyW(u32::from(*vk_code), MAP_VIRTUAL_KEY_TYPE(0))).unwrap(),
|
|
||||||
KEYBD_EVENT_FLAGS(0),
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
keybd_event(
|
|
||||||
*vk_code,
|
|
||||||
u8::try_from(MapVirtualKeyW(u32::from(*vk_code), MAP_VIRTUAL_KEY_TYPE(0))).unwrap(),
|
|
||||||
KEYBD_EVENT_FLAGS(2),
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::util::{
|
use crate::util::{
|
||||||
consts::{ENHANCED, LEGACY},
|
consts::game::{EXE_ENHANCED, EXE_LEGACY},
|
||||||
countdown::Countdown,
|
countdown::Countdown,
|
||||||
};
|
};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
@@ -9,7 +9,7 @@ use windows::Win32::{
|
|||||||
System::Threading::{OpenProcess, PROCESS_SUSPEND_RESUME},
|
System::Threading::{OpenProcess, PROCESS_SUSPEND_RESUME},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const INTERVAL: Duration = Duration::from_secs(10);
|
const INTERVAL: Duration = Duration::from_secs(10);
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct EmptySession {
|
pub struct EmptySession {
|
||||||
@@ -28,10 +28,24 @@ impl Default for EmptySession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl EmptySession {
|
||||||
|
pub fn run_timers(&mut self, game_handle: &mut HANDLE) {
|
||||||
|
if self.disabled {
|
||||||
|
self.countdown.count();
|
||||||
|
} else {
|
||||||
|
self.countdown.reset();
|
||||||
|
}
|
||||||
|
if self.interval.elapsed() >= INTERVAL {
|
||||||
|
deactivate(game_handle);
|
||||||
|
self.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[link(name = "ntdll")]
|
#[link(name = "ntdll")]
|
||||||
unsafe extern "system" {
|
unsafe extern "system" {
|
||||||
pub unsafe fn NtSuspendProcess(ProcessHandle: HANDLE) -> NTSTATUS;
|
unsafe fn NtSuspendProcess(ProcessHandle: HANDLE) -> NTSTATUS;
|
||||||
pub unsafe fn NtResumeProcess(ProcessHandle: HANDLE) -> NTSTATUS;
|
unsafe fn NtResumeProcess(ProcessHandle: HANDLE) -> NTSTATUS;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_gta_pid(sysinfo: &mut System) -> u32 {
|
fn get_gta_pid(sysinfo: &mut System) -> u32 {
|
||||||
@@ -39,29 +53,29 @@ fn get_gta_pid(sysinfo: &mut System) -> u32 {
|
|||||||
if let Some((pid, _)) = sysinfo
|
if let Some((pid, _)) = sysinfo
|
||||||
.processes()
|
.processes()
|
||||||
.iter()
|
.iter()
|
||||||
.find(|(_, p)| p.name() == ENHANCED || p.name() == LEGACY)
|
.find(|(_, p)| p.name() == EXE_ENHANCED || p.name() == EXE_LEGACY)
|
||||||
{
|
{
|
||||||
return pid.as_u32();
|
return pid.as_u32();
|
||||||
}
|
}
|
||||||
u32::MAX
|
u32::MAX
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn activate(handle: &mut HANDLE, sysinfo: &mut System) {
|
pub fn activate(game_handle: &mut HANDLE, sysinfo: &mut System) {
|
||||||
let pid = get_gta_pid(sysinfo);
|
let pid = get_gta_pid(sysinfo);
|
||||||
if pid == u32::MAX {
|
if pid == u32::MAX {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
unsafe {
|
unsafe {
|
||||||
*handle = OpenProcess(PROCESS_SUSPEND_RESUME, false, pid).unwrap();
|
*game_handle = OpenProcess(PROCESS_SUSPEND_RESUME, false, pid).unwrap();
|
||||||
let _ = NtSuspendProcess(*handle);
|
let _ = NtSuspendProcess(*game_handle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deactivate(handle: &mut HANDLE) {
|
pub fn deactivate(game_handle: &mut HANDLE) {
|
||||||
unsafe {
|
unsafe {
|
||||||
if !handle.is_invalid() {
|
if !game_handle.is_invalid() {
|
||||||
let _ = NtResumeProcess(*handle);
|
let _ = NtResumeProcess(*game_handle);
|
||||||
let _ = CloseHandle(*handle);
|
let _ = CloseHandle(*game_handle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+35
-11
@@ -1,38 +1,62 @@
|
|||||||
use crate::util::consts::{ENHANCED, LEGACY};
|
use crate::util::consts::game::{EXE_ENHANCED, EXE_LEGACY};
|
||||||
use std::time::Instant;
|
use std::time::{Duration, Instant};
|
||||||
use sysinfo::System;
|
use sysinfo::System;
|
||||||
|
|
||||||
|
const INTERVAL: Duration = Duration::from_secs(3);
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ForceClose {
|
pub struct ForceClose {
|
||||||
pub button_text: String,
|
pub button_text: String,
|
||||||
pub prompting: bool,
|
timer: Instant,
|
||||||
pub interval: Instant,
|
counting: bool,
|
||||||
|
current_frame: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ForceClose {
|
impl Default for ForceClose {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
button_text: "Force close game".to_owned(),
|
button_text: "Force close game".to_owned(),
|
||||||
prompting: false,
|
timer: Instant::now(),
|
||||||
interval: Instant::now(),
|
counting: false,
|
||||||
|
current_frame: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ForceClose {
|
impl ForceClose {
|
||||||
pub fn prompting(&mut self) {
|
pub fn prompt(&mut self, force_close_button_clicked: bool, sysinfo: &mut System) {
|
||||||
|
if force_close_button_clicked && !self.counting {
|
||||||
self.button_text = "Are you sure?".to_owned();
|
self.button_text = "Are you sure?".to_owned();
|
||||||
self.prompting = true;
|
self.timer = Instant::now();
|
||||||
self.interval = Instant::now();
|
self.counting = true;
|
||||||
|
self.current_frame = true;
|
||||||
|
}
|
||||||
|
if self.counting && self.timer.elapsed() >= INTERVAL {
|
||||||
|
self.reset();
|
||||||
|
} else if force_close_button_clicked && !self.current_frame {
|
||||||
|
activate(sysinfo);
|
||||||
|
self.reset();
|
||||||
|
}
|
||||||
|
self.finish_current_frame();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
*self = Self::default();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish_current_frame(&mut self) {
|
||||||
|
if self.current_frame {
|
||||||
|
self.current_frame = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn activate(sysinfo: &mut System) {
|
fn activate(sysinfo: &mut System) {
|
||||||
sysinfo.refresh_all();
|
sysinfo.refresh_all();
|
||||||
sysinfo
|
sysinfo
|
||||||
.processes()
|
.processes()
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(_, p)| p.name() == ENHANCED || p.name() == LEGACY)
|
.filter(|(_, p)| p.name() == EXE_ENHANCED || p.name() == EXE_LEGACY)
|
||||||
.for_each(|(_, p)| {
|
.for_each(|(_, p)| {
|
||||||
p.kill();
|
p.kill();
|
||||||
});
|
});
|
||||||
|
|||||||
+133
-58
@@ -1,72 +1,147 @@
|
|||||||
#![allow(clippy::zombie_processes)]
|
use crate::util::consts::game::{EXE_ENHANCED, EXE_LEGACY};
|
||||||
|
use std::{
|
||||||
use crate::util::consts::{ENHANCED, LEGACY};
|
path::Path,
|
||||||
use std::{os::windows::process::CommandExt, path::Path, process::Command};
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
use strum::{Display, EnumIter};
|
||||||
use sysinfo::System;
|
use sysinfo::System;
|
||||||
use windows::Win32::System::Threading::CREATE_NO_WINDOW;
|
use windows::{
|
||||||
|
Win32::{
|
||||||
|
NetworkManagement::WindowsFirewall::{
|
||||||
|
INetFwPolicy2, INetFwRule, NET_FW_ACTION_BLOCK, NET_FW_IP_PROTOCOL_ANY,
|
||||||
|
NET_FW_RULE_DIR_IN, NET_FW_RULE_DIR_OUT, NetFwPolicy2, NetFwRule,
|
||||||
|
},
|
||||||
|
System::Com::{
|
||||||
|
CLSCTX_INPROC_SERVER, COINIT_MULTITHREADED, CoCreateInstance, CoInitializeEx,
|
||||||
|
CoUninitialize,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
core::BSTR,
|
||||||
|
};
|
||||||
|
|
||||||
const FILTER_NAME: &str = "[GTA Tools] Block all traffic for GTA V";
|
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 INTERVAL: Duration = Duration::from_secs(3);
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Display, PartialEq, Eq, EnumIter)]
|
||||||
|
pub enum BlockedStatus {
|
||||||
|
Blocked,
|
||||||
|
Failed,
|
||||||
|
Unblocked,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<bool> for BlockedStatus {
|
||||||
|
fn from(value: bool) -> Self {
|
||||||
|
if value {
|
||||||
|
Self::Blocked
|
||||||
|
} else {
|
||||||
|
Self::Unblocked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct GameNetworking {
|
||||||
|
com_initialized: bool,
|
||||||
|
pub blocked_status: BlockedStatus,
|
||||||
|
timer: Instant,
|
||||||
|
counting: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for GameNetworking {
|
||||||
|
fn default() -> Self {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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> {
|
fn get_game_exe_path(sysinfo: &mut System) -> Option<&Path> {
|
||||||
sysinfo.refresh_all();
|
sysinfo.refresh_all();
|
||||||
if let Some((_, process)) = sysinfo
|
if let Some((_, process)) = sysinfo
|
||||||
.processes()
|
.processes()
|
||||||
.iter()
|
.iter()
|
||||||
.find(|(_, p)| p.name() == ENHANCED || p.name() == LEGACY)
|
.find(|(_, p)| p.name() == EXE_ENHANCED || p.name() == EXE_LEGACY)
|
||||||
{
|
{
|
||||||
process.exe()
|
process.exe()
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn block_all(sysinfo: &mut System) {
|
|
||||||
let Some(exe_path) = get_game_exe_path(sysinfo) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let exe_path = exe_path.display();
|
|
||||||
Command::new("netsh")
|
|
||||||
.args([
|
|
||||||
"advfirewall",
|
|
||||||
"firewall",
|
|
||||||
"add",
|
|
||||||
"rule",
|
|
||||||
&format!("name={FILTER_NAME}"),
|
|
||||||
"dir=out",
|
|
||||||
"action=block",
|
|
||||||
"protocol=ANY",
|
|
||||||
&format!("program={exe_path}"),
|
|
||||||
])
|
|
||||||
.creation_flags(CREATE_NO_WINDOW.0)
|
|
||||||
.spawn()
|
|
||||||
.unwrap();
|
|
||||||
Command::new("netsh")
|
|
||||||
.args([
|
|
||||||
"advfirewall",
|
|
||||||
"firewall",
|
|
||||||
"add",
|
|
||||||
"rule",
|
|
||||||
&format!("name={FILTER_NAME}"),
|
|
||||||
"dir=in",
|
|
||||||
"action=block",
|
|
||||||
"protocol=ANY",
|
|
||||||
&format!("program={exe_path}"),
|
|
||||||
])
|
|
||||||
.creation_flags(CREATE_NO_WINDOW.0)
|
|
||||||
.spawn()
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn unblock_all() {
|
|
||||||
Command::new("netsh")
|
|
||||||
.args([
|
|
||||||
"advfirewall",
|
|
||||||
"firewall",
|
|
||||||
"delete",
|
|
||||||
"rule",
|
|
||||||
&format!("name={FILTER_NAME}"),
|
|
||||||
])
|
|
||||||
.creation_flags(CREATE_NO_WINDOW.0)
|
|
||||||
.spawn()
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|||||||
+5
-14
@@ -1,27 +1,18 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{fmt::Display, path::PathBuf, process::Command};
|
use std::{path::PathBuf, process::Command};
|
||||||
use strum::EnumIter;
|
use strum::{Display, EnumIter};
|
||||||
use winreg::{RegKey, enums::HKEY_LOCAL_MACHINE};
|
use winreg::{RegKey, enums::HKEY_LOCAL_MACHINE};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Default, Debug, PartialEq, Eq, Serialize, Deserialize, EnumIter)]
|
#[derive(Clone, Copy, Default, Debug, Display, PartialEq, Eq, Serialize, Deserialize, EnumIter)]
|
||||||
pub enum Platform {
|
pub enum Platform {
|
||||||
#[default]
|
#[default]
|
||||||
Steam,
|
Steam,
|
||||||
|
#[strum(to_string = "Rockstar Games")]
|
||||||
Rockstar,
|
Rockstar,
|
||||||
|
#[strum(to_string = "Epic Games")]
|
||||||
Epic,
|
Epic,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Platform {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
let x = match self {
|
|
||||||
Self::Steam => "Steam",
|
|
||||||
Self::Rockstar => "Rockstar Games",
|
|
||||||
Self::Epic => "Epic Games",
|
|
||||||
};
|
|
||||||
write!(f, "{x}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct Launch {
|
pub struct Launch {
|
||||||
pub selected: Platform,
|
pub selected: Platform,
|
||||||
|
|||||||
+88
-180
@@ -1,92 +1,70 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
features::{
|
features,
|
||||||
self, anti_afk::AntiAfk, empty_session::EmptySession, force_close::ForceClose,
|
gui::{settings::Settings, tools},
|
||||||
launch::Launch,
|
util::{consts::game::WINDOW_TITLE, meta::Meta, persistent_state::PersistentState, win},
|
||||||
},
|
|
||||||
gui::{
|
|
||||||
settings::{self, Settings},
|
|
||||||
tools,
|
|
||||||
},
|
|
||||||
util::{
|
|
||||||
self,
|
|
||||||
consts::{APP_STORAGE_PATH, ENHANCED, GTA_WINDOW_TITLE, LEGACY},
|
|
||||||
meta::Meta,
|
|
||||||
persistent_state::PersistentState,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
use strum::{Display, EnumIter};
|
||||||
|
|
||||||
pub const WINDOW_SIZE: [f32; 2] = [240.0, 240.0];
|
pub const WINDOW_SIZE: [f32; 2] = [240.0, 240.0];
|
||||||
|
|
||||||
#[derive(Debug, Default, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, Default, Display, PartialEq, Eq, EnumIter)]
|
||||||
pub enum Stage {
|
enum Stage {
|
||||||
#[default]
|
#[default]
|
||||||
Main,
|
Main,
|
||||||
Settings,
|
Settings,
|
||||||
About,
|
About,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::struct_excessive_bools)]
|
#[derive(Debug)]
|
||||||
#[derive(Debug, Default)]
|
|
||||||
pub struct Flags {
|
pub struct Flags {
|
||||||
pub elevated: bool,
|
pub elevated: bool,
|
||||||
debug: bool,
|
pub debug: bool,
|
||||||
closing: bool,
|
closing: bool,
|
||||||
current_frame: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
impl Default for Flags {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
elevated: win::is_elevated(),
|
||||||
|
debug: false,
|
||||||
|
closing: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
pub struct App {
|
pub struct App {
|
||||||
meta: Meta,
|
pub meta: Meta,
|
||||||
pub settings: Settings,
|
pub settings: Settings,
|
||||||
stage: Stage,
|
stage: Stage,
|
||||||
pub flags: Flags,
|
pub flags: Flags,
|
||||||
pub sysinfo: sysinfo::System,
|
pub sysinfo: sysinfo::System,
|
||||||
pub game_handle: windows::Win32::Foundation::HANDLE,
|
game_handle: windows::Win32::Foundation::HANDLE,
|
||||||
pub launch: Launch,
|
pub anti_afk: features::anti_afk::AntiAfk,
|
||||||
force_close: ForceClose,
|
empty_session: features::empty_session::EmptySession,
|
||||||
empty_session: EmptySession,
|
force_close: features::force_close::ForceClose,
|
||||||
anti_afk: AntiAfk,
|
pub game_networking: features::game_networking::GameNetworking,
|
||||||
}
|
pub launch: features::launch::Launch,
|
||||||
|
|
||||||
impl Default for App {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
meta: Meta::default(),
|
|
||||||
settings: Settings::default(),
|
|
||||||
stage: Stage::default(),
|
|
||||||
flags: Flags::default(),
|
|
||||||
sysinfo: sysinfo::System::new_all(),
|
|
||||||
game_handle: windows::Win32::Foundation::HANDLE::default(),
|
|
||||||
launch: Launch::default(),
|
|
||||||
force_close: ForceClose::default(),
|
|
||||||
empty_session: EmptySession::default(),
|
|
||||||
anti_afk: AntiAfk::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl eframe::App for App {
|
impl eframe::App for App {
|
||||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||||
catppuccin_egui::set_theme(ctx, self.settings.theme.into());
|
ctx.request_repaint_after(Duration::from_millis(100));
|
||||||
self.run_timers();
|
self.empty_session.run_timers(&mut self.game_handle);
|
||||||
egui::TopBottomPanel::bottom("bottom_panel")
|
egui::TopBottomPanel::bottom("bottom_panel")
|
||||||
.exact_height(25.0)
|
.exact_height(25.0)
|
||||||
.show(ctx, |ui| {
|
.show(ctx, |ui| {
|
||||||
ui.with_layout(egui::Layout::left_to_right(egui::Align::Center), |ui| {
|
ui.with_layout(egui::Layout::left_to_right(egui::Align::Center), |ui| {
|
||||||
ui.selectable_value(&mut self.stage, Stage::Main, "Main");
|
tools::build_menu::<Stage>(ui, &mut self.stage);
|
||||||
ui.selectable_value(&mut self.stage, Stage::Settings, "Settings");
|
|
||||||
ui.selectable_value(&mut self.stage, Stage::About, "About");
|
|
||||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||||
let button = ui
|
let button = ui
|
||||||
.add_enabled(!self.flags.elevated, egui::Button::new("Elevate"))
|
.add_enabled(!self.flags.elevated, egui::Button::new("Elevate"))
|
||||||
.on_hover_text("Relaunch ourselves as administrator.")
|
.on_hover_text("Relaunch ourselves as administrator.")
|
||||||
.on_disabled_hover_text("We are already running elevated.");
|
.on_disabled_hover_text("We are already running elevated.");
|
||||||
if button.clicked() {
|
if button.clicked() {
|
||||||
util::win::elevate(util::win::ElevationExitMethod::Gentle(
|
win::elevate(win::ElevationExitMethod::Gentle(&mut self.flags.closing));
|
||||||
&mut self.flags.closing,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -95,53 +73,17 @@ impl eframe::App for App {
|
|||||||
egui::ScrollArea::vertical()
|
egui::ScrollArea::vertical()
|
||||||
.auto_shrink([false, true])
|
.auto_shrink([false, true])
|
||||||
.show(ui, |ui| match self.stage {
|
.show(ui, |ui| match self.stage {
|
||||||
Stage::Main => {
|
Stage::Main => self.show_main_stage(ctx, ui),
|
||||||
self.show_game(ctx, ui);
|
Stage::Settings => self.show_settings_stage(ctx, ui),
|
||||||
self.show_session(ctx, ui);
|
Stage::About => self.show_about_stage(ctx, ui),
|
||||||
self.show_network(ctx, ui);
|
|
||||||
}
|
|
||||||
Stage::Settings => self.show_settings(ctx, ui),
|
|
||||||
Stage::About => self.show_about(ctx, ui),
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
if tools::check_debug_keycombo_pressed(ctx) {
|
if tools::debug_keycombo_pressed(ctx) || tools::debug_viewport_close_pressed(ctx) {
|
||||||
self.flags.debug = !self.flags.debug;
|
self.flags.debug = !self.flags.debug;
|
||||||
}
|
}
|
||||||
if tools::check_debug_viewport_close_button_pressed(ctx) {
|
|
||||||
self.flags.debug = false;
|
|
||||||
}
|
|
||||||
if self.flags.debug {
|
if self.flags.debug {
|
||||||
let main_rect = ctx.input(|i| {
|
self.show_debug_viewport(ctx);
|
||||||
i.viewport()
|
|
||||||
.clone()
|
|
||||||
.outer_rect
|
|
||||||
.unwrap_or(egui::Rect::EVERYTHING)
|
|
||||||
});
|
|
||||||
let position = [main_rect.right(), main_rect.min.y];
|
|
||||||
ctx.show_viewport_immediate(
|
|
||||||
egui::ViewportId::from_hash_of("debug_viewport"),
|
|
||||||
egui::ViewportBuilder::default()
|
|
||||||
.with_title("GTA Tools Debug")
|
|
||||||
.with_minimize_button(false)
|
|
||||||
.with_maximize_button(false)
|
|
||||||
.with_inner_size(WINDOW_SIZE)
|
|
||||||
.with_position(position)
|
|
||||||
.with_icon(tools::load_icon()),
|
|
||||||
|ctx, _class| {
|
|
||||||
if tools::check_debug_keycombo_pressed(ctx) {
|
|
||||||
self.flags.debug = !self.flags.debug;
|
|
||||||
}
|
}
|
||||||
egui::CentralPanel::default().show(ctx, |ui| {
|
|
||||||
egui::ScrollArea::both()
|
|
||||||
.auto_shrink([false, true])
|
|
||||||
.show(ui, |ui| {
|
|
||||||
self.show_debug(ctx, ui);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
ctx.request_repaint_after(Duration::from_millis(100));
|
|
||||||
if self.flags.closing {
|
if self.flags.closing {
|
||||||
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
||||||
}
|
}
|
||||||
@@ -149,42 +91,28 @@ impl eframe::App for App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
fn show_game(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) {
|
fn show_game_section(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) {
|
||||||
tools::header(ui, "Game");
|
tools::header(ui, "Game");
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
if ui.button("Launch").clicked() {
|
if ui.button("Launch").clicked() {
|
||||||
features::launch::launch(&self.launch.selected);
|
features::launch::launch(&self.launch.selected);
|
||||||
}
|
}
|
||||||
tools::build_combo_box::<features::launch::Platform>(
|
egui::ComboBox::from_id_salt("Launch")
|
||||||
ui,
|
.selected_text(self.launch.selected.to_string())
|
||||||
&mut self.launch.selected,
|
.width(120.0)
|
||||||
"Launch",
|
.show_ui(ui, |ui| {
|
||||||
);
|
tools::build_menu(ui, &mut self.launch.selected);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
let force_close_button = ui.add_sized(
|
let force_close_button = ui.add_sized(
|
||||||
[104.0, 0.0],
|
[104.0, 0.0],
|
||||||
egui::Button::new(&self.force_close.button_text),
|
egui::Button::new(&self.force_close.button_text),
|
||||||
);
|
);
|
||||||
if force_close_button.clicked() && !self.force_close.prompting {
|
self.force_close
|
||||||
self.force_close.prompting();
|
.prompt(force_close_button.clicked(), &mut self.sysinfo);
|
||||||
self.flags.current_frame = true;
|
|
||||||
}
|
|
||||||
if self.force_close.prompting
|
|
||||||
&& self.force_close.interval.elapsed() <= Duration::from_secs(3)
|
|
||||||
{
|
|
||||||
if force_close_button.clicked() && !self.flags.current_frame {
|
|
||||||
features::force_close::activate(&mut self.sysinfo);
|
|
||||||
self.force_close = ForceClose::default();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
self.force_close = ForceClose::default();
|
|
||||||
}
|
|
||||||
if self.flags.current_frame {
|
|
||||||
self.flags.current_frame = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_session(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) {
|
fn show_session_section(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) {
|
||||||
tools::header(ui, "Session");
|
tools::header(ui, "Session");
|
||||||
ui.add_enabled_ui(!self.empty_session.disabled, |ui| {
|
ui.add_enabled_ui(!self.empty_session.disabled, |ui| {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
@@ -202,7 +130,7 @@ impl App {
|
|||||||
if self.anti_afk.enabled {
|
if self.anti_afk.enabled {
|
||||||
ui.add_space(8.0);
|
ui.add_space(8.0);
|
||||||
ui.add_enabled_ui(false, |ui| {
|
ui.add_enabled_ui(false, |ui| {
|
||||||
ui.label(if util::win::is_window_focused(GTA_WINDOW_TITLE) {
|
ui.label(if win::is_window_focused(WINDOW_TITLE) {
|
||||||
"GTA is focused."
|
"GTA is focused."
|
||||||
} else {
|
} else {
|
||||||
"GTA is not focused!"
|
"GTA is not focused!"
|
||||||
@@ -210,34 +138,44 @@ impl App {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if self.anti_afk.enabled && self.anti_afk.interval.elapsed() >= features::anti_afk::INTERVAL
|
if self.anti_afk.can_activate() && self.anti_afk.should_activate() {
|
||||||
{
|
|
||||||
self.anti_afk.activate();
|
self.anti_afk.activate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_network(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) {
|
fn show_network_section(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) {
|
||||||
tools::header(ui, "Network");
|
tools::header(ui, "Network");
|
||||||
egui::Frame::new()
|
egui::Frame::new()
|
||||||
.outer_margin(egui::vec2(0.0, -2.0))
|
.outer_margin(egui::vec2(0.0, -2.0))
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
let response = ui.add_enabled_ui(self.flags.elevated, |ui| {
|
let response = ui.add_enabled_ui(self.flags.elevated, |ui| {
|
||||||
|
let label = ui.horizontal(|ui| {
|
||||||
let label = ui.label("Game's network access");
|
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| {
|
ui.horizontal(|ui| {
|
||||||
let available_width = label.rect.width();
|
let available_width = label.inner.rect.width();
|
||||||
let spacing = ui.spacing().item_spacing.x;
|
let spacing = ui.spacing().item_spacing.x;
|
||||||
let button_width = (available_width - spacing) / 2.0;
|
let button_width = (available_width - spacing) / 2.0;
|
||||||
if ui
|
if ui
|
||||||
.add_sized([button_width, 18.0], egui::Button::new("Block"))
|
.add_sized([button_width, 18.0], egui::Button::new("Block"))
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
features::game_networking::block_all(&mut self.sysinfo);
|
self.game_networking.block_all(&mut self.sysinfo);
|
||||||
}
|
}
|
||||||
if ui
|
if ui
|
||||||
.add_sized([button_width, 18.0], egui::Button::new("Unblock"))
|
.add_sized([button_width, 18.0], egui::Button::new("Unblock"))
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
features::game_networking::unblock_all();
|
self.game_networking.unblock_all();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -247,7 +185,29 @@ impl App {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_about(&self, _ctx: &egui::Context, ui: &mut egui::Ui) {
|
fn show_main_stage(&mut self, ctx: &egui::Context, ui: &mut egui::Ui) {
|
||||||
|
self.show_game_section(ctx, ui);
|
||||||
|
self.show_session_section(ctx, ui);
|
||||||
|
self.show_network_section(ctx, ui);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_settings_stage(&mut self, ctx: &egui::Context, ui: &mut egui::Ui) {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
if selection != self.settings.theme {
|
||||||
|
catppuccin_egui::set_theme(ctx, self.settings.theme.into());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ui.checkbox(&mut self.settings.start_elevated, "Always start elevated");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_about_stage(&self, _ctx: &egui::Context, ui: &mut egui::Ui) {
|
||||||
ui.with_layout(egui::Layout::bottom_up(egui::Align::Center), |ui| {
|
ui.with_layout(egui::Layout::bottom_up(egui::Align::Center), |ui| {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
@@ -282,68 +242,16 @@ impl App {
|
|||||||
)));
|
)));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_settings(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) {
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.label("Theme");
|
|
||||||
tools::build_combo_box::<settings::Theme>(ui, &mut self.settings.theme, "Theme");
|
|
||||||
});
|
|
||||||
ui.checkbox(&mut self.settings.start_elevated, "Always start elevated");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn show_debug(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) {
|
|
||||||
if ui.button("open storage path").clicked() {
|
|
||||||
open::that_detached(APP_STORAGE_PATH.as_path()).unwrap();
|
|
||||||
}
|
|
||||||
ui.collapsing("anti afk", |ui| {
|
|
||||||
ui.label(format!(
|
|
||||||
"timer: {}",
|
|
||||||
self.anti_afk.interval.elapsed().as_secs()
|
|
||||||
));
|
|
||||||
ui.label(format!(
|
|
||||||
"can activate: {}",
|
|
||||||
features::anti_afk::can_activate()
|
|
||||||
));
|
|
||||||
});
|
|
||||||
ui.collapsing("sysinfo", |ui| {
|
|
||||||
if ui.button("refresh all").clicked() {
|
|
||||||
self.sysinfo.refresh_all();
|
|
||||||
}
|
|
||||||
let pid = self
|
|
||||||
.sysinfo
|
|
||||||
.processes()
|
|
||||||
.iter()
|
|
||||||
.find(|(_, p)| p.name() == ENHANCED || p.name() == LEGACY)
|
|
||||||
.map_or_else(
|
|
||||||
|| "no pid found!".to_owned(),
|
|
||||||
|(pid, _)| pid.as_u32().to_string(),
|
|
||||||
);
|
|
||||||
ui.label(format!("gta pid: {pid}"));
|
|
||||||
});
|
|
||||||
ui.collapsing("app state", |ui| ui.label(format!("{self:#?}")));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run_timers(&mut self) {
|
|
||||||
if self.empty_session.disabled {
|
|
||||||
self.empty_session.countdown.count();
|
|
||||||
} else {
|
|
||||||
self.empty_session.countdown.reset();
|
|
||||||
}
|
|
||||||
if self.empty_session.interval.elapsed() >= features::empty_session::INTERVAL {
|
|
||||||
features::empty_session::deactivate(&mut self.game_handle);
|
|
||||||
self.empty_session.disabled = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for App {
|
impl Drop for App {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
// save any persistent state to config file
|
// save any persistent state to config file
|
||||||
let persistent_state = PersistentState {
|
PersistentState {
|
||||||
launcher: self.launch.selected,
|
launcher: self.launch.selected,
|
||||||
settings: self.settings.clone(),
|
settings: self.settings.clone(),
|
||||||
};
|
}
|
||||||
persistent_state.set();
|
.set();
|
||||||
// make sure we are not suspending game
|
// make sure we are not suspending game
|
||||||
features::empty_session::deactivate(&mut self.game_handle);
|
features::empty_session::deactivate(&mut self.game_handle);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
use crate::{
|
||||||
|
gui::{
|
||||||
|
app::{App, WINDOW_SIZE},
|
||||||
|
tools,
|
||||||
|
},
|
||||||
|
util::consts::{
|
||||||
|
APP_STORAGE_PATH,
|
||||||
|
game::{EXE_ENHANCED, EXE_LEGACY},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use eframe::egui;
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
pub fn show_debug_viewport(&mut self, ctx: &egui::Context) {
|
||||||
|
let main = ctx.input(|i| i.viewport().outer_rect.unwrap_or(egui::Rect::EVERYTHING));
|
||||||
|
let builder = egui::ViewportBuilder::default()
|
||||||
|
.with_title("GTA Tools Debug")
|
||||||
|
.with_minimize_button(false)
|
||||||
|
.with_maximize_button(false)
|
||||||
|
.with_inner_size(WINDOW_SIZE)
|
||||||
|
.with_position([main.right() - 12.0, main.min.y])
|
||||||
|
.with_icon(tools::load_icon());
|
||||||
|
ctx.show_viewport_immediate(
|
||||||
|
egui::ViewportId::from_hash_of("debug_viewport"),
|
||||||
|
builder,
|
||||||
|
|ctx, _class| {
|
||||||
|
if tools::debug_keycombo_pressed(ctx) {
|
||||||
|
self.flags.debug = !self.flags.debug;
|
||||||
|
}
|
||||||
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
|
egui::ScrollArea::both()
|
||||||
|
.auto_shrink([false, true])
|
||||||
|
.show(ui, |ui| {
|
||||||
|
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:#?}")));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
mod app;
|
mod app;
|
||||||
|
mod colours;
|
||||||
|
mod debug;
|
||||||
pub mod run;
|
pub mod run;
|
||||||
pub mod settings;
|
pub mod settings;
|
||||||
mod tools;
|
mod tools;
|
||||||
|
|||||||
+13
-4
@@ -21,26 +21,34 @@ fn panic_hook(panic_info: &std::panic::PanicHookInfo<'_>) {
|
|||||||
file.write_all(message.as_bytes()).unwrap();
|
file.write_all(message.as_bytes()).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::unnecessary_wraps)]
|
|
||||||
fn app_creator(
|
fn app_creator(
|
||||||
cc: &eframe::CreationContext<'_>,
|
cc: &eframe::CreationContext<'_>,
|
||||||
) -> Result<Box<dyn eframe::App>, Box<dyn std::error::Error + Send + Sync>> {
|
) -> Result<Box<dyn eframe::App>, Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
// use our own panic hook which logs all panics to a file
|
||||||
std::panic::set_hook(Box::new(panic_hook));
|
std::panic::set_hook(Box::new(panic_hook));
|
||||||
|
// initialize App early to modify some things before returning it
|
||||||
let mut app = Box::<App>::default();
|
let mut app = Box::<App>::default();
|
||||||
|
// load previously selected launch platform & settings from persistent state
|
||||||
if let Some(persistent_state) = PersistentState::get() {
|
if let Some(persistent_state) = PersistentState::get() {
|
||||||
app.launch.selected = persistent_state.launcher;
|
app.launch.selected = persistent_state.launcher;
|
||||||
app.settings = persistent_state.settings;
|
app.settings = persistent_state.settings;
|
||||||
}
|
}
|
||||||
let elevated = util::win::is_elevated();
|
// check if we're elevated. if not, and the user wants an elevated launch - relaunch elevated
|
||||||
if app.settings.start_elevated && !elevated {
|
if !app.flags.elevated && app.settings.start_elevated {
|
||||||
util::win::elevate(util::win::ElevationExitMethod::Forced);
|
util::win::elevate(util::win::ElevationExitMethod::Forced);
|
||||||
}
|
}
|
||||||
app.flags.elevated = elevated;
|
// refresh sysinfo because it initializes with nothing
|
||||||
|
app.sysinfo.refresh_all();
|
||||||
|
// enable image loading support in egui
|
||||||
egui_extras::install_image_loaders(&cc.egui_ctx);
|
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());
|
||||||
|
// apply some global styling that we like
|
||||||
cc.egui_ctx.style_mut(|style| {
|
cc.egui_ctx.style_mut(|style| {
|
||||||
style.spacing.item_spacing = egui::vec2(4.0, 4.0);
|
style.spacing.item_spacing = egui::vec2(4.0, 4.0);
|
||||||
style.interaction.selectable_labels = false;
|
style.interaction.selectable_labels = false;
|
||||||
});
|
});
|
||||||
|
// load any extra fonts that we need
|
||||||
let mut fonts = egui::FontDefinitions::default();
|
let mut fonts = egui::FontDefinitions::default();
|
||||||
fonts.font_data.insert(
|
fonts.font_data.insert(
|
||||||
"Ubuntu-Regular".to_owned(),
|
"Ubuntu-Regular".to_owned(),
|
||||||
@@ -51,6 +59,7 @@ fn app_creator(
|
|||||||
vec!["Ubuntu-Regular".to_owned()],
|
vec!["Ubuntu-Regular".to_owned()],
|
||||||
);
|
);
|
||||||
cc.egui_ctx.set_fonts(fonts);
|
cc.egui_ctx.set_fonts(fonts);
|
||||||
|
// finally return the App
|
||||||
Ok(app)
|
Ok(app)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-22
@@ -1,13 +1,15 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fmt::Display;
|
use strum::{Display, EnumIter};
|
||||||
use strum::EnumIter;
|
|
||||||
|
|
||||||
#[allow(clippy::enum_variant_names)]
|
#[derive(Clone, Copy, Debug, Display, PartialEq, Eq, Serialize, Deserialize, EnumIter)]
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, EnumIter)]
|
|
||||||
pub enum Theme {
|
pub enum Theme {
|
||||||
|
#[strum(to_string = "Catppuccin Latte")]
|
||||||
CatppuccinLatte,
|
CatppuccinLatte,
|
||||||
|
#[strum(to_string = "Catppuccin Frappe")]
|
||||||
CatppuccinFrappe,
|
CatppuccinFrappe,
|
||||||
|
#[strum(to_string = "Catppuccin Macchiato")]
|
||||||
CatppuccinMacchiato,
|
CatppuccinMacchiato,
|
||||||
|
#[strum(to_string = "Catppuccin Mocha")]
|
||||||
CatppuccinMocha,
|
CatppuccinMocha,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,24 +24,6 @@ impl From<Theme> for catppuccin_egui::Theme {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Theme {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
let x = match self {
|
|
||||||
Self::CatppuccinLatte => "Catppuccin Latte",
|
|
||||||
Self::CatppuccinFrappe => "Catppuccin Frappe",
|
|
||||||
Self::CatppuccinMacchiato => "Catppuccin Macchiato",
|
|
||||||
Self::CatppuccinMocha => "Catppuccin Mocha",
|
|
||||||
};
|
|
||||||
write!(f, "{x}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Theme {
|
|
||||||
pub fn to_catppuccin(self) -> catppuccin_egui::Theme {
|
|
||||||
self.into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct Settings {
|
pub struct Settings {
|
||||||
pub theme: Theme,
|
pub theme: Theme,
|
||||||
|
|||||||
+5
-9
@@ -21,24 +21,20 @@ pub fn header(ui: &mut egui::Ui, text: &str) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_combo_box<E>(ui: &mut egui::Ui, current_value: &mut E, label: impl std::hash::Hash)
|
pub fn build_menu<E>(ui: &mut egui::Ui, current_value: &mut E)
|
||||||
where
|
where
|
||||||
E: strum::IntoEnumIterator + std::fmt::Display + std::cmp::PartialEq + Copy,
|
E: strum::IntoEnumIterator + std::fmt::Display + std::cmp::PartialEq + Copy,
|
||||||
{
|
{
|
||||||
egui::ComboBox::from_id_salt(label)
|
E::iter().for_each(|variant| {
|
||||||
.selected_text(current_value.to_string())
|
ui.selectable_value(current_value, variant, variant.to_string());
|
||||||
.show_ui(ui, |ui| {
|
|
||||||
E::iter().for_each(|v| {
|
|
||||||
ui.selectable_value(current_value, v, v.to_string());
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn check_debug_keycombo_pressed(ctx: &egui::Context) -> bool {
|
pub fn debug_keycombo_pressed(ctx: &egui::Context) -> bool {
|
||||||
ctx.input(|i| i.modifiers.all() && i.key_pressed(egui::Key::D))
|
ctx.input(|i| i.modifiers.all() && i.key_pressed(egui::Key::D))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn check_debug_viewport_close_button_pressed(ctx: &egui::Context) -> bool {
|
pub fn debug_viewport_close_pressed(ctx: &egui::Context) -> bool {
|
||||||
ctx.input(|i| {
|
ctx.input(|i| {
|
||||||
i.raw
|
i.raw
|
||||||
.viewports
|
.viewports
|
||||||
|
|||||||
+6
-5
@@ -1,9 +1,10 @@
|
|||||||
use std::{path::PathBuf, sync::LazyLock};
|
use std::{path::PathBuf, sync::LazyLock};
|
||||||
|
|
||||||
pub const ENHANCED: &str = "GTA5_Enhanced.exe";
|
|
||||||
pub const LEGACY: &str = "GTA5.exe";
|
|
||||||
|
|
||||||
pub const GTA_WINDOW_TITLE: &str = "Grand Theft Auto V";
|
|
||||||
|
|
||||||
pub static APP_STORAGE_PATH: LazyLock<PathBuf> =
|
pub static APP_STORAGE_PATH: LazyLock<PathBuf> =
|
||||||
LazyLock::new(|| dirs::config_local_dir().unwrap().join("GTA Tools"));
|
LazyLock::new(|| dirs::config_local_dir().unwrap().join("GTA Tools"));
|
||||||
|
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ use std::time::{Duration, Instant};
|
|||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Countdown {
|
pub struct Countdown {
|
||||||
pub i: u64,
|
i: u64,
|
||||||
pub i_original: u64,
|
i_original: u64,
|
||||||
pub i_string: String,
|
pub i_string: String,
|
||||||
pub interval: Instant,
|
interval: Instant,
|
||||||
pub first_count: bool,
|
first_count: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Countdown {
|
impl Countdown {
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
pub mod codeberg;
|
mod codeberg;
|
||||||
pub mod consts;
|
pub mod consts;
|
||||||
pub mod countdown;
|
pub mod countdown;
|
||||||
pub mod meta;
|
pub mod meta;
|
||||||
|
|||||||
+3
-4
@@ -4,7 +4,7 @@ use windows::{
|
|||||||
Security::{GetTokenInformation, TOKEN_ELEVATION, TOKEN_QUERY, TokenElevation},
|
Security::{GetTokenInformation, TOKEN_ELEVATION, TOKEN_QUERY, TokenElevation},
|
||||||
System::Threading::{GetCurrentProcess, OpenProcessToken},
|
System::Threading::{GetCurrentProcess, OpenProcessToken},
|
||||||
UI::{
|
UI::{
|
||||||
Input::KeyboardAndMouse::GetAsyncKeyState,
|
Input::KeyboardAndMouse::{GetAsyncKeyState, VIRTUAL_KEY},
|
||||||
Shell::ShellExecuteW,
|
Shell::ShellExecuteW,
|
||||||
WindowsAndMessaging::{
|
WindowsAndMessaging::{
|
||||||
CURSOR_SHOWING, CURSORINFO, GetCursorInfo, GetForegroundWindow, GetWindowTextW,
|
CURSOR_SHOWING, CURSORINFO, GetCursorInfo, GetForegroundWindow, GetWindowTextW,
|
||||||
@@ -31,7 +31,6 @@ pub fn is_cursor_visible() -> bool {
|
|||||||
ci.flags == CURSOR_SHOWING
|
ci.flags == CURSOR_SHOWING
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::cast_sign_loss)]
|
|
||||||
pub fn is_window_focused(target_title: &str) -> bool {
|
pub fn is_window_focused(target_title: &str) -> bool {
|
||||||
let mut buffer = [0; 512];
|
let mut buffer = [0; 512];
|
||||||
unsafe {
|
unsafe {
|
||||||
@@ -42,9 +41,9 @@ pub fn is_window_focused(target_title: &str) -> bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_any_key_pressed(keys: &[u8]) -> bool {
|
pub fn is_any_key_pressed(keys: &[VIRTUAL_KEY]) -> bool {
|
||||||
keys.iter()
|
keys.iter()
|
||||||
.any(|&key| unsafe { (GetAsyncKeyState(i32::from(key)) & i16::MIN) != 0 })
|
.any(|&key| unsafe { (GetAsyncKeyState(i32::from(key.0)) & i16::MIN) != 0 })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn elevate(closing: ElevationExitMethod) {
|
pub fn elevate(closing: ElevationExitMethod) {
|
||||||
|
|||||||
Reference in New Issue
Block a user