Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3158f71fb | ||
|
|
070e477fd4 | ||
|
|
7745bba10e | ||
|
|
8d7a0d12b9 | ||
|
|
b325ebb03c | ||
|
|
38c6402a12 | ||
|
|
2d42c0c3c4 | ||
|
|
a423721aee | ||
|
|
f3436c8ddc | ||
|
|
afcf0a87c0 | ||
|
|
73d610949f | ||
|
|
7f358d9adb | ||
|
|
7caa5421f7 | ||
|
|
ecac0d54aa | ||
|
|
a84401ca33 | ||
|
|
1bcd6b2c7d | ||
|
|
1dbdc0e4cb | ||
|
|
3610e44a08 | ||
|
|
f5fd22d35c | ||
|
|
022828b802 | ||
|
|
262d2d0720 | ||
|
|
2326edf97b | ||
|
|
301be82837 | ||
|
|
0d7dde929d | ||
|
|
3ab802fd56 | ||
|
|
53030cc5f7 | ||
|
|
383224c2fa | ||
|
|
575a6a4f1d | ||
|
|
17579de277 | ||
|
|
00727b9913 | ||
|
|
1f6fc22525 | ||
|
|
27b09be76e | ||
|
|
6f15e8c348 | ||
|
|
d294aa9861 | ||
|
|
f688f1c3c4 | ||
|
|
58ec9f6567 | ||
|
|
1c9ce2a4d3 | ||
|
|
8a5d6cf38c | ||
|
|
d789b0f62d | ||
|
|
6079a72f47 | ||
|
|
914ffe46d2 | ||
|
|
935703e1ab | ||
|
|
202554e03b | ||
|
|
da2b15a0b6 | ||
|
|
ccc27b1944 | ||
|
|
10168e84d1 | ||
|
|
e72964a9f8 | ||
|
|
18052d0c78 | ||
|
|
09f5a3895b | ||
|
|
89892ef592 | ||
|
|
61de3aeee3 | ||
|
|
4ed00e6ac3 | ||
|
|
332db0ea03 | ||
|
|
64d13e3964 | ||
|
|
feb3a793d9 | ||
|
|
585f454bc0 | ||
|
|
27524e855a | ||
|
|
0257834d04 | ||
|
|
64e94bf790 | ||
|
|
668a6eb38b | ||
|
|
0de79769d2 | ||
|
|
b79c5f62b3 | ||
|
|
2c3092026a | ||
|
|
69eda1103d | ||
|
|
19b1ade758 | ||
|
|
18f667a91a | ||
|
|
186371f835 | ||
|
|
ce2022db55 | ||
|
|
d992d6b8f6 | ||
|
|
b71dbbdf03 | ||
|
|
c85a169765 | ||
|
|
9df757c169 | ||
|
|
92cbec7934 | ||
|
|
41cfb12ea8 | ||
|
|
e328eb91f6 | ||
|
|
10c840ac4d | ||
|
|
2fc0aa2005 | ||
|
|
29f726b1bd | ||
|
|
16d3c2cd96 | ||
|
|
4ce7f3459d | ||
|
|
c8cbb51f43 | ||
|
|
9c53239276 | ||
|
|
af5ea259e9 | ||
|
|
162350699a | ||
|
|
2977398437 | ||
|
|
36dce5aa1e | ||
|
|
6935b852ce | ||
|
|
1c048e6aff | ||
|
|
84a659a2b1 | ||
|
|
db4ca22f95 | ||
|
|
284c4bb46d | ||
|
|
5fe697223c | ||
|
|
33fbbc7fc0 | ||
|
|
5bcdccdfea | ||
|
|
31ac09cd25 | ||
|
|
1283f34174 | ||
|
|
072c8ae406 | ||
|
|
e283393889 | ||
|
|
cf5c37c7dc | ||
|
|
c6cd0cf2f9 | ||
|
|
d182ca5318 | ||
|
|
42ee688639 | ||
|
|
ba49ed0e67 | ||
|
|
f9f326904e | ||
|
|
c3a1e864a7 | ||
|
|
7f795ba52e | ||
|
|
62efe3f411 | ||
|
|
4bb0f151a9 | ||
|
|
1aebc6b17c | ||
|
|
861ebe97d6 | ||
|
|
53b20a437a | ||
|
|
03ca861a09 | ||
|
|
a4ebfc515c | ||
|
|
52ffb34d83 | ||
|
|
6248e8379f | ||
|
|
99bc15b5ed | ||
|
|
e84d1347fd | ||
|
|
f49bb56398 | ||
|
|
99b6519ffb | ||
|
|
807cb5242f | ||
|
|
c1c1547e4d | ||
|
|
6e3aa70398 | ||
|
|
9331f79038 | ||
|
|
0ab7727425 | ||
|
|
c73dc4bd86 | ||
|
|
68cf28dad8 | ||
|
|
c284bad06b | ||
|
|
57db451958 | ||
|
|
186fcde20e |
Generated
+379
-1518
File diff suppressed because it is too large
Load Diff
+21
-5
@@ -1,19 +1,36 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "gta-tools"
|
name = "gta-tools"
|
||||||
version = "0.1.2"
|
version = "0.6.0"
|
||||||
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",
|
||||||
] }
|
] }
|
||||||
eframe = "0.31.1"
|
eframe = { version = "0.31.1", default-features = false, features = [
|
||||||
image = { version = "0.25.6", default-features = false }
|
"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"] }
|
||||||
|
nyquest = { version = "0.1.1", features = ["blocking", "json"] }
|
||||||
|
nyquest-backend-winrt = { version = "0.1.0", features = ["blocking"] }
|
||||||
open = "5.3.2"
|
open = "5.3.2"
|
||||||
sysinfo = "0.33.1"
|
semver = "1.0.26"
|
||||||
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
|
serde_json = "1.0.140"
|
||||||
|
strum = { version = "0.27.1", features = ["derive"] }
|
||||||
|
sysinfo = "0.35.0"
|
||||||
windows = { version = "0.61.1", features = [
|
windows = { version = "0.61.1", features = [
|
||||||
"Win32_UI_Input_KeyboardAndMouse",
|
"Win32_UI_Input_KeyboardAndMouse",
|
||||||
"Win32_System_Threading",
|
"Win32_System_Threading",
|
||||||
|
"Win32_UI_Shell",
|
||||||
|
"Win32_UI_WindowsAndMessaging",
|
||||||
|
"Win32_Security",
|
||||||
|
"Win32_NetworkManagement_WindowsFirewall",
|
||||||
] }
|
] }
|
||||||
winreg = "0.55.0"
|
winreg = "0.55.0"
|
||||||
|
|
||||||
@@ -22,6 +39,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
|
||||||
|
|||||||
@@ -0,0 +1,176 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
@@ -1,3 +1,61 @@
|
|||||||
more spaghetti than the nation of italy
|
# GTA Tools
|
||||||
|
|
||||||

|
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**.
|
||||||
|
|
||||||
|
## Guide
|
||||||
|
|
||||||
|
Almost every[^1] 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 use an up-to-date version of GTA Tools from [releases](https://codeberg.org/futile/gta-tools/releases). You can check for updates to your existing installation by going to the <kbd>About</kbd> page of GTA Tools and hovering the <kbd>↓</kbd> button near the version number.
|
||||||
|
|
||||||
|
#### Game
|
||||||
|
|
||||||
|
This section is quite simple.
|
||||||
|
|
||||||
|
There is a <kbd>Launch</kbd>[^1] feature, which will start your game on the chosen launcher (I am unsure if launching Epic Games works, let me know).
|
||||||
|
|
||||||
|
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`.
|
||||||
|
|
||||||
|
#### Session
|
||||||
|
|
||||||
|
This section also has two features.
|
||||||
|
|
||||||
|
The first being <kbd>Empty current session</kbd>, which "removes everyone" from your session by suspending your game for 10 seconds, and then resuming it. This method is exceedingly similar to the Resource Monitor method, but made convenient in one button. This can be useful in numerous ways:
|
||||||
|
* You are stuck loading into a session
|
||||||
|
* You are stuck loading into an interior
|
||||||
|
* You are about to get griefed while doing a freemode mission
|
||||||
|
* You want to quickly get a "new" session
|
||||||
|
|
||||||
|
The other feature is <kbd>Anti AFK</kbd>, which, when toggled, prevents you from getting kicked for idling by pressing keys on your keyboard every 60 seconds. At present, those keys are `VK_NUMPAD4` & `VK_NUMPAD6`, due to being a relatively good balance of keys that the game actually registers but don't interrupt gameplay too much if they activate while you're playing. You *must* be tabbed into GTA V for <kbd>Anti AFK</kbd> to work. While toggled, <kbd>Anti AFK</kbd> will only activate when all of the following conditions are true:
|
||||||
|
* The `Grand Theft Auto V` window is currently focused
|
||||||
|
* None of the designated keys are currently being pressed[^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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
[^1]: <kbd>Launch</kbd> only launches Enhanced installations of the game.
|
||||||
|
[^2]: Otherwise, if you are, for example, using those keys to fly a plane, <kbd>Anti AFK</kbd> activating could cancel the input.
|
||||||
|
[^3]: Specifically, the Windows mouse cursor, not the in-game mouse cursor. This check is mostly used to prevent typing while you are in the Rockstar overlay.
|
||||||
|
|||||||
Binary file not shown.
@@ -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 |
@@ -0,0 +1,6 @@
|
|||||||
|
set windows-shell := ["pwsh", "-NoLogo", "-NoProfileLoadTime", "-Command"]
|
||||||
|
|
||||||
|
install:
|
||||||
|
cargo build --release
|
||||||
|
cp .\target\release\gta-tools.exe ~\.cargo\bin
|
||||||
|
cp .\target\release\gta-tools.exe ~\Documents
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
pub mod anti_afk;
|
pub mod anti_afk;
|
||||||
pub mod empty_session;
|
pub mod empty_session;
|
||||||
pub mod force_close;
|
pub mod force_close;
|
||||||
|
pub mod game_networking;
|
||||||
pub mod launch;
|
pub mod launch;
|
||||||
+39
-19
@@ -1,11 +1,14 @@
|
|||||||
|
use crate::util::{self, consts::game::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, 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 VK_SHIFT: u8 = 16;
|
const PRESS_KEYS: [VIRTUAL_KEY; 2] = [VK_NUMPAD4, VK_NUMPAD6];
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct AntiAfk {
|
pub struct AntiAfk {
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
pub interval: Instant,
|
pub interval: Instant,
|
||||||
@@ -20,23 +23,40 @@ impl Default for AntiAfk {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send(vk_code: u8) {
|
impl AntiAfk {
|
||||||
unsafe {
|
pub fn can_activate(&self) -> bool {
|
||||||
keybd_event(
|
use util::win::{is_any_key_pressed, is_cursor_visible, is_window_focused};
|
||||||
vk_code,
|
is_window_focused(WINDOW_TITLE) && !is_any_key_pressed(&PRESS_KEYS) && !is_cursor_visible()
|
||||||
u8::try_from(MapVirtualKeyW(u32::from(vk_code), MAP_VIRTUAL_KEY_TYPE(0))).unwrap(),
|
}
|
||||||
KEYBD_EVENT_FLAGS(0),
|
|
||||||
0,
|
pub fn should_activate(&self) -> bool {
|
||||||
);
|
self.enabled && self.interval.elapsed() >= INTERVAL
|
||||||
keybd_event(
|
}
|
||||||
vk_code,
|
|
||||||
u8::try_from(MapVirtualKeyW(u32::from(vk_code), MAP_VIRTUAL_KEY_TYPE(0))).unwrap(),
|
pub fn activate(&mut self) {
|
||||||
KEYBD_EVENT_FLAGS(2),
|
send(&PRESS_KEYS);
|
||||||
0,
|
self.interval = Instant::now();
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn activate() {
|
fn send(vk_codes: &[VIRTUAL_KEY]) {
|
||||||
send(VK_SHIFT);
|
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 };
|
||||||
|
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) };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
use crate::{features, gui::App, util::Countdown};
|
use crate::util::{
|
||||||
|
consts::game::{EXE_ENHANCED, EXE_LEGACY},
|
||||||
|
countdown::Countdown,
|
||||||
|
};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use sysinfo::System;
|
use sysinfo::System;
|
||||||
use windows::Win32::{
|
use windows::Win32::{
|
||||||
@@ -6,12 +9,11 @@ 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);
|
||||||
const ENHANCED: &str = "GTA5_Enhanced.exe";
|
|
||||||
const LEGACY: &str = "GTA5.exe";
|
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct EmptySession {
|
pub struct EmptySession {
|
||||||
pub enabled: bool,
|
pub disabled: bool,
|
||||||
pub interval: Instant,
|
pub interval: Instant,
|
||||||
pub countdown: Countdown,
|
pub countdown: Countdown,
|
||||||
}
|
}
|
||||||
@@ -19,17 +21,31 @@ pub struct EmptySession {
|
|||||||
impl Default for EmptySession {
|
impl Default for EmptySession {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
enabled: true,
|
disabled: false,
|
||||||
interval: Instant::now(),
|
interval: Instant::now(),
|
||||||
countdown: Countdown::new(features::empty_session::INTERVAL.as_secs() as usize),
|
countdown: Countdown::new(INTERVAL.as_secs()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
@@ -37,27 +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(app: &mut App) {
|
pub fn activate(game_handle: &mut HANDLE, sysinfo: &mut System) {
|
||||||
let pid = get_gta_pid(&mut app.sysinfo);
|
let pid = get_gta_pid(sysinfo);
|
||||||
if pid == u32::MAX {
|
if pid == u32::MAX {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
unsafe {
|
unsafe {
|
||||||
app.game_handle = OpenProcess(PROCESS_SUSPEND_RESUME, false, pid).unwrap();
|
*game_handle = OpenProcess(PROCESS_SUSPEND_RESUME, false, pid).unwrap();
|
||||||
let _ = NtSuspendProcess(app.game_handle);
|
let _ = NtSuspendProcess(*game_handle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deactivate(app: &mut App) {
|
pub fn deactivate(game_handle: &mut HANDLE) {
|
||||||
unsafe {
|
unsafe {
|
||||||
let _ = NtResumeProcess(app.game_handle);
|
if !game_handle.is_invalid() {
|
||||||
let _ = CloseHandle(app.game_handle);
|
let _ = NtResumeProcess(*game_handle);
|
||||||
|
let _ = CloseHandle(*game_handle);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+42
-10
@@ -1,32 +1,64 @@
|
|||||||
use std::time::Instant;
|
use crate::util::consts::game::{EXE_ENHANCED, EXE_LEGACY};
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
use sysinfo::System;
|
use sysinfo::System;
|
||||||
|
|
||||||
const ENHANCED: &str = "GTA5_Enhanced.exe";
|
const INTERVAL: Duration = Duration::from_secs(3);
|
||||||
const LEGACY: &str = "GTA5.exe";
|
|
||||||
|
|
||||||
|
#[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_string(),
|
button_text: "Force close game".to_owned(),
|
||||||
prompting: false,
|
timer: Instant::now(),
|
||||||
interval: Instant::now(),
|
counting: false,
|
||||||
|
current_frame: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn activate(sysinfo: &mut System) {
|
impl ForceClose {
|
||||||
|
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.timer = Instant::now();
|
||||||
|
self.counting = true;
|
||||||
|
self.current_frame = true;
|
||||||
|
}
|
||||||
|
if self.counting && self.timer.elapsed() >= INTERVAL {
|
||||||
|
self.reset();
|
||||||
|
} else if force_close_button_clicked && !self.current_frame {
|
||||||
|
activate(sysinfo);
|
||||||
|
self.reset();
|
||||||
|
}
|
||||||
|
self.finish_current_frame();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
*self = Self::default();
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn finish_current_frame(&mut self) {
|
||||||
|
if self.current_frame {
|
||||||
|
self.current_frame = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
});
|
});
|
||||||
|
sysinfo.refresh_all();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
use crate::util::consts::game::{EXE_ENHANCED, EXE_LEGACY};
|
||||||
|
use std::{
|
||||||
|
path::Path,
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
use strum::{Display, EnumIter};
|
||||||
|
use sysinfo::System;
|
||||||
|
use windows::{
|
||||||
|
Win32::{
|
||||||
|
NetworkManagement::WindowsFirewall::{
|
||||||
|
INetFwPolicy2, INetFwRule, NET_FW_ACTION_BLOCK, NET_FW_IP_PROTOCOL_ANY,
|
||||||
|
NET_FW_RULE_DIR_IN, NET_FW_RULE_DIR_OUT, NetFwPolicy2, NetFwRule,
|
||||||
|
},
|
||||||
|
System::Com::{
|
||||||
|
CLSCTX_INPROC_SERVER, COINIT_MULTITHREADED, CoCreateInstance, CoInitializeEx,
|
||||||
|
CoUninitialize,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
core::BSTR,
|
||||||
|
};
|
||||||
|
|
||||||
|
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_boolean(&mut self) {
|
||||||
|
if self.blocked_status == BlockedStatus::Failed && !self.counting {
|
||||||
|
self.counting = true;
|
||||||
|
self.timer = Instant::now();
|
||||||
|
}
|
||||||
|
if self.blocked_status == BlockedStatus::Failed
|
||||||
|
&& self.counting
|
||||||
|
&& self.timer.elapsed() >= INTERVAL
|
||||||
|
{
|
||||||
|
self.counting = false;
|
||||||
|
self.blocked_status = Self::is_blocked().into();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_game_exe_path(sysinfo: &mut System) -> Option<&Path> {
|
||||||
|
sysinfo.refresh_all();
|
||||||
|
if let Some((_, process)) = sysinfo
|
||||||
|
.processes()
|
||||||
|
.iter()
|
||||||
|
.find(|(_, p)| p.name() == EXE_ENHANCED || p.name() == EXE_LEGACY)
|
||||||
|
{
|
||||||
|
process.exe()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
-14
@@ -1,26 +1,19 @@
|
|||||||
use std::{fmt::Display, path::PathBuf, process::Command};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::{path::PathBuf, process::Command};
|
||||||
|
use strum::{Display, EnumIter};
|
||||||
use winreg::{RegKey, enums::HKEY_LOCAL_MACHINE};
|
use winreg::{RegKey, enums::HKEY_LOCAL_MACHINE};
|
||||||
|
|
||||||
#[derive(Default, Debug, PartialEq, Eq)]
|
#[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 {
|
#[derive(Debug, Default)]
|
||||||
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(Default)]
|
|
||||||
pub struct Launch {
|
pub struct Launch {
|
||||||
pub selected: Platform,
|
pub selected: Platform,
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-170
@@ -1,170 +1,7 @@
|
|||||||
use crate::features::{
|
mod app;
|
||||||
self,
|
mod colours;
|
||||||
anti_afk::AntiAfk,
|
mod debug;
|
||||||
empty_session::EmptySession,
|
pub mod run;
|
||||||
force_close::ForceClose,
|
pub mod settings;
|
||||||
launch::{Launch, Platform},
|
mod tools;
|
||||||
};
|
mod ui_ext;
|
||||||
use eframe::egui;
|
|
||||||
use std::time::{Duration, Instant};
|
|
||||||
use sysinfo::System;
|
|
||||||
use windows::Win32::Foundation::HANDLE;
|
|
||||||
|
|
||||||
pub struct App {
|
|
||||||
pub current_frame: bool,
|
|
||||||
pub sysinfo: System,
|
|
||||||
pub game_handle: HANDLE,
|
|
||||||
pub launch: Launch,
|
|
||||||
pub force_close: ForceClose,
|
|
||||||
pub empty_session: EmptySession,
|
|
||||||
pub anti_afk: AntiAfk,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for App {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
current_frame: false,
|
|
||||||
sysinfo: System::new_all(),
|
|
||||||
game_handle: HANDLE::default(),
|
|
||||||
launch: Launch::default(),
|
|
||||||
force_close: ForceClose::default(),
|
|
||||||
empty_session: EmptySession::default(),
|
|
||||||
anti_afk: AntiAfk::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl eframe::App for App {
|
|
||||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
|
||||||
catppuccin_egui::set_theme(ctx, catppuccin_egui::MOCHA);
|
|
||||||
ctx.request_repaint_after(Duration::from_millis(100));
|
|
||||||
egui::CentralPanel::default().show(ctx, |ui| {
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.label("Game");
|
|
||||||
ui.add(egui::Separator::default().horizontal());
|
|
||||||
});
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
if ui.button("Launch").clicked() {
|
|
||||||
features::launch::launch(&self.launch.selected);
|
|
||||||
};
|
|
||||||
egui::ComboBox::from_label("")
|
|
||||||
.selected_text(self.launch.selected.to_string())
|
|
||||||
.width(125.0)
|
|
||||||
.show_ui(ui, |ui| {
|
|
||||||
ui.selectable_value(
|
|
||||||
&mut self.launch.selected,
|
|
||||||
Platform::Steam,
|
|
||||||
Platform::Steam.to_string(),
|
|
||||||
);
|
|
||||||
ui.selectable_value(
|
|
||||||
&mut self.launch.selected,
|
|
||||||
Platform::Rockstar,
|
|
||||||
Platform::Rockstar.to_string(),
|
|
||||||
);
|
|
||||||
ui.selectable_value(
|
|
||||||
&mut self.launch.selected,
|
|
||||||
Platform::Epic,
|
|
||||||
Platform::Epic.to_string(),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
let force_close_button = ui.button(&self.force_close.button_text);
|
|
||||||
if force_close_button.clicked() && !self.force_close.prompting {
|
|
||||||
self.force_close.button_text = "Are you sure?".to_string();
|
|
||||||
self.force_close.prompting = true;
|
|
||||||
self.force_close.interval = Instant::now();
|
|
||||||
self.current_frame = true;
|
|
||||||
};
|
|
||||||
if self.force_close.prompting
|
|
||||||
&& self.force_close.interval.elapsed() <= Duration::from_secs(3)
|
|
||||||
{
|
|
||||||
if force_close_button.clicked() && !self.current_frame {
|
|
||||||
features::force_close::activate(&mut self.sysinfo);
|
|
||||||
self.force_close = ForceClose::default();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
self.force_close = ForceClose::default();
|
|
||||||
}
|
|
||||||
if self.current_frame {
|
|
||||||
self.current_frame = false;
|
|
||||||
}
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.label("Session");
|
|
||||||
ui.add(egui::Separator::default().horizontal());
|
|
||||||
});
|
|
||||||
ui.add_enabled_ui(self.empty_session.enabled, |ui| {
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
if ui.button("Empty current session").clicked() {
|
|
||||||
self.empty_session.interval = Instant::now();
|
|
||||||
self.empty_session.enabled = false;
|
|
||||||
features::empty_session::activate(self);
|
|
||||||
}
|
|
||||||
if !self.empty_session.enabled {
|
|
||||||
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(self);
|
|
||||||
self.empty_session.enabled = true;
|
|
||||||
}
|
|
||||||
ui.label(&self.empty_session.countdown.i_string);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
ui.checkbox(&mut self.anti_afk.enabled, "Anti AFK")
|
|
||||||
.on_hover_text("You should be tabbed in\nwhile this is enabled.");
|
|
||||||
if self.anti_afk.enabled
|
|
||||||
&& self.anti_afk.interval.elapsed() >= features::anti_afk::INTERVAL
|
|
||||||
{
|
|
||||||
features::anti_afk::activate();
|
|
||||||
self.anti_afk.interval = Instant::now();
|
|
||||||
}
|
|
||||||
ui.with_layout(egui::Layout::bottom_up(egui::Align::Center), |ui| {
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.spacing_mut().item_spacing.x = 0.0;
|
|
||||||
ui.label("with love from ");
|
|
||||||
ui.hyperlink_to("futile", "http://futile.eu");
|
|
||||||
});
|
|
||||||
ui.separator();
|
|
||||||
ui.label(format!("v{}", env!("CARGO_PKG_VERSION")));
|
|
||||||
});
|
|
||||||
// ui.separator();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_icon() -> eframe::egui::IconData {
|
|
||||||
let (icon_rgba, icon_width, icon_height) = {
|
|
||||||
let icon = include_bytes!("../assets/icon.png");
|
|
||||||
let image = image::load_from_memory(icon).unwrap().into_rgba8();
|
|
||||||
let (width, height) = image.dimensions();
|
|
||||||
let rgba = image.into_raw();
|
|
||||||
(rgba, width, height)
|
|
||||||
};
|
|
||||||
|
|
||||||
eframe::egui::IconData {
|
|
||||||
rgba: icon_rgba,
|
|
||||||
width: icon_width,
|
|
||||||
height: icon_height,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run() {
|
|
||||||
let options = eframe::NativeOptions {
|
|
||||||
viewport: egui::ViewportBuilder::default()
|
|
||||||
.with_resizable(false)
|
|
||||||
.with_maximize_button(false)
|
|
||||||
.with_inner_size([250.0, 180.0])
|
|
||||||
.with_icon(load_icon()),
|
|
||||||
centered: true,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
eframe::run_native(
|
|
||||||
"GTA Tools",
|
|
||||||
options,
|
|
||||||
Box::new(|_cc| Ok(Box::<App>::default())),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|||||||
+267
@@ -0,0 +1,267 @@
|
|||||||
|
use crate::{
|
||||||
|
features,
|
||||||
|
gui::{
|
||||||
|
settings::{Settings, Theme},
|
||||||
|
tools,
|
||||||
|
ui_ext::UiExt,
|
||||||
|
},
|
||||||
|
util::{consts::game::WINDOW_TITLE, meta::Meta, persistent_state::PersistentState, win},
|
||||||
|
};
|
||||||
|
use eframe::egui;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
use strum::{Display, EnumIter};
|
||||||
|
|
||||||
|
pub const WINDOW_SIZE: [f32; 2] = [240.0, 240.0];
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default, Display, PartialEq, Eq, EnumIter)]
|
||||||
|
enum Stage {
|
||||||
|
#[default]
|
||||||
|
Main,
|
||||||
|
Settings,
|
||||||
|
About,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Flags {
|
||||||
|
pub elevated: bool,
|
||||||
|
pub debug: bool,
|
||||||
|
closing: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Flags {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
elevated: win::is_elevated(),
|
||||||
|
debug: false,
|
||||||
|
closing: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct App {
|
||||||
|
pub meta: Meta,
|
||||||
|
pub settings: Settings,
|
||||||
|
stage: Stage,
|
||||||
|
pub flags: Flags,
|
||||||
|
pub sysinfo: sysinfo::System,
|
||||||
|
game_handle: windows::Win32::Foundation::HANDLE,
|
||||||
|
pub anti_afk: features::anti_afk::AntiAfk,
|
||||||
|
empty_session: features::empty_session::EmptySession,
|
||||||
|
force_close: features::force_close::ForceClose,
|
||||||
|
pub game_networking: features::game_networking::GameNetworking,
|
||||||
|
pub launch: features::launch::Launch,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl eframe::App for App {
|
||||||
|
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| {
|
||||||
|
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::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),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if tools::debug_keycombo_pressed(ctx) || tools::debug_viewport_close_pressed(ctx) {
|
||||||
|
self.flags.debug = !self.flags.debug;
|
||||||
|
}
|
||||||
|
if self.flags.debug {
|
||||||
|
self.show_debug_viewport(ctx);
|
||||||
|
}
|
||||||
|
if self.flags.closing {
|
||||||
|
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
fn show_game_section(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) {
|
||||||
|
ui.header("Game");
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
if ui.button("Launch").clicked() {
|
||||||
|
features::launch::launch(&self.launch.selected);
|
||||||
|
}
|
||||||
|
egui::ComboBox::from_id_salt("Launch")
|
||||||
|
.selected_text(self.launch.selected.to_string())
|
||||||
|
.width(120.0)
|
||||||
|
.show_ui(ui, |ui| {
|
||||||
|
ui.build_menu(&mut self.launch.selected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
let force_close_button = ui.add_sized(
|
||||||
|
[104.0, 0.0],
|
||||||
|
egui::Button::new(&self.force_close.button_text),
|
||||||
|
);
|
||||||
|
self.force_close
|
||||||
|
.prompt(force_close_button.clicked(), &mut self.sysinfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_session_section(&mut self, _ctx: &egui::Context, 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() {
|
||||||
|
self.empty_session.interval = Instant::now();
|
||||||
|
self.empty_session.disabled = true;
|
||||||
|
features::empty_session::activate(&mut self.game_handle, &mut self.sysinfo);
|
||||||
|
}
|
||||||
|
ui.label(&self.empty_session.countdown.i_string);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.checkbox(&mut self.anti_afk.enabled, "Anti AFK")
|
||||||
|
.on_hover_text("You should be tabbed in\nfor this to work.");
|
||||||
|
if self.anti_afk.enabled {
|
||||||
|
ui.add_space(8.0);
|
||||||
|
ui.add_enabled_ui(false, |ui| {
|
||||||
|
ui.label(if win::is_window_focused(WINDOW_TITLE) {
|
||||||
|
"GTA is focused."
|
||||||
|
} else {
|
||||||
|
"GTA is not focused!"
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if self.anti_afk.can_activate() && self.anti_afk.should_activate() {
|
||||||
|
self.anti_afk.activate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_network_section(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) {
|
||||||
|
ui.header("Network");
|
||||||
|
egui::Frame::new()
|
||||||
|
.outer_margin(egui::vec2(0.0, -2.0))
|
||||||
|
.show(ui, |ui| {
|
||||||
|
let response = ui.add_enabled_ui(self.flags.elevated, |ui| {
|
||||||
|
let label = ui.horizontal(|ui| {
|
||||||
|
let label = ui.label("Game's network access");
|
||||||
|
ui.add_space(1.0);
|
||||||
|
ui.create_indicator_dot(self.game_networking.blocked_status)
|
||||||
|
.on_hover_text(
|
||||||
|
"This turns yellow if GTA Tools\ncannot find your game.",
|
||||||
|
);
|
||||||
|
self.game_networking.if_failed_return_to_boolean();
|
||||||
|
label
|
||||||
|
});
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
self.game_networking.block_all(&mut self.sysinfo);
|
||||||
|
}
|
||||||
|
if ui
|
||||||
|
.add_sized([button_width, 18.0], egui::Button::new("Unblock"))
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
self.game_networking.unblock_all();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
response.response.on_disabled_hover_text(
|
||||||
|
"This requires administrator.\nUse the Elevate button.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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| {
|
||||||
|
ui.build_menu(&mut self.settings.theme);
|
||||||
|
});
|
||||||
|
if selection != self.settings.theme {
|
||||||
|
catppuccin_egui::set_theme(ctx, self.settings.theme.into());
|
||||||
|
}
|
||||||
|
if ui
|
||||||
|
.add_visible(self.settings.theme == Theme::Auto, egui::Button::new("⟲"))
|
||||||
|
.on_hover_text("Refresh theme")
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
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.horizontal(|ui| {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.spacing_mut().item_spacing.x = 0.0;
|
||||||
|
ui.label("with ");
|
||||||
|
ui.hyperlink_to("❤", "https://codeberg.org/futile/gta-tools");
|
||||||
|
ui.label(" from ");
|
||||||
|
ui.hyperlink_to("futile", "http://futile.eu");
|
||||||
|
});
|
||||||
|
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||||
|
ui.label(format!(
|
||||||
|
"v{} {}",
|
||||||
|
self.meta.current_version,
|
||||||
|
if cfg!(debug_assertions) { "(dev)" } else { "" }
|
||||||
|
));
|
||||||
|
let button = ui.add_enabled_ui(self.meta.newer_version_available, |ui| {
|
||||||
|
ui.style_mut().spacing.button_padding = egui::Vec2::new(3.0, 0.0);
|
||||||
|
ui.button("⬇")
|
||||||
|
.on_disabled_hover_text("Already up to date.")
|
||||||
|
.on_hover_text(format!(
|
||||||
|
"New version available! ({})",
|
||||||
|
self.meta.latest_release.version
|
||||||
|
))
|
||||||
|
});
|
||||||
|
if button.inner.clicked() {
|
||||||
|
open::that(&self.meta.latest_release.download_url).unwrap();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.add(egui::Image::new(egui::include_image!(
|
||||||
|
"../../assets/icon.png"
|
||||||
|
)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for App {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
// save any persistent state to config file
|
||||||
|
PersistentState {
|
||||||
|
launcher: self.launch.selected,
|
||||||
|
settings: self.settings.clone(),
|
||||||
|
}
|
||||||
|
.set();
|
||||||
|
// make sure we are not suspending game
|
||||||
|
features::empty_session::deactivate(&mut self.game_handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
use crate::{
|
||||||
|
gui::{
|
||||||
|
app::{App, WINDOW_SIZE},
|
||||||
|
tools,
|
||||||
|
ui_ext::UiExt,
|
||||||
|
},
|
||||||
|
util::consts::{
|
||||||
|
APP_STORAGE_PATH,
|
||||||
|
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(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| {
|
||||||
|
ui.build_menu(&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:#?}")));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show_debug_viewport(&mut self, ctx: &egui::Context) {
|
||||||
|
let main = ctx.input(|i| i.viewport().outer_rect.unwrap_or(egui::Rect::EVERYTHING));
|
||||||
|
let builder = egui::ViewportBuilder::default()
|
||||||
|
.with_title("GTA Tools Debug")
|
||||||
|
.with_minimize_button(false)
|
||||||
|
.with_maximize_button(false)
|
||||||
|
.with_inner_size(WINDOW_SIZE)
|
||||||
|
.with_position([main.right() - 12.0, main.min.y])
|
||||||
|
.with_icon(tools::load_icon());
|
||||||
|
ctx.show_viewport_immediate(
|
||||||
|
egui::ViewportId::from_hash_of("debug_viewport"),
|
||||||
|
builder,
|
||||||
|
|ctx, _class| {
|
||||||
|
if tools::debug_keycombo_pressed(ctx) {
|
||||||
|
self.flags.debug = !self.flags.debug;
|
||||||
|
}
|
||||||
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
|
egui::ScrollArea::both()
|
||||||
|
.auto_shrink([false, true])
|
||||||
|
.show(ui, |ui| self.add_debug_viewport_contents(ui));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
use crate::{
|
||||||
|
gui::{
|
||||||
|
app::{App, WINDOW_SIZE},
|
||||||
|
tools,
|
||||||
|
},
|
||||||
|
util::{consts::APP_STORAGE_PATH, persistent_state::PersistentState, win},
|
||||||
|
};
|
||||||
|
use eframe::egui;
|
||||||
|
use std::{
|
||||||
|
fs::File,
|
||||||
|
io::Write,
|
||||||
|
time::{SystemTime, UNIX_EPOCH},
|
||||||
|
};
|
||||||
|
|
||||||
|
fn panic_hook(panic_info: &std::panic::PanicHookInfo<'_>) {
|
||||||
|
let log_path = APP_STORAGE_PATH.join("panic.log");
|
||||||
|
let mut file = File::options()
|
||||||
|
.create(true)
|
||||||
|
.append(true)
|
||||||
|
.open(log_path)
|
||||||
|
.unwrap();
|
||||||
|
let timestamp = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_secs();
|
||||||
|
let backtrace = std::backtrace::Backtrace::capture();
|
||||||
|
let message = format!("[{timestamp}]\n{panic_info}\nstack backtrace:\n{backtrace}\n");
|
||||||
|
file.write_all(message.as_bytes()).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn app_creator(
|
||||||
|
cc: &eframe::CreationContext<'_>,
|
||||||
|
) -> Result<Box<dyn eframe::App>, Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
// use our own panic hook which logs all panics to a file
|
||||||
|
std::panic::set_hook(Box::new(panic_hook));
|
||||||
|
// initialize http client (nyquest) for windows
|
||||||
|
nyquest_backend_winrt::register();
|
||||||
|
// 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() {
|
||||||
|
app.launch.selected = persistent_state.launcher;
|
||||||
|
app.settings = persistent_state.settings;
|
||||||
|
}
|
||||||
|
// 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 sysinfo because it initializes with nothing
|
||||||
|
app.sysinfo.refresh_all();
|
||||||
|
// 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());
|
||||||
|
// 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;
|
||||||
|
});
|
||||||
|
// load any extra fonts that we need
|
||||||
|
let mut fonts = egui::FontDefinitions::default();
|
||||||
|
fonts.font_data.insert(
|
||||||
|
"Ubuntu-Regular".to_owned(),
|
||||||
|
egui::FontData::from_static(include_bytes!("../../assets/Ubuntu-Regular.ttf")).into(),
|
||||||
|
);
|
||||||
|
fonts.families.insert(
|
||||||
|
egui::FontFamily::Name("Ubuntu-Regular".into()),
|
||||||
|
vec!["Ubuntu-Regular".to_owned()],
|
||||||
|
);
|
||||||
|
cc.egui_ctx.set_fonts(fonts);
|
||||||
|
// finally return the App
|
||||||
|
Ok(app)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run() {
|
||||||
|
let options = eframe::NativeOptions {
|
||||||
|
viewport: egui::ViewportBuilder::default()
|
||||||
|
.with_resizable(false)
|
||||||
|
.with_maximize_button(false)
|
||||||
|
.with_inner_size(WINDOW_SIZE)
|
||||||
|
.with_icon(tools::load_icon()),
|
||||||
|
centered: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
eframe::run_native("GTA Tools", options, Box::new(app_creator)).unwrap();
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
use crate::util::win;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use strum::{Display, EnumIter};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default, Display, PartialEq, Eq, Serialize, Deserialize, EnumIter)]
|
||||||
|
pub enum Theme {
|
||||||
|
#[default]
|
||||||
|
#[strum(to_string = "Auto")]
|
||||||
|
Auto,
|
||||||
|
#[strum(to_string = "Catppuccin Latte")]
|
||||||
|
CatppuccinLatte,
|
||||||
|
#[strum(to_string = "Catppuccin Frappe")]
|
||||||
|
CatppuccinFrappe,
|
||||||
|
#[strum(to_string = "Catppuccin Macchiato")]
|
||||||
|
CatppuccinMacchiato,
|
||||||
|
#[strum(to_string = "Catppuccin Mocha")]
|
||||||
|
CatppuccinMocha,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Theme> for catppuccin_egui::Theme {
|
||||||
|
fn from(val: Theme) -> Self {
|
||||||
|
match val {
|
||||||
|
Theme::Auto => {
|
||||||
|
if win::is_system_theme_dark() {
|
||||||
|
catppuccin_egui::MOCHA
|
||||||
|
} else {
|
||||||
|
catppuccin_egui::LATTE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Theme::CatppuccinLatte => catppuccin_egui::LATTE,
|
||||||
|
Theme::CatppuccinFrappe => catppuccin_egui::FRAPPE,
|
||||||
|
Theme::CatppuccinMacchiato => catppuccin_egui::MACCHIATO,
|
||||||
|
Theme::CatppuccinMocha => catppuccin_egui::MOCHA,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||||
|
pub struct Settings {
|
||||||
|
pub theme: Theme,
|
||||||
|
pub start_elevated: bool,
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
use eframe::egui;
|
||||||
|
|
||||||
|
pub fn load_icon() -> egui::IconData {
|
||||||
|
let icon = include_bytes!("../../assets/icon.png");
|
||||||
|
let image = image::load_from_memory(icon).unwrap().into_rgba8();
|
||||||
|
let (width, height) = image.dimensions();
|
||||||
|
egui::IconData {
|
||||||
|
rgba: image.into_raw(),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn debug_keycombo_pressed(ctx: &egui::Context) -> bool {
|
||||||
|
ctx.input(|i| i.modifiers.all() && i.key_pressed(egui::Key::D))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn debug_viewport_close_pressed(ctx: &egui::Context) -> bool {
|
||||||
|
ctx.input(|i| {
|
||||||
|
i.raw
|
||||||
|
.viewports
|
||||||
|
.get(&egui::ViewportId::from_hash_of("debug_viewport"))
|
||||||
|
.filter(|vp| vp.close_requested())
|
||||||
|
.is_some()
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
use eframe::egui;
|
||||||
|
|
||||||
|
pub trait UiExt {
|
||||||
|
fn header(&mut self, text: &str);
|
||||||
|
fn build_menu<E>(&mut self, current_value: &mut E)
|
||||||
|
where
|
||||||
|
E: strum::IntoEnumIterator + std::fmt::Display + std::cmp::PartialEq + Copy;
|
||||||
|
fn create_indicator_dot(&mut self, colour: impl Into<egui::Color32>) -> egui::Response;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UiExt for egui::Ui {
|
||||||
|
fn header(&mut self, text: &str) {
|
||||||
|
self.horizontal(|ui| {
|
||||||
|
ui.label(egui::RichText::new(text).font(egui::FontId::new(
|
||||||
|
12.5,
|
||||||
|
egui::FontFamily::Name("Ubuntu-Regular".into()),
|
||||||
|
)));
|
||||||
|
ui.add(egui::Separator::default().horizontal());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_menu<E>(&mut self, current_value: &mut E)
|
||||||
|
where
|
||||||
|
E: strum::IntoEnumIterator + std::fmt::Display + std::cmp::PartialEq + Copy,
|
||||||
|
{
|
||||||
|
E::iter().for_each(|variant| {
|
||||||
|
self.selectable_value(current_value, variant, variant.to_string());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_indicator_dot(&mut self, colour: impl Into<egui::Color32>) -> egui::Response {
|
||||||
|
self.add(
|
||||||
|
egui::Image::new(egui::include_image!("../../assets/circle.svg"))
|
||||||
|
.max_size([4.0, 4.0].into())
|
||||||
|
.tint(colour),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-1
@@ -1,9 +1,12 @@
|
|||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
compile_error!("This application must be compiled on Windows.");
|
||||||
|
|
||||||
mod features;
|
mod features;
|
||||||
mod gui;
|
mod gui;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
gui::run();
|
gui::run::run();
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-40
@@ -1,40 +1,6 @@
|
|||||||
use std::time::{Duration, Instant};
|
mod codeberg;
|
||||||
|
pub mod consts;
|
||||||
pub struct Countdown {
|
pub mod countdown;
|
||||||
pub i: usize,
|
pub mod meta;
|
||||||
pub i_original: usize,
|
pub mod persistent_state;
|
||||||
pub i_string: String,
|
pub mod win;
|
||||||
pub interval: Instant,
|
|
||||||
pub first_count: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Countdown {
|
|
||||||
pub fn new(i: usize) -> Self {
|
|
||||||
Self {
|
|
||||||
i,
|
|
||||||
i_original: i,
|
|
||||||
i_string: String::new(),
|
|
||||||
interval: Instant::now(),
|
|
||||||
first_count: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn reset(&mut self) {
|
|
||||||
*self = Self::new(self.i_original);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn count(&mut self) {
|
|
||||||
if self.first_count {
|
|
||||||
self.interval = Instant::now();
|
|
||||||
self.first_count = false;
|
|
||||||
}
|
|
||||||
if self.interval.elapsed() >= Duration::from_secs(1) {
|
|
||||||
self.i -= 1;
|
|
||||||
self.interval = Instant::now();
|
|
||||||
}
|
|
||||||
self.i_string = self.i.to_string();
|
|
||||||
if self.i == 0 {
|
|
||||||
self.reset();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
use nyquest::{ClientBuilder, blocking::Request};
|
||||||
|
use semver::Version;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
const APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
|
||||||
|
const CODEBERG_ENDPOINT_ROOT: &str = "https://codeberg.org/api/v1";
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Release {
|
||||||
|
pub version: Version,
|
||||||
|
pub download_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Release {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
version: Version::new(0, 0, 0),
|
||||||
|
download_url: String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct LatestRelease {
|
||||||
|
tag_name: String,
|
||||||
|
assets: Vec<Asset>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Asset {
|
||||||
|
browser_download_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_latest_release() -> Option<Release> {
|
||||||
|
let request_url = format!("{CODEBERG_ENDPOINT_ROOT}/repos/futile/gta-tools/releases/latest");
|
||||||
|
let client = ClientBuilder::default()
|
||||||
|
.user_agent(APP_USER_AGENT)
|
||||||
|
.build_blocking()
|
||||||
|
.unwrap();
|
||||||
|
let response = client.request(Request::get(request_url)).unwrap();
|
||||||
|
let json = response.json::<LatestRelease>().ok()?;
|
||||||
|
let tag_name = json.tag_name;
|
||||||
|
let browser_download_url = &json.assets[0].browser_download_url;
|
||||||
|
Some(Release {
|
||||||
|
version: Version::parse(&tag_name).ok()?,
|
||||||
|
download_url: browser_download_url.to_owned(),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
use std::{env, path::PathBuf, sync::LazyLock};
|
||||||
|
|
||||||
|
pub static APP_STORAGE_PATH: LazyLock<PathBuf> = LazyLock::new(|| {
|
||||||
|
PathBuf::from(env::var("LOCALAPPDATA").unwrap_or_else(|_| String::from("."))).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";
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Countdown {
|
||||||
|
i: u64,
|
||||||
|
i_original: u64,
|
||||||
|
pub i_string: String,
|
||||||
|
interval: Instant,
|
||||||
|
first_count: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Countdown {
|
||||||
|
pub fn new(i: u64) -> Self {
|
||||||
|
Self {
|
||||||
|
i,
|
||||||
|
i_original: i,
|
||||||
|
i_string: String::new(),
|
||||||
|
interval: Instant::now(),
|
||||||
|
first_count: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
*self = Self::new(self.i_original);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn count(&mut self) {
|
||||||
|
if self.first_count {
|
||||||
|
self.interval = Instant::now();
|
||||||
|
self.first_count = false;
|
||||||
|
}
|
||||||
|
if self.interval.elapsed() >= Duration::from_secs(1) {
|
||||||
|
self.i -= 1;
|
||||||
|
self.interval = Instant::now();
|
||||||
|
}
|
||||||
|
self.i_string = self.i.to_string();
|
||||||
|
if self.i == 0 {
|
||||||
|
self.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
use crate::util::codeberg;
|
||||||
|
use semver::Version;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Meta {
|
||||||
|
pub current_version: Version,
|
||||||
|
pub latest_release: codeberg::Release,
|
||||||
|
pub newer_version_available: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Meta {
|
||||||
|
fn default() -> Self {
|
||||||
|
let current_version = Version::parse(env!("CARGO_PKG_VERSION")).unwrap();
|
||||||
|
let latest_release = codeberg::get_latest_release().unwrap_or_default();
|
||||||
|
let newer_version_available = matches!(
|
||||||
|
¤t_version.cmp_precedence(&latest_release.version),
|
||||||
|
std::cmp::Ordering::Less
|
||||||
|
);
|
||||||
|
Self {
|
||||||
|
current_version,
|
||||||
|
latest_release,
|
||||||
|
newer_version_available,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
use crate::{features::launch::Platform, gui::settings::Settings, util::consts::APP_STORAGE_PATH};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::{
|
||||||
|
fs::{self, File},
|
||||||
|
io::Write,
|
||||||
|
path::PathBuf,
|
||||||
|
sync::LazyLock,
|
||||||
|
};
|
||||||
|
|
||||||
|
static CONFIG_PATH: LazyLock<PathBuf> = LazyLock::new(|| APP_STORAGE_PATH.join("config.json"));
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct PersistentState {
|
||||||
|
pub launcher: Platform,
|
||||||
|
pub settings: Settings,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PersistentState {
|
||||||
|
pub fn get() -> Option<Self> {
|
||||||
|
fs::read_to_string(CONFIG_PATH.as_path())
|
||||||
|
.ok()
|
||||||
|
.and_then(|config| serde_json::from_str::<Self>(&config).ok())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set(&self) {
|
||||||
|
let config_path = CONFIG_PATH.as_path();
|
||||||
|
let config_path_parent = config_path.parent().unwrap();
|
||||||
|
if !config_path_parent.exists() {
|
||||||
|
fs::create_dir(config_path_parent).unwrap();
|
||||||
|
}
|
||||||
|
let mut config_file = File::create(config_path).unwrap();
|
||||||
|
let json = serde_json::to_string_pretty(&self).unwrap();
|
||||||
|
config_file.write_all(json.as_bytes()).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
core::{HSTRING, PCWSTR},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub enum ElevationExitMethod<'a> {
|
||||||
|
Gentle(&'a mut bool),
|
||||||
|
Forced,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_cursor_visible() -> bool {
|
||||||
|
let mut ci = CURSORINFO {
|
||||||
|
cbSize: u32::try_from(std::mem::size_of::<CURSORINFO>()).unwrap(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
unsafe {
|
||||||
|
GetCursorInfo(&mut ci).unwrap();
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_any_key_pressed(keys: &[VIRTUAL_KEY]) -> bool {
|
||||||
|
keys.iter()
|
||||||
|
.any(|&key| unsafe { (GetAsyncKeyState(i32::from(key.0)) & i16::MIN) != 0 })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn elevate(closing: ElevationExitMethod) {
|
||||||
|
let exe = std::env::current_exe().unwrap();
|
||||||
|
unsafe {
|
||||||
|
ShellExecuteW(
|
||||||
|
None,
|
||||||
|
&HSTRING::from("runas"),
|
||||||
|
&HSTRING::from(exe.as_path()),
|
||||||
|
PCWSTR::null(),
|
||||||
|
PCWSTR::null(),
|
||||||
|
SW_NORMAL,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
match closing {
|
||||||
|
ElevationExitMethod::Gentle(closing) => *closing = true,
|
||||||
|
ElevationExitMethod::Forced => std::process::exit(0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_elevated() -> bool {
|
||||||
|
let mut token: HANDLE = HANDLE::default();
|
||||||
|
unsafe {
|
||||||
|
if OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token).is_err() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let mut elevation = TOKEN_ELEVATION::default();
|
||||||
|
let mut size = u32::try_from(std::mem::size_of::<TOKEN_ELEVATION>()).unwrap();
|
||||||
|
let result = GetTokenInformation(
|
||||||
|
token,
|
||||||
|
TokenElevation,
|
||||||
|
Some((&raw mut elevation).cast()),
|
||||||
|
size,
|
||||||
|
&mut size,
|
||||||
|
);
|
||||||
|
CloseHandle(token).unwrap();
|
||||||
|
result.is_ok() && elevation.TokenIsElevated != 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_system_theme_dark() -> bool {
|
||||||
|
use winreg::RegKey;
|
||||||
|
let hkcu = RegKey::predef(winreg::enums::HKEY_CURRENT_USER);
|
||||||
|
let Ok(subkey) =
|
||||||
|
hkcu.open_subkey("Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize")
|
||||||
|
else {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
let Ok(dword): Result<u32, std::io::Error> = subkey.get_value("AppsUseLightTheme") else {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
dword != 1
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user