266 Commits
Author SHA1 Message Date
futile fce32eab52 update readme 2026-07-11 08:01:25 +01:00
futile a70f868120 update readme 2026-07-11 07:50:57 +01:00
futile 9bc7ef4177 update readme 2026-07-11 07:41:23 +01:00
futile 6cb70e87af 0.12.0 2026-07-11 07:31:00 +01:00
futile 393588d8d0 update readme 2026-07-11 06:53:18 +01:00
futile f22040d8e3 update view source code url 2026-07-11 06:52:15 +01:00
futile 937d775f63 delete github workflows 2026-07-11 04:39:23 +01:00
futile 709b17d729 update screenshots 2026-07-11 04:36:52 +01:00
futile 9a18ea1576 fix width of save server ip box 2026-07-11 04:30:49 +01:00
futile 96c4fd80a4 optimise circle.svg, for some reason 2026-06-28 17:47:39 +01:00
futile 47b647784e revert "update logo"
This reverts commit 890ae36590.
2026-06-28 17:34:02 +01:00
futile 3cf1d708a8 update egui & co, and rip out catppuccin_egui (garbage) 2026-06-28 17:30:43 +01:00
futile e8df86c250 update dependencies 2026-06-28 16:33:21 +01:00
futile 40bb3e2cc2 update text a lil 2026-06-28 16:24:09 +01:00
futile 7c5e64774c fuck that file 2026-06-28 16:22:21 +01:00
futile a02293a518 update readme 2026-06-28 16:14:56 +01:00
futile 1f296c3880 remove conditional elevation 2026-06-28 16:14:49 +01:00
futile 054c86e2d2 show reset button only when save server ip is not the default value 2026-06-28 15:38:19 +01:00
futile eed72cdec9 firewall: save a to_string call 2026-01-16 16:10:49 +00:00
Sapphireandfutile 749bf9a7a7 cargo clippy 2025-12-12 22:54:00 +00:00
Sapphireandfutile f1f71e7533 Upload artifact 2025-12-12 22:54:00 +00:00
Sapphireandfutile 27eeb48044 Add CI 2025-12-12 22:54:00 +00:00
Sapphire 60f1868334 Use anyhow for Result type 2025-12-12 03:40:33 -06:00
futile 44e5997847 update todo 2025-12-12 09:39:56 +00:00
Sapphireandfutile 0384eee42c Improve logging 2025-12-12 09:36:21 +00:00
Sapphireandfutile 89a320fd0f logging: remove padding newlines 2025-12-12 09:36:21 +00:00
Sapphireandfutile 58fd6b7e9c game_networking: turn blocked into enum
Allows us to keep track of what's blocked without querying rules constantly.
2025-12-12 09:36:21 +00:00
futileandSapphire 53fd0112c2 apply review suggestions
Co-authored-by: Sapphire <imsapphire0@gmail.com>
2025-12-12 03:52:40 -05:00
futileandSapphire 23fd4b12f9 use Firewall in game_networking 2025-12-12 03:52:40 -05:00
futileandSapphire e0a5821431 add is_blocked method to Firewall 2025-12-12 03:52:40 -05:00
futileandSapphire 50b5388f8a use Firewall in empty_session 2025-12-12 03:52:40 -05:00
futileandSapphire d185ab1a57 add Firewall to App 2025-12-12 03:52:40 -05:00
futileandSapphire 4205a332a7 add firewall helper util 2025-12-12 03:52:40 -05:00
futileandSapphire ade95b5c65 empty session with firewall instead of suspend 2025-12-12 03:52:40 -05:00
futileandSapphire 54985d350a initialize COM in app_creator instead of local to module 2025-12-12 03:52:40 -05:00
futileandSapphire 97e1446996 move get_game_exe_path to be a method on SystemInfo 2025-12-12 03:52:40 -05:00
futile 746b36dfc0 add panic button (useful) 2025-12-12 05:08:56 +00:00
futile 200b92cd48 update todo 2025-12-11 05:58:39 +00:00
futile fb7e1f7864 apply (some) clippy suggestions 2025-12-11 02:44:40 +00:00
futile b0afa9de1a update todo 2025-12-11 02:35:19 +00:00
futile 890ae36590 update logo 2025-12-11 02:34:10 +00:00
futile 89eec435ff update static_vcruntime 2025-12-08 18:30:40 +00:00
futileandSapphire 2611dbfd3e only show save server ip input box sometimes
Co-authored-by: Sapphire <imsapphire0@gmail.com>
2025-12-01 07:34:21 +00:00
futile eafc9e1d10 update todo 2025-11-26 22:43:48 +00:00
futile 0e92c9908d update todo 2025-11-24 12:18:08 +00:00
futile 8ffc5efb8d bump tauri-winres 2025-11-22 14:20:31 +00:00
futile bd8f70ed5f update todo 2025-11-22 11:30:38 +00:00
futile 9227533303 update readme 2025-11-22 10:06:23 +00:00
futile 4b522d9b3f move "open storage path" feature from debug to settings 2025-11-22 09:47:44 +00:00
futile d393bbfc37 better error handling and variable name changes in launch 2025-11-22 09:39:14 +00:00
futile 7190dc05fe rename ensure_not_both_blocked_simultaneously to ensure_block_exclusivity 2025-11-22 09:27:03 +00:00
futile 23b6c879ae better error handling of block exclusivity 2025-11-22 09:27:02 +00:00
futile 660a478fbb greatly simplify blocked status by removing temporary failure state 2025-11-22 09:27:02 +00:00
futile 28486174f2 create block/unblock buttons more idiomatically 2025-11-22 09:27:01 +00:00
futile 0f65c76055 remove size limit of force close button, as it could be too large on various scaling options (why was it even sized) 2025-11-22 09:26:40 +00:00
futile 8943e61f3a 0.11.0 2025-11-21 12:08:02 +00:00
futile ed0a828fd4 ensure all filters do not use the same name (oops) 2025-11-21 11:56:52 +00:00
futile 96ef494443 let's check in this todo for fun 2025-11-21 11:21:17 +00:00
futile 9b5340a6c5 apply clippy suggestions 2025-11-21 11:19:24 +00:00
futile 74132ec322 heavily modify game_networking & fully implement save server blocking 2025-11-21 11:09:18 +00:00
futile a58606c91d nicen up game_networking 2025-11-21 10:01:41 +00:00
futile 47adf42a48 add settings support for alternative replay glitching 2025-11-20 17:46:58 +00:00
futile 0fb587027c nicen up settings 2025-11-20 16:02:03 +00:00
futile 0424b7b114 fix weird changing colour on futile link in about 2025-11-20 14:16:24 +00:00
futile e89c3acc06 change is_system_theme_dark to is_system_theme_light 2025-11-20 14:10:26 +00:00
futile ab64c29935 update readme 2025-11-08 13:54:42 +00:00
futile e216445c7f update readme screenshots 2025-11-07 12:48:05 +00:00
futile aeb99992bb 0.10.0 2025-11-07 12:26:12 +00:00
futile 8ab6e14560 show git commit hash as monospace 2025-11-07 11:59:57 +00:00
futile b9ebdfdb5c update force close game button width to fit new font 2025-11-07 11:56:26 +00:00
futile 2804812691 replace Ubuntu fonts with Inter 2025-11-07 11:51:38 +00:00
futile 606fb7e191 update readme 2025-11-07 11:37:13 +00:00
futile e2a779b7f5 redo links in about stage a bit 2025-11-07 11:35:56 +00:00
futile a8b04bb72b move colour constants from gui::colours to util::consts::colours 2025-11-07 00:34:42 +00:00
futile c34cf187d5 ensure persistent_state is easier and more infallible to add fields to 2025-11-06 23:55:04 +00:00
futile f07421e75a use newer syntax for cargo instructions in build.rs 2025-11-06 08:53:09 +00:00
futile 8b4a455dc1 make empty_session::activate simply return a bool 2025-11-06 08:45:50 +00:00
futile a1300f8d76 lowkey improve logging 2025-11-06 08:41:53 +00:00
futile 9e623219a5 shorten some paths in main.rs 2025-11-06 05:07:49 +00:00
futile 09e19b6e16 remove hardly useful "windows compilation only" warning 2025-11-06 05:06:57 +00:00
futile 4498c45965 apply clippy suggestion 2025-11-06 04:20:40 +00:00
futile af8a5b1746 persist anti afk toggle 2025-11-06 04:17:03 +00:00
futile 560d9659f4 make git hash embedding more reliable 2025-11-06 03:06:25 +00:00
futile bf086d0442 amend comment 2025-11-06 02:38:13 +00:00
futile a6ea8ad8e2 ignore return of NtResumeProcess in empty_session::deactivate to reduce crashing 2025-11-06 02:37:26 +00:00
futile 4c00d1846b add git commit hash when hovering version info 2025-11-05 00:33:17 +00:00
futile e5e335f4f3 apply clippy suggestion 2025-11-05 00:10:39 +00:00
futile 3ab5987b67 use more unwraps instead of let _ = 2025-11-05 00:08:53 +00:00
futile 2030796337 add description to Cargo.toml after making sure tauri-winres doesn't use it 2025-11-04 23:08:59 +00:00
futile 2edb4f492a get rid of justfile it sucks 2025-11-04 23:08:31 +00:00
futile 114292d912 use tauri-winres instead of winresource & specify some more fields in the windows resource in build.rs 2025-11-04 22:56:03 +00:00
futile 8adc1f49ad add license and author information to Cargo.toml 2025-11-04 20:15:09 +00:00
futile bb27334f57 0.9.0 2025-11-03 07:43:40 +00:00
futile 1e13905885 update codebase for github 2025-11-03 07:13:00 +00:00
futile 8fc73b2992 update readme for github 2025-11-03 06:56:28 +00:00
futile 43b72995dd create github workflow to build and release on tag push 2025-11-03 06:42:22 +00:00
futile d3473d46e3 create github workflow to rebuild and rerelease all existing tags 2025-11-03 05:26:46 +00:00
futile 1363f5b125 use match instead of if for LaunchVersion checks 2025-10-25 09:35:31 +01:00
futile d15b8070ec improve strings in util::system_info 2025-10-11 03:32:39 +01:00
futile efccd6ed50 update readme 2025-09-13 16:58:52 +01:00
futile 5c73ee7bd5 improve (tighten) scoping of unsafe blocks 2025-09-13 16:50:44 +01:00
futile 445b63fa4c small system_info improvements 2025-09-13 16:39:16 +01:00
futile 941868dcc8 use &[c_char] instead of &[i8] in c_char_arr_to_string 2025-09-13 12:02:19 +01:00
futile 762a3b9b26 update readme 2025-09-13 00:34:48 +01:00
futile 0cb211c103 0.8.0 2025-09-12 05:17:47 +01:00
futile 90b32c5c5a this couldn't hurt to be create_dir_all instead of create_dir 2025-09-12 05:12:11 +01:00
futile 71c3c69ea2 a few clippy fixes 2025-09-12 05:04:12 +01:00
futile b553e418f5 reduce clutter in system_info 2025-09-12 01:21:56 +01:00
futile ab026c0bc6 adapt codebase to own, new system info impl 2025-09-12 01:07:10 +01:00
futile 32e21f58a1 create a clone of sysinfo focused on only our needs 2025-09-12 00:52:19 +01:00
futile b0c339cc5f update readme 2025-09-09 14:09:17 +01:00
futile c2584b5d01 reimplement "update checking" in the form of a simple button that goes to latest release 2025-09-09 14:07:46 +01:00
futile 3322f9582f update readme 2025-09-09 03:16:00 +01:00
futile 5348a1d6ee rip out update checking (too unreliable) 2025-09-09 03:15:01 +01:00
futile 05ce68cb47 update readme 2025-09-09 03:03:36 +01:00
futile f63d24ea43 improve reliability and logging when running into game access denied issues 2025-09-09 03:03:32 +01:00
futile 356d5f17e5 me when i don't smoke the drugs that make you write retarded code 2025-09-08 14:32:36 +01:00
futile bc5e80aaca update readme 2025-09-06 21:49:44 +01:00
futile e050231692 update readme 2025-09-06 12:26:38 +01:00
futile 6e33536dfe 0.7.0 2025-09-06 12:11:39 +01:00
futile c36a41f919 remove extra release optimizations to save sanity on compile times 2025-09-06 12:11:20 +01:00
futile 660e7a874f improve reliability of panic hook by moving it to main and always creating its storage path 2025-09-06 12:10:38 +01:00
futile 72abda7352 partial fix some weird colouring that was introduced in egui 0.32 (?) 2025-09-05 17:12:38 +01:00
futile ebf704c6f4 update readme to reflect prior changes 2025-09-05 16:10:47 +01:00
futile 5c5e6d60a1 add option to launch legacy version of the game & fix epic games launch urls 2025-09-05 16:09:24 +01:00
futile 98a6545b27 update dependencies i think 2025-09-05 14:26:27 +01:00
futile a9a965462a random small fixes 2025-09-05 13:24:16 +01:00
futile 81b4b26f96 small grammar fix on non-windows compilation error 2025-09-04 09:44:07 +01:00
futile 06df466d24 switch back to ureq from nyquest for http client
note: even if this raises binary size by a significant amount, it should in theory be more reliable, so it is a worthy sacrifice.
2025-09-04 07:41:54 +01:00
futile b8c444dc5b move theme refresh button to debug menu 2025-09-04 06:21:27 +01:00
futile e848ca8d79 GET OUT 2025-09-04 05:36:00 +01:00
futile 0a831580e2 use reworked logging to log errors in util::meta::default 2025-05-16 10:10:16 +01:00
futile 10e69eaac3 rework (error) logging 2025-05-16 10:03:48 +01:00
futile 9dbfbc4419 rework storage paths a bit 2025-05-13 09:40:45 +01:00
futile ef0b6de6ba improve reliability of util::codeberg::get_latest_release 2025-05-13 09:40:18 +01:00
futile e7470bd694 update readme 2025-05-12 08:00:25 +01:00
futile 936f683385 update readme 2025-05-12 07:50:00 +01:00
futile b3158f71fb 0.6.0 2025-05-10 07:04:59 +01:00
futile 070e477fd4 derive default on some shit instead of manually 2025-05-10 06:57:26 +01:00
futile 7745bba10e improve styling in relation to windows theme 2025-05-10 06:44:58 +01:00
futile 8d7a0d12b9 don't force capture backtrace in panic hook & otherwise improve gui::run 2025-05-04 18:36:32 +01:00
futile b325ebb03c clippy stuff 2025-05-04 18:21:55 +01:00
futile 38c6402a12 split ui of App::show_debug_viewport into App::add_debug_viewport_contents 2025-05-04 18:01:50 +01:00
futile 2d42c0c3c4 improve util::codeberg 2025-05-04 17:52:50 +01:00
futile a423721aee bump nyquest and stop using .json() workaround 2025-05-04 17:52:06 +01:00
futile f3436c8ddc 0.5.4 2025-05-03 13:12:47 +01:00
futile afcf0a87c0 get sysinfo from crates again, instead of git repo 2025-05-03 13:12:14 +01:00
futile 73d610949f use nyquest instead of ureq for http client 2025-05-03 13:10:40 +01:00
futile 7f358d9adb change how module roots are organized 2025-05-01 11:23:27 +01:00
futile 7caa5421f7 eliminate chrono dependency by using unix timestamp in panic logs 2025-04-30 01:36:17 +01:00
futile ecac0d54aa eliminate dirs dependency by checking LOCALAPPDATA envvar instead 2025-04-30 00:53:27 +01:00
futile a84401ca33 fail compilation if compiling outside of windows 2025-04-30 00:48:44 +01:00
futile 1bcd6b2c7d fix duplicate inputs bug in features::anti_afk::send 2025-04-26 05:47:10 +01:00
futile 1dbdc0e4cb remove redundant method to_color32 2025-04-26 04:42:29 +01:00
futile 3610e44a08 create trait UiExt 2025-04-26 04:37:10 +01:00
futile f5fd22d35c redesign if_failed_return_to_unblocked to if_failed_return_to_boolean 2025-04-26 04:23:37 +01:00
futile 022828b802 update readme 2025-04-26 04:07:39 +01:00
futile 262d2d0720 update readme 2025-04-25 17:18:38 +00:00
futile 2326edf97b update readme with a guide 2025-04-24 23:43:52 +01:00
futile 301be82837 update screenshot in readme 2025-04-24 13:54:39 +01:00
futile 0d7dde929d 0.5.3 2025-04-24 13:24:35 +01:00
futile 3ab802fd56 make RED, YELLOW, and GREEN slightly brighter 2025-04-23 14:33:08 +01:00
futile 53030cc5f7 add debug menu feature to change blocked_status of GameNetworking 2025-04-23 14:32:29 +01:00
futile 383224c2fa update readme 2025-04-23 14:31:50 +01:00
futile 575a6a4f1d update readme 2025-04-22 20:03:09 +01:00
futile 17579de277 update readme 2025-04-22 19:57:09 +01:00
futile 00727b9913 add compilation instructions to the repo 2025-04-22 14:34:37 +01:00
futile 1f6fc22525 check for app elevation in Default of Flags 2025-04-21 12:38:20 +01:00
futile 27b09be76e update screenshot in readme 2025-04-20 12:06:57 +01:00
futile 6f15e8c348 0.5.2 2025-04-20 10:52:21 +01:00
futile d294aa9861 reduce amount of underlying dependencies 2025-04-20 10:48:32 +01:00
futile f688f1c3c4 strip = true doesn't do anything extra anymore 2025-04-20 10:15:33 +01:00
futile 58ec9f6567 clippy 2025-04-20 08:01:41 +01:00
futile 1c9ce2a4d3 remember to place to_color32 for BlockedStatus in the correct place 2025-04-20 07:57:41 +01:00
futile 8a5d6cf38c simplify features::game_networking 2025-04-20 07:56:54 +01:00
futile d789b0f62d rework networking indicator dot 2025-04-20 07:33:34 +01:00
futile 6079a72f47 use SendInput instead of keybd_event for anti afk 2025-04-20 07:07:51 +01:00
futile 914ffe46d2 add tooltip to game networking indicator 2025-04-20 06:55:43 +01:00
futile 935703e1ab listen to a few clippy lints 2025-04-20 06:19:29 +01:00
futile 202554e03b 0.5.1 2025-04-20 01:47:16 +01:00
futile da2b15a0b6 redesign features::force_close internals a bit 2025-04-20 01:42:06 +01:00
futile ccc27b1944 slightly tweak features::game_networking design & add visual indicator for blocking failing due to game being closed 2025-04-20 01:25:25 +01:00
futile 10168e84d1 update readme 2025-04-19 10:52:20 +01:00
futile e72964a9f8 0.5.0 2025-04-19 10:37:06 +01:00
futile 18052d0c78 add red/green circle indicator beside Block/Unblock buttons 2025-04-19 10:35:47 +01:00
futile 09f5a3895b use windows api instead of netsh command to manage game's network access 2025-04-19 10:34:10 +01:00
futile 89892ef592 change a couple of imports in gui::app 2025-04-19 07:20:49 +01:00
futile 61de3aeee3 move debug viewport code to own module 2025-04-19 06:48:56 +01:00
futile 4ed00e6ac3 don't assign PersistentState to variable when setting 2025-04-19 05:23:11 +01:00
futile 332db0ea03 document the process of gui::run::app_creator with comments 2025-04-19 05:18:04 +01:00
futile 64d13e3964 move EmptySession::run_timers back into main update loop so it actually runs 2025-04-19 05:02:24 +01:00
futile feb3a793d9 redesign how anti afk activates 2025-04-19 04:54:02 +01:00
futile 585f454bc0 hunt down some unused pubs 2025-04-19 04:45:47 +01:00
futile 27524e855a numerous improvements in gui and feature code 2025-04-19 04:28:24 +01:00
futile 0257834d04 add checkbox to debug to spoof new version of the app beign available 2025-04-18 20:32:39 +01:00
futile 64e94bf790 change how vks are stored 2025-04-18 05:26:28 +01:00
futile 668a6eb38b get rid of clippy-related allows 2025-04-18 04:44:11 +01:00
futile 0de79769d2 reorganize some consts 2025-04-18 03:17:40 +01:00
futile b79c5f62b3 update screenshot in readme 2025-04-18 03:09:14 +01:00
futile 2c3092026a 0.4.2 2025-04-18 02:44:11 +01:00
futile 69eda1103d improve panic hook 2025-04-18 01:50:19 +01:00
futile 19b1ade758 remove 1 to_string and replace some others with to_owned 2025-04-18 00:59:07 +01:00
futile 18f667a91a improve util::codeberg (error handling & general) 2025-04-18 00:43:40 +01:00
futile 186371f835 include new release's version in download hover 2025-04-17 22:36:10 +01:00
futile ce2022db55 get sysinfo from the git repo, to bump windows 2025-04-17 22:36:10 +01:00
futile d992d6b8f6 refactor gui module 2025-04-17 20:52:19 +01:00
futile b71dbbdf03 change for loop syntax in build_combo_box 2025-04-17 20:38:16 +01:00
futile c85a169765 move gui::persistent_state to util::persistent_state 2025-04-17 20:36:42 +01:00
futile 9df757c169 add button to download latest gta-tools exe 2025-04-17 20:28:25 +01:00
futile 92cbec7934 return window size to [240.0, 240.0] 2025-04-17 04:45:01 +01:00
futile 41cfb12ea8 fix some idiotic const use in features::empty_session 2025-04-17 04:42:31 +01:00
futile e328eb91f6 get CREATE_NO_WINDOW from windows crate 2025-04-17 04:40:06 +01:00
futile 10c840ac4d update readme 2025-04-17 04:26:19 +01:00
futile 2fc0aa2005 update screenshot in readme 2025-04-17 04:16:34 +01:00
futile 29f726b1bd add LICENSE 2025-04-17 03:42:22 +01:00
futile 16d3c2cd96 update justfile 2025-04-17 03:41:24 +01:00
futile 4ce7f3459d 0.4.1 2025-04-17 02:33:27 +01:00
futile c8cbb51f43 thgank you clippy that's very smar thtat's very clever 2025-04-17 02:27:10 +01:00
futile 9c53239276 remove an i32 cast from is_any_key_pressed 2025-04-17 02:25:07 +01:00
futile af5ea259e9 implicit type signature on buffer var in is_window_focused 2025-04-17 02:12:34 +01:00
futile 162350699a change flow of util::win::is_elevated 2025-04-17 02:01:31 +01:00
futile 2977398437 slightly reduce unsafe scopes 2025-04-17 01:48:45 +01:00
futile 36dce5aa1e reduce ai idiocy 2025-04-17 01:39:01 +01:00
futile 6935b852ce clippy 2025-04-17 01:31:49 +01:00
futile 1c048e6aff revert 33fbbc7f 2025-04-17 00:50:09 +01:00
futile 84a659a2b1 add button to debug to open app storage path 2025-04-17 00:49:20 +01:00
futile db4ca22f95 log panics to a file 2025-04-17 00:44:06 +01:00
futile 284c4bb46d add a mouse cursor check to anti afk activation 2025-04-16 23:59:50 +01:00
futile 5fe697223c 0.4.0 2025-04-11 17:27:28 +01:00
futile 33fbbc7fc0 change windows_subsystem = "windows" requirement to debug also 2025-04-11 17:27:22 +01:00
futile 5bcdccdfea use windows vkcodes instead of defining our own 2025-04-10 18:19:12 +01:00
futile 31ac09cd25 make the thing nicer 2025-04-10 18:14:24 +01:00
futile 1283f34174 make Frame of "Game's network access" area invisible 2025-04-10 17:40:40 +01:00
futile 072c8ae406 clippy 2025-04-10 17:28:58 +01:00
futile e283393889 move header 2025-04-10 17:23:22 +01:00
futile cf5c37c7dc add bolder ubuntu font for headers & move rest of setup code into app_creator 2025-04-10 17:19:42 +01:00
futile c6cd0cf2f9 reintroduce ScrollArea to CentralPanel and increase WINDOW_SIZE to accommodate 2025-04-10 16:52:20 +01:00
futile d182ca5318 implement ComboBox builder 2025-04-10 16:32:30 +01:00
futile 42ee688639 move persistent state into own file 2025-04-10 15:17:19 +01:00
futile ba49ed0e67 add basic settings 2025-04-10 13:56:00 +01:00
futile f9f326904e turn crate::gui into a dir module 2025-04-10 08:38:51 +01:00
futile c3a1e864a7 improvements 2025-04-10 08:26:24 +01:00
futile 7f795ba52e make label text not selectable 2025-04-10 07:48:58 +01:00
futile 62efe3f411 0.3.0 2025-04-09 16:39:25 +01:00
futile 4bb0f151a9 improve anti afk with added checks and different keys 2025-04-09 16:34:30 +01:00
futile 1aebc6b17c make empty session interval 10 seconds again 2025-04-08 17:27:28 +01:00
futile 861ebe97d6 build CONFIG_PATH more nicely and fix crash when minimizing with debug open 2025-04-08 17:27:03 +01:00
futile 53b20a437a some clippy fixes 2025-04-08 17:11:25 +01:00
futile 03ca861a09 change ui window size + ui spacing size & make an enum Copy 2025-04-08 15:45:18 +01:00
futile a4ebfc515c some more ui changes 2025-04-08 13:18:45 +01:00
futile 52ffb34d83 better comments in drop of App 2025-04-08 10:52:34 +01:00
futile 6248e8379f change ui stuff 2025-04-08 10:50:34 +01:00
futile 99bc15b5ed ensure game isn't suspended when we close 2025-04-08 09:24:01 +01:00
futile e84d1347fd fix stupid justfile 2025-04-08 09:23:29 +01:00
futile f49bb56398 0.2.1 2025-04-08 09:05:29 +01:00
futile 99b6519ffb don't std::process::exit 2025-04-08 09:02:16 +01:00
futile 807cb5242f implement some persistent state (namely selected game launcher) 2025-04-08 08:35:29 +01:00
futile c1c1547e4d improve debug experience 2025-04-08 07:52:55 +01:00
futile 6e3aa70398 simplify load_icon 2025-04-08 06:35:15 +01:00
futile 9331f79038 edit justfile 2025-04-08 03:25:33 +01:00
futile 0ab7727425 0.2.0 2025-04-07 16:02:45 +01:00
futile c73dc4bd86 some little fixes 2025-04-07 16:02:32 +01:00
futile 68cf28dad8 do so many things 2025-04-07 12:19:06 +01:00
futile c284bad06b change stuff and add stuff 2025-04-05 07:06:41 +01:00
futile 57db451958 move util.rs to util/countdown.rs 2025-03-31 12:43:54 +01:00
futile 186fcde20e add justfile 2025-03-31 12:20:39 +01:00
31 changed files with 2930 additions and 2401 deletions
+1
View File
@@ -1,2 +1,3 @@
/target
.vscode
.idea
Generated
+1244 -2069
View File
File diff suppressed because it is too large Load Diff
+28 -17
View File
@@ -1,27 +1,38 @@
[package]
name = "gta-tools"
version = "0.1.2"
version = "0.12.0"
edition = "2024"
license = "Apache-2.0"
authors = ["futile <git@futile.eu>"]
description = "A toolset of convenient things for GTA V Online."
[dependencies]
catppuccin-egui = { version = "5.5.0", default-features = false, features = [
"egui31",
anyhow = "1.0"
eframe = { version = "0.35", default-features = false, features = [
"glow",
"default_fonts",
] }
eframe = "0.31.1"
image = { version = "0.25.6", default-features = false }
open = "5.3.2"
sysinfo = "0.33.1"
windows = { version = "0.61.1", features = [
"Win32_UI_Input_KeyboardAndMouse",
egui_extras = { version = "0.35", default-features = false, features = [
"image",
"svg",
] }
humantime = "2.3.0"
image = { version = "0.25", default-features = false, features = ["png"] }
log = "0.4"
open = "5"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
strum = { version = "0.28", features = ["derive"] }
windows = { version = "0.62", features = [
"Win32_NetworkManagement_WindowsFirewall",
"Win32_System_Com",
"Win32_System_Diagnostics_ToolHelp",
"Win32_System_Threading",
"Win32_UI_Input_KeyboardAndMouse",
"Win32_UI_WindowsAndMessaging",
] }
winreg = "0.55.0"
winreg = "0.56"
[build-dependencies]
static_vcruntime = "2.0.0"
winresource = "0.1.20"
[profile.release]
strip = true
lto = true
codegen-units = 1
static_vcruntime = "3.0.0"
tauri-winres = "0.3"
+176
View File
@@ -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
+80 -2
View File
@@ -1,3 +1,81 @@
more spaghetti than the nation of italy
# GTA Tools
![](https://i.ibb.co/SwdWRfGj/gta-tools-WEQ02u-ROT5.png)
A toolset of convenient things for GTA V Online.
![](https://i.vgy.me/HVBklM.png)
![](https://i.vgy.me/yzRGjG.png)
> [!NOTE]
> I have been forced to move this project from [GitHub](https://github.com/elituf/gta-tools) to [here](https://gitlab.com/futile/gta-tools). Sorry for the inconvenience. From now on you may always find the project source code via the redirect at [futile.eu/gta-tools](https://futile.eu/gta-tools).
## Installing
**Option 1** — <ins>Download</ins>
Download the latest release [here](https://gitlab.com/futile/gta-tools/-/releases/permalink/latest/downloads/gta-tools.exe) and place it somewhere convenient for you, such as Documents. You could then make a shortcut titled "GTA Tools", and pin it to taskbar or Start.
**Option 2** — <ins>Build from source</ins>
You will need Rust, which you can get [here](https://rustup.rs). Then, clone this repo and navigate to it. At this point, you should probably `git checkout x.x.x`, where `x.x.x` is the latest tag. You can then run `cargo build --release`. Once you do that, you can use the binary located at `.\target\release\gta-tools.exe` in the same way as **Option 1**.
> [!NOTE]
> It is recommended to always use an up-to-date version of GTA Tools from [releases](https://gitlab.com/futile/gta-tools/-/releases). You can also easily access this repository by going to the <kbd>About</kbd> page of GTA Tools and clicking the <kbd>git</kbd> button beside the version number.
## Guide
Every feature of GTA Tools works for both the Legacy and Enhanced versions of the game. GTA Tools requires administrator access.
#### Game
This section is quite simple.
<kbd>Launch</kbd> — Starts your game on the chosen launcher and game version.
<kbd>Force close game</kbd> — Kills all game processes. The button requires a second press after the first one for confirmation. This *does not* touch Rockstar Games Launcher or any other processes, only ones named `GTA5_Enhanced.exe` or `GTA5.exe`.
#### Session
This section also has two features.
<kbd>Empty current session</kbd> — Clears your session of players[^1] by blocking the game's network access for 10 seconds, then unblocking it. This can be useful in numerous ways:
* You are stuck loading into a session
* You are stuck loading into an interior
* You are about to get griefed while doing a freemode mission
* You want to quickly get a "new" session
<kbd>Anti AFK</kbd> — Prevents you from getting kicked for idling by pressing keys on your keyboard every 60 seconds. At present, those keys are `VK_NUMPAD4` & `VK_NUMPAD6`, due to being a relatively good balance of keys that the game actually registers but don't interrupt gameplay too much if they activate while you're playing. You *must* be tabbed into GTA V for <kbd>Anti AFK</kbd> to work. While toggled, <kbd>Anti AFK</kbd> will only activate when all of the following conditions are true:
* The `Grand Theft Auto V` window is currently focused
* None of the designated keys are currently being pressed[^2]
* The mouse cursor is not currently visible[^3]
#### Network
<kbd>Game's network access</kbd> — Blocks/unblocks GTA V's entire internet access using the Windows Firewall. This feature can also be changed to <kbd>Rockstar save server access</kbd> via the **Network** dropdown of the settings. When in this mode, instead of blocking the entire game's network, it will only block the Rockstar save server at the IP `192.81.241.171`. The IP can also be changed in the same settings, if ever needed.
This feature is primarily useful for **replay glitching**, which is an exploit that allows you to complete any heist/mission finale, get the money for it, and then be able to do it again right away.
<details>
<summary>How to do a replay glitch</summary>
1. Get to the finale of a heist/contract/mission series, such as **The Contract: Dr. Dre**
2. Complete the finale as normal
3. Right *after* the **HEIST/MISSION PASSED** screen you get when you complete a finale (basically, when your cut is being shown), <kbd>Block</kbd> the game's internet access
4. After a few seconds, you will be shown an error screen and kicked to the main menu
5. At this point, <kbd>Unblock</kbd> the game's internet access and load back into online
6. Finally, spend any amount of money, and you should receive the finale cut, but also be able to go play the finale again immediately
</details>
<details>
<summary>How to do a replay glitch (save server method)</summary>
1. Get to the finale of a heist/contract/mission series, such as **The Contract: Dr. Dre**
2. At any point before you complete the finale, <kbd>Block</kbd> the save server access (for example, at the beginning of the mission)
3. Complete the finale as normal
4. After loading into online, exit to story mode
5. At this point, <kbd>Unblock</kbd> the save server access and load back into online
6. Finally, force a save by changing your outfit, and you should receive the finale cut, but also be able to go play the finale again immediately
</details>
[^1]: Technically, it just makes you lose connection to the session. But it looks like everyone left you instead.
[^2]: Otherwise, if you are, for example, using those keys to fly a plane, <kbd>Anti AFK</kbd> activating could cancel the input.
[^3]: Specifically, the Windows mouse cursor, not the in-game mouse cursor. This check is mostly used to prevent typing while you are in the Rockstar overlay.
Binary file not shown.
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><circle style="fill:#fff" cx="256" cy="256" r="256"/></svg>

After

Width:  |  Height:  |  Size: 147 B

+28 -1
View File
@@ -1,7 +1,34 @@
fn main() {
static_vcruntime::metabuild();
winresource::WindowsResource::new()
tauri_winres::WindowsResource::new()
.set("FileDescription", "GTA Tools")
.set("ProductName", "GTA Tools")
.set("LegalCopyright", "futile <git@futile.eu>")
.set_language(0x0009)
.set_icon("assets/icon.ico")
.set_manifest(
r#"<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
</assembly>"#,
)
.compile()
.unwrap();
embed_latest_git_hash();
}
fn embed_latest_git_hash() {
let git_rev_parse = std::process::Command::new("git")
.args(["rev-parse", "--short=8", "HEAD"])
.output()
.unwrap();
let git_hash = String::from_utf8(git_rev_parse.stdout).unwrap();
println!("cargo::rustc-env=LATEST_GIT_COMMIT_HASH={git_hash}");
println!("cargo::rerun-if-changed=.git/refs/heads/main");
println!("cargo::rerun-if-changed=.git/HEAD");
}
+1
View File
@@ -1,4 +1,5 @@
pub mod anti_afk;
pub mod empty_session;
pub mod force_close;
pub mod game_networking;
pub mod launch;
+39 -19
View File
@@ -1,11 +1,14 @@
use crate::util::{self, consts::game::WINDOW_TITLE};
use std::time::{Duration, Instant};
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 VK_SHIFT: u8 = 16;
const INTERVAL: Duration = Duration::from_secs(60);
const PRESS_KEYS: [VIRTUAL_KEY; 2] = [VK_NUMPAD4, VK_NUMPAD6];
#[derive(Debug)]
pub struct AntiAfk {
pub enabled: bool,
pub interval: Instant,
@@ -20,23 +23,40 @@ impl Default for AntiAfk {
}
}
pub fn send(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,
);
impl AntiAfk {
pub fn can_activate(&self) -> bool {
use util::win::{is_any_key_pressed, is_cursor_visible, is_window_focused};
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();
}
}
pub fn activate() {
send(VK_SHIFT);
fn send(vk_codes: &[VIRTUAL_KEY]) {
let mut inputs = Vec::new();
for &vk_code in vk_codes {
let scan_code = unsafe { MapVirtualKeyW(u32::from(vk_code.0), MAPVK_VK_TO_VSC) } as u16;
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) };
}
+49 -41
View File
@@ -1,17 +1,20 @@
use crate::{features, gui::App, util::Countdown};
use std::time::{Duration, Instant};
use sysinfo::System;
use windows::Win32::{
Foundation::{CloseHandle, HANDLE, NTSTATUS},
System::Threading::{OpenProcess, PROCESS_SUSPEND_RESUME},
use crate::util::{
countdown::Countdown,
firewall::{Firewall, RuleDirection, RuleMode, RuleProtocol},
system_info::SystemInfo,
};
use anyhow::Result;
use std::time::{Duration, Instant};
pub const INTERVAL: Duration = Duration::from_secs(10);
const ENHANCED: &str = "GTA5_Enhanced.exe";
const LEGACY: &str = "GTA5.exe";
const FILTER_NAME_EMPTY_SESSION_IN: &str = "[GTA Tools] Block inbound UDP traffic for all of GTA V";
const FILTER_NAME_EMPTY_SESSION_OUT: &str =
"[GTA Tools] Block outbound UDP traffic for all of GTA V";
const INTERVAL: Duration = Duration::from_secs(10);
#[derive(Debug)]
pub struct EmptySession {
pub enabled: bool,
pub disabled: bool,
pub interval: Instant,
pub countdown: Countdown,
}
@@ -19,45 +22,50 @@ pub struct EmptySession {
impl Default for EmptySession {
fn default() -> Self {
Self {
enabled: true,
disabled: false,
interval: Instant::now(),
countdown: Countdown::new(features::empty_session::INTERVAL.as_secs() as usize),
countdown: Countdown::new(INTERVAL.as_secs()),
}
}
}
#[link(name = "ntdll")]
unsafe extern "system" {
pub unsafe fn NtSuspendProcess(ProcessHandle: HANDLE) -> NTSTATUS;
pub unsafe fn NtResumeProcess(ProcessHandle: HANDLE) -> NTSTATUS;
impl EmptySession {
pub fn run_timers(&mut self, firewall: &Firewall) -> Result<()> {
if self.disabled {
self.countdown.count();
} else {
self.countdown.reset();
}
if self.interval.elapsed() >= INTERVAL {
deactivate(firewall)?;
self.disabled = false;
}
Ok(())
}
}
fn get_gta_pid(sysinfo: &mut System) -> u32 {
sysinfo.refresh_all();
if let Some((pid, _)) = sysinfo
.processes()
.iter()
.find(|(_, p)| p.name() == ENHANCED || p.name() == LEGACY)
{
return pid.as_u32();
pub fn activate(system_info: &mut SystemInfo, firewall: &Firewall) -> Result<bool> {
let Some(exe_path) = system_info.get_game_exe_path() else {
log::info!("wasn't able to find game exe");
return Ok(false);
};
u32::MAX
firewall.add(
FILTER_NAME_EMPTY_SESSION_IN,
RuleMode::Executable(exe_path.to_path_buf()),
RuleDirection::In,
RuleProtocol::Udp,
)?;
firewall.add(
FILTER_NAME_EMPTY_SESSION_OUT,
RuleMode::Executable(exe_path.to_path_buf()),
RuleDirection::Out,
RuleProtocol::Udp,
)?;
Ok(true)
}
pub fn activate(app: &mut App) {
let pid = get_gta_pid(&mut app.sysinfo);
if pid == u32::MAX {
return;
}
unsafe {
app.game_handle = OpenProcess(PROCESS_SUSPEND_RESUME, false, pid).unwrap();
let _ = NtSuspendProcess(app.game_handle);
}
}
pub fn deactivate(app: &mut App) {
unsafe {
let _ = NtResumeProcess(app.game_handle);
let _ = CloseHandle(app.game_handle);
}
pub fn deactivate(firewall: &Firewall) -> Result<()> {
firewall.remove(FILTER_NAME_EMPTY_SESSION_IN)?;
firewall.remove(FILTER_NAME_EMPTY_SESSION_OUT)?;
Ok(())
}
+51 -15
View File
@@ -1,32 +1,68 @@
use std::time::Instant;
use sysinfo::System;
use crate::util::{
consts::game::{EXE_ENHANCED, EXE_LEGACY},
system_info::SystemInfo,
};
use std::time::{Duration, Instant};
const ENHANCED: &str = "GTA5_Enhanced.exe";
const LEGACY: &str = "GTA5.exe";
const INTERVAL: Duration = Duration::from_secs(3);
#[derive(Debug)]
pub struct ForceClose {
pub button_text: String,
pub prompting: bool,
pub interval: Instant,
timer: Instant,
counting: bool,
current_frame: bool,
}
impl Default for ForceClose {
fn default() -> Self {
Self {
button_text: "Force close game".to_string(),
prompting: false,
interval: Instant::now(),
button_text: "Force close game".to_owned(),
timer: Instant::now(),
counting: false,
current_frame: false,
}
}
}
pub fn activate(sysinfo: &mut System) {
sysinfo.refresh_all();
sysinfo
impl ForceClose {
pub fn prompt(&mut self, force_close_button_clicked: bool, system_info: &mut SystemInfo) {
if force_close_button_clicked && !self.counting {
self.button_text = "Are you sure?".to_owned();
self.timer = Instant::now();
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(system_info);
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(system_info: &mut SystemInfo) {
system_info.refresh();
system_info
.processes()
.iter()
.filter(|(_, p)| p.name() == ENHANCED || p.name() == LEGACY)
.for_each(|(_, p)| {
p.kill();
.filter(|p| p.name() == EXE_ENHANCED || p.name() == EXE_LEGACY)
.for_each(|p| {
if !p.kill() {
log::error!("failed to force close game, probably due to access denied");
}
});
system_info.refresh();
}
+100
View File
@@ -0,0 +1,100 @@
use crate::{
gui::settings::BlockMethod,
util::{
firewall::{Firewall, RuleDirection, RuleMode, RuleProtocol},
system_info::SystemInfo,
},
};
use anyhow::Result;
use strum::{Display, EnumIter};
const FILTER_NAME_EXE: &str = "[GTA Tools] Block outbound traffic for all of GTA V";
const FILTER_NAME_SAVE_SERVER: &str = "[GTA Tools] Block outbound traffic to Rockstar save server";
#[derive(Clone, Copy, Debug, Default, Display, EnumIter, PartialEq)]
pub enum BlockedStatus {
#[default]
Unblocked,
Server,
Executable,
}
#[derive(Debug)]
pub struct GameNetworking {
pub blocked: BlockedStatus,
}
impl Default for GameNetworking {
fn default() -> Self {
let firewall = Firewall::default();
Self {
blocked: if firewall.is_blocked(FILTER_NAME_SAVE_SERVER).unwrap() {
BlockedStatus::Server
} else if firewall.is_blocked(FILTER_NAME_EXE).unwrap() {
BlockedStatus::Executable
} else {
BlockedStatus::Unblocked
},
}
}
}
impl GameNetworking {
pub fn block_exe(&mut self, system_info: &mut SystemInfo, firewall: &Firewall) -> Result<()> {
let Some(exe_path) = system_info.get_game_exe_path() else {
log::warn!("Unable to find game executable path.");
return Ok(());
};
firewall
.add(
FILTER_NAME_EXE,
RuleMode::Executable(exe_path.to_path_buf()),
RuleDirection::Out,
RuleProtocol::Any,
)
.inspect(|_| self.blocked = BlockedStatus::Executable)
}
pub fn unblock_exe(&mut self, firewall: &Firewall) -> Result<()> {
firewall
.remove(FILTER_NAME_EXE)
.inspect(|_| self.blocked = BlockedStatus::Unblocked)
}
pub fn block_save_server(&mut self, save_server_ip: &str, firewall: &Firewall) -> Result<()> {
firewall
.add(
FILTER_NAME_SAVE_SERVER,
RuleMode::Address(save_server_ip.to_owned()),
RuleDirection::Out,
RuleProtocol::Any,
)
.inspect(|_| self.blocked = BlockedStatus::Server)
}
pub fn unblock_save_server(&mut self, firewall: &Firewall) -> Result<()> {
firewall
.remove(FILTER_NAME_SAVE_SERVER)
.inspect(|_| self.blocked = BlockedStatus::Unblocked)
}
pub fn ensure_block_exclusivity(
&mut self,
block_method: BlockMethod,
firewall: &Firewall,
) -> Result<()> {
match block_method {
BlockMethod::EntireGame => {
if self.blocked == BlockedStatus::Server {
self.unblock_save_server(firewall)?;
}
}
BlockMethod::SaveServer => {
if self.blocked == BlockedStatus::Executable {
self.unblock_exe(firewall)?;
}
}
}
Ok(())
}
}
+34 -26
View File
@@ -1,55 +1,63 @@
use std::{fmt::Display, path::PathBuf, process::Command};
use crate::gui::settings::LaunchVersion;
use serde::{Deserialize, Serialize};
use std::{path::PathBuf, process::Command};
use strum::{Display, EnumIter};
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 {
#[default]
Steam,
#[strum(to_string = "Rockstar Games")]
Rockstar,
#[strum(to_string = "Epic Games")]
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(Default)]
#[derive(Debug, Default)]
pub struct Launch {
pub selected: Platform,
}
pub fn launch(platform: &Platform) {
pub fn launch(platform: &Platform, version: &LaunchVersion) {
match platform {
Platform::Steam => {
let _ = open::that_detached("steam://run/3240220");
let steam_url = match version {
LaunchVersion::Enhanced => "steam://run/3240220",
LaunchVersion::Legacy => "steam://run/271590",
};
open::that_detached(steam_url).unwrap();
}
Platform::Rockstar => {
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
let Ok(gta_v_enhanced) =
hklm.open_subkey(r"SOFTWARE\WOW6432Node\Rockstar Games\GTAV Enhanced")
else {
let rockstar_url = match version {
LaunchVersion::Enhanced => r"SOFTWARE\WOW6432Node\Rockstar Games\GTAV Enhanced",
LaunchVersion::Legacy => r"SOFTWARE\WOW6432Node\Rockstar Games\Grand Theft Auto V",
};
let Ok(gta_key) = hklm.open_subkey(rockstar_url) else {
return;
};
let Ok(install_folder): Result<String, std::io::Error> =
gta_v_enhanced.get_value("InstallFolder")
gta_key.get_value("InstallFolder")
else {
return;
};
let mut play_gtav_path = PathBuf::from(install_folder);
play_gtav_path.push("PlayGTAV.exe");
let _ = Command::new(play_gtav_path).spawn();
let mut exe_path = PathBuf::from(install_folder);
exe_path.push("PlayGTAV.exe");
if let Err(why) = Command::new(exe_path).spawn() {
log::warn!("Couldn't launch GTA 5 via Rockstar Games Launcher: {why}");
}
}
Platform::Epic => {
let _ = open::that_detached(
"com.epicgames.launcher://apps/331226ba7c944720baa99103cb1fe80c?action=launch&silent=true",
);
let epic_url = match version {
LaunchVersion::Enhanced => {
"com.epicgames.launcher://apps/8769e24080ea413b8ebca3f1b8c50951?action=launch&silent=true"
}
LaunchVersion::Legacy => {
"com.epicgames.launcher://apps/9d2d0eb64d5c44529cece33fe2a46482?action=launch&silent=true"
}
};
open::that_detached(epic_url).unwrap();
}
}
}
+6 -170
View File
@@ -1,170 +1,6 @@
use crate::features::{
self,
anti_afk::AntiAfk,
empty_session::EmptySession,
force_close::ForceClose,
launch::{Launch, Platform},
};
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();
}
pub mod app;
mod debug;
pub mod run;
pub mod settings;
mod tools;
mod ui_ext;
+339
View File
@@ -0,0 +1,339 @@
use crate::{
features::{self, game_networking::BlockedStatus},
gui::{
settings::{BlockMethod, ROCKSTAR_SAVE_SERVER, Settings},
tools,
ui_ext::UiExt,
},
util::{
consts::{colours, game::WINDOW_TITLE, path},
firewall::Firewall,
persistent_state::PersistentState,
system_info::SystemInfo,
win,
},
};
use eframe::egui;
use std::time::{Duration, Instant};
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 debug: bool,
closing: bool,
}
impl Default for Flags {
fn default() -> Self {
Self {
debug: false,
closing: false,
}
}
}
#[derive(Debug, Default)]
pub struct App {
pub settings: Settings,
stage: Stage,
pub flags: Flags,
pub system_info: SystemInfo,
firewall: Firewall,
pub anti_afk: features::anti_afk::AntiAfk,
empty_session: features::empty_session::EmptySession,
force_close: features::force_close::ForceClose,
pub game_networking: features::game_networking::GameNetworking,
pub launch: features::launch::Launch,
}
impl eframe::App for App {
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
ui.request_repaint_after(Duration::from_millis(100));
self.empty_session.run_timers(&self.firewall).unwrap();
egui::Panel::bottom("bottom_panel")
.exact_size(25.0)
.show(ui, |ui| {
ui.with_layout(egui::Layout::left_to_right(egui::Align::Center), |ui| {
ui.build_menu(&mut self.stage);
});
});
egui::CentralPanel::default().show(ui, |ui| {
egui::ScrollArea::vertical()
.auto_shrink([false, true])
.show(ui, |ui| match self.stage {
Stage::Main => self.show_main_stage(ui),
Stage::Settings => self.show_settings_stage(ui),
Stage::About => self.show_about_stage(ui),
});
});
if tools::debug_keycombo_pressed(ui) || tools::debug_viewport_close_pressed(ui) {
self.flags.debug = !self.flags.debug;
}
if self.flags.debug {
self.show_debug_viewport(ui);
}
if self.flags.closing {
ui.send_viewport_cmd(egui::ViewportCommand::Close);
}
}
}
impl App {
fn show_game_section(&mut self, ui: &mut egui::Ui) {
ui.header("Game");
ui.horizontal(|ui| {
if ui.button("Launch").clicked() {
features::launch::launch(&self.launch.selected, &self.settings.launch_version);
}
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.button(&self.force_close.button_text);
self.force_close
.prompt(force_close_button.clicked(), &mut self.system_info);
}
fn show_session_section(&mut self, ui: &mut egui::Ui) {
ui.header("Session");
ui.add_enabled_ui(!self.empty_session.disabled, |ui| {
ui.horizontal(|ui| {
if ui.button("Empty current session").clicked()
&& features::empty_session::activate(&mut self.system_info, &self.firewall)
.unwrap()
{
self.empty_session.interval = Instant::now();
self.empty_session.disabled = true;
}
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) {
"Game is focused."
} else {
"Game is not focused!"
})
});
}
});
if self.anti_afk.can_activate() && self.anti_afk.should_activate() {
self.anti_afk.activate();
}
}
fn show_network_section(&mut self, ui: &mut egui::Ui) {
ui.header("Network");
egui::Frame::new()
.outer_margin(egui::vec2(0.0, -2.0))
.show(ui, |ui| {
let label = ui.horizontal(|ui| {
let label = match self.settings.block_method {
BlockMethod::EntireGame => ui.label("Game's network access"),
BlockMethod::SaveServer => ui.label("Rockstar save server access"),
};
ui.add_space(1.0);
ui.create_indicator_dot(match self.game_networking.blocked {
BlockedStatus::Executable
if self.settings.block_method == BlockMethod::EntireGame =>
{
colours::RED
}
BlockedStatus::Server
if self.settings.block_method == BlockMethod::SaveServer =>
{
colours::RED
}
_ => colours::GREEN,
});
label
});
ui.allocate_ui_with_layout(
egui::vec2(label.inner.rect.width(), 0.0),
egui::Layout::top_down(egui::Align::Min),
|ui| {
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
if ui.button("Block").clicked() {
match self.settings.block_method {
BlockMethod::EntireGame => {
self.game_networking
.block_exe(&mut self.system_info, &self.firewall)
.unwrap();
}
BlockMethod::SaveServer => {
self.game_networking
.block_save_server(
&self.settings.save_server_ip,
&self.firewall,
)
.unwrap();
}
}
}
});
columns[1].vertical_centered_justified(|ui| {
if ui.button("Unblock").clicked() {
match self.settings.block_method {
BlockMethod::EntireGame => {
self.game_networking
.unblock_exe(&self.firewall)
.unwrap();
}
BlockMethod::SaveServer => {
self.game_networking
.unblock_save_server(&self.firewall)
.unwrap();
}
}
}
});
});
},
);
});
}
fn show_main_stage(&mut self, ui: &mut egui::Ui) {
self.show_game_section(ui);
self.show_session_section(ui);
self.show_network_section(ui);
}
fn show_settings_stage(&mut self, ui: &mut egui::Ui) {
ui.collapsing("General", |ui| {
ui.horizontal(|ui| {
let selection = self.settings.theme;
egui::ComboBox::from_id_salt("Theme")
.selected_text(self.settings.theme.to_string())
.show_ui(ui, |ui| {
ui.build_menu(&mut self.settings.theme);
});
if selection != self.settings.theme {
ui.set_theme(egui::Theme::from(self.settings.theme));
}
ui.label("Theme");
});
});
ui.collapsing("Game", |ui| {
ui.horizontal(|ui| {
egui::ComboBox::from_id_salt("Launch version")
.selected_text(self.settings.launch_version.to_string())
.show_ui(ui, |ui| {
ui.build_menu(&mut self.settings.launch_version);
});
ui.label("Launch version");
});
});
ui.collapsing("Network", |ui| {
ui.horizontal(|ui| {
egui::ComboBox::from_id_salt("Block method")
.selected_text(self.settings.block_method.to_string())
.show_ui(ui, |ui| {
ui.build_menu(&mut self.settings.block_method);
});
ui.label("Block method");
if let Err(why) = self
.game_networking
.ensure_block_exclusivity(self.settings.block_method, &self.firewall)
{
log::warn!("Couldn't ensure block exclusivity: {why}");
}
});
if self.settings.block_method == BlockMethod::SaveServer {
ui.horizontal(|ui| {
ui.add(
egui::TextEdit::singleline(&mut self.settings.save_server_ip)
.char_limit(15)
.desired_width(100.0),
);
ui.label("Save server IP");
if self.settings.save_server_ip != Settings::default().save_server_ip {
if ui.button("").clicked() {
self.settings.save_server_ip = String::from(ROCKSTAR_SAVE_SERVER);
}
}
});
};
});
ui.collapsing("Miscellaneous", |ui| {
if ui.button("Open storage path").clicked() {
open::that_detached(path::APP_STORAGE.as_path()).unwrap();
}
});
}
fn show_about_stage(&self, 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.scope(|ui| {
ui.style_mut().visuals.override_text_color = Some(colours::RED);
ui.label("");
});
ui.label(" from ");
ui.scope(|ui| {
ui.style_mut().visuals.hyperlink_color =
egui::Theme::from(self.settings.theme)
.default_style()
.visuals
.text_color();
ui.hyperlink_to("futile", "https://futile.eu");
});
});
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
ui.style_mut().spacing.button_padding = egui::Vec2::new(4.0, 0.0);
if ui.button("").on_hover_text("View source code").clicked() {
open::that("https://futile.eu/gta-tools").unwrap();
}
if cfg!(debug_assertions) {
ui.label("(dev)");
}
ui.label(format!("v{}", env!("CARGO_PKG_VERSION")))
.on_hover_text(
egui::RichText::new(env!("LATEST_GIT_COMMIT_HASH")).monospace(),
);
});
});
ui.add(egui::Image::new(egui::include_image!(
"../../assets/icon.png"
)));
});
}
}
impl Drop for App {
fn drop(&mut self) {
// save any persistent state to config file
PersistentState {
launcher: self.launch.selected,
anti_afk_enabled: self.anti_afk.enabled,
settings: self.settings.clone(),
}
.set();
// make sure we are not network blocking game
if let Err(why) = features::empty_session::deactivate(&self.firewall) {
log::error!("couldn't deactivate empty session: {why}");
}
}
}
+85
View File
@@ -0,0 +1,85 @@
use crate::{
gui::{
app::{App, WINDOW_SIZE},
tools,
ui_ext::UiExt,
},
util::consts::game::{EXE_ENHANCED, EXE_LEGACY},
};
use eframe::egui;
impl App {
fn add_debug_viewport_contents(&mut self, ui: &mut egui::Ui) {
ui.collapsing("misc", |ui| {
ui.scope(|ui| {
use windows::Win32::UI::WindowsAndMessaging::{
GetForegroundWindow, GetWindowTextW,
};
let current_title = {
let mut buffer = [0; 512];
let hwnd = unsafe { GetForegroundWindow() };
let length = unsafe { GetWindowTextW(hwnd, &mut buffer) };
String::from_utf16_lossy(&buffer[..length as usize])
};
ui.label(format!("focused: \"{current_title}\""));
});
ui.horizontal(|ui| {
ui.label("blocked");
egui::ComboBox::from_id_salt("blocked")
.selected_text(self.game_networking.blocked.to_string())
.show_ui(ui, |ui| ui.build_menu(&mut self.game_networking.blocked));
});
if ui.add(egui::Button::new("force refresh theme")).clicked() {
ui.set_theme(egui::Theme::from(self.settings.theme));
}
if ui.button("panic button").clicked() {
panic!("this is the panic button");
}
});
ui.collapsing("anti afk", |ui| {
ui.label(format!(
"timer: {}",
self.anti_afk.interval.elapsed().as_secs()
));
ui.label(format!("can activate: {}", self.anti_afk.can_activate()));
});
ui.collapsing("system info", |ui| {
if ui.button("refresh").clicked() {
self.system_info.refresh();
}
let pid = self
.system_info
.processes()
.iter()
.find(|p| p.name() == EXE_ENHANCED || p.name() == EXE_LEGACY)
.map_or_else(|| "no pid found!".to_owned(), |p| p.pid().to_string());
ui.label(format!("gta pid: {pid}"));
});
ui.collapsing("app state", |ui| ui.label(format!("{self:#?}")));
}
pub fn show_debug_viewport(&mut self, ctx: &egui::Context) {
let main = ctx.input(|i| i.viewport().outer_rect.unwrap_or(egui::Rect::EVERYTHING));
let builder = egui::ViewportBuilder::default()
.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));
});
},
);
}
}
+72
View File
@@ -0,0 +1,72 @@
use crate::{
gui::{
app::{App, WINDOW_SIZE},
tools,
},
util::persistent_state::PersistentState,
};
use eframe::egui;
use windows::Win32::System::Com::{COINIT_APARTMENTTHREADED, CoInitializeEx};
fn app_creator(
cc: &eframe::CreationContext<'_>,
) -> Result<Box<dyn eframe::App>, Box<dyn std::error::Error + Send + Sync>> {
// initialize COM just in case
if let Err(e) = unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED) }.ok() {
log::error!("couldn't initialize COM: {e}");
}
// initialize App early to modify some things before returning it
let mut app = Box::new(App::default());
// load previously selected launch platform & settings from persistent state
if let Some(persistent_state) = PersistentState::get() {
persistent_state.apply_to(&mut app);
}
// refresh system info because it initializes with nothing
app.system_info.refresh();
// enable image loading support in egui
egui_extras::install_image_loaders(&cc.egui_ctx);
// set our initial theme, from earlier loaded settings. we set the egui theme here to work
// around system theme based switching of the egui style
cc.egui_ctx.set_theme(egui::Theme::from(app.settings.theme));
// apply some global styling that we like
cc.egui_ctx.all_styles_mut(|style| {
style.spacing.item_spacing = egui::vec2(4.0, 4.0);
style.interaction.selectable_labels = false;
// style.visuals.hyperlink_color = catppuccin_egui::Theme::from(app.settings.theme).text;
});
// load any extra fonts that we need
let mut fonts = egui::FontDefinitions::default();
fonts.font_data.insert(
"Inter 18pt Light".to_owned(),
egui::FontData::from_static(include_bytes!("../../assets/Inter_18pt-Light.ttf")).into(),
);
fonts
.families
.get_mut(&egui::FontFamily::Proportional)
.unwrap()
.insert(0, "Inter 18pt Light".to_owned());
fonts.font_data.insert(
"Inter 18pt Regular".to_owned(),
egui::FontData::from_static(include_bytes!("../../assets/Inter_18pt-Regular.ttf")).into(),
);
fonts.families.insert(
egui::FontFamily::Name("Inter 18pt Regular".into()),
vec!["Inter 18pt 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();
}
+68
View File
@@ -0,0 +1,68 @@
use crate::util::win;
use eframe::egui;
use serde::{Deserialize, Serialize};
use strum::{Display, EnumIter};
pub const ROCKSTAR_SAVE_SERVER: &str = "192.81.241.171";
#[derive(Clone, Copy, Debug, Default, Display, PartialEq, Eq, Serialize, Deserialize, EnumIter)]
pub enum Theme {
#[default]
#[strum(to_string = "Auto")]
Auto,
#[strum(to_string = "Light")]
Light,
#[strum(to_string = "Dark")]
Dark,
}
impl From<Theme> for egui::Theme {
fn from(val: Theme) -> Self {
match val {
Theme::Auto => {
if win::is_system_theme_light() {
egui::Theme::Light
} else {
egui::Theme::Dark
}
}
Theme::Light => egui::Theme::Light,
Theme::Dark => egui::Theme::Dark,
}
}
}
#[derive(Clone, Copy, Debug, Default, Display, PartialEq, Eq, Serialize, Deserialize, EnumIter)]
pub enum LaunchVersion {
#[default]
Enhanced,
Legacy,
}
#[derive(Clone, Copy, Debug, Default, Display, PartialEq, Eq, Serialize, Deserialize, EnumIter)]
pub enum BlockMethod {
#[default]
#[strum(to_string = "Entire game")]
EntireGame,
#[strum(to_string = "Save server")]
SaveServer,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Settings {
pub theme: Theme,
pub launch_version: LaunchVersion,
pub save_server_ip: String,
pub block_method: BlockMethod,
}
impl Default for Settings {
fn default() -> Self {
Self {
theme: Theme::default(),
launch_version: LaunchVersion::default(),
block_method: BlockMethod::default(),
save_server_ip: String::from(ROCKSTAR_SAVE_SERVER),
}
}
}
+26
View File
@@ -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(ui: &egui::Ui) -> bool {
ui.input(|i| i.modifiers.all() && i.key_pressed(egui::Key::D))
}
pub fn debug_viewport_close_pressed(ui: &egui::Ui) -> bool {
ui.input(|i| {
i.raw
.viewports
.get(&egui::ViewportId::from_hash_of("debug_viewport"))
.filter(|vp| vp.close_requested())
.is_some()
})
}
+38
View File
@@ -0,0 +1,38 @@
use eframe::egui;
pub trait UiExt {
fn header(&mut self, text: &str);
fn build_menu<E>(&mut self, current_value: &mut E)
where
E: strum::IntoEnumIterator + std::fmt::Display + std::cmp::PartialEq + Copy;
fn create_indicator_dot(&mut self, colour: impl Into<egui::Color32>) -> egui::Response;
}
impl UiExt for egui::Ui {
fn header(&mut self, text: &str) {
self.horizontal(|ui| {
ui.label(egui::RichText::new(text).font(egui::FontId::new(
12.5,
egui::FontFamily::Name("Inter 18pt Regular".into()),
)));
ui.add(egui::Separator::default().horizontal());
});
}
fn build_menu<E>(&mut self, current_value: &mut E)
where
E: strum::IntoEnumIterator + std::fmt::Display + std::cmp::PartialEq + Copy,
{
E::iter().for_each(|variant| {
self.selectable_value(current_value, variant, variant.to_string());
});
}
fn create_indicator_dot(&mut self, colour: impl Into<egui::Color32>) -> egui::Response {
self.add(
egui::Image::new(egui::include_image!("../../assets/circle.svg"))
.max_size([4.0, 4.0].into())
.tint(colour),
)
}
}
+21 -1
View File
@@ -4,6 +4,26 @@ mod features;
mod gui;
mod util;
use std::fmt::Write;
fn init_storage() {
if !util::consts::path::APP_STORAGE.exists() {
std::fs::create_dir_all(util::consts::path::APP_STORAGE.as_path()).unwrap();
}
}
fn panic_hook(panic_info: &std::panic::PanicHookInfo<'_>) {
let backtrace = std::backtrace::Backtrace::capture();
let mut message = format!("{panic_info}");
if backtrace.status() == std::backtrace::BacktraceStatus::Captured {
write!(message, "\nstack backtrace:\n{backtrace}").unwrap();
}
log::error!("{message}");
}
fn main() {
gui::run();
init_storage();
util::logging::Logger::init(log::LevelFilter::Info);
std::panic::set_hook(Box::new(panic_hook));
gui::run::run();
}
+7 -40
View File
@@ -1,40 +1,7 @@
use std::time::{Duration, Instant};
pub struct Countdown {
pub i: usize,
pub i_original: usize,
pub i_string: String,
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();
};
}
}
pub mod consts;
pub mod countdown;
pub mod firewall;
pub mod logging;
pub mod persistent_state;
pub mod system_info;
pub mod win;
+22
View File
@@ -0,0 +1,22 @@
pub mod path {
use std::{env, path::PathBuf, sync::LazyLock};
pub static APP_STORAGE: LazyLock<PathBuf> = LazyLock::new(|| {
PathBuf::from(env::var("LOCALAPPDATA").unwrap_or_else(|_| String::from(".")))
.join("GTA Tools")
});
pub static APP_CONFIG: LazyLock<PathBuf> = LazyLock::new(|| APP_STORAGE.join("config.json"));
pub static APP_LOG: LazyLock<PathBuf> = LazyLock::new(|| APP_STORAGE.join("gta-tools.log"));
}
pub mod game {
pub const EXE_ENHANCED: &str = "GTA5_Enhanced.exe";
pub const EXE_LEGACY: &str = "GTA5.exe";
pub const WINDOW_TITLE: &str = "Grand Theft Auto V";
}
pub mod colours {
use eframe::egui;
pub const RED: egui::Color32 = egui::Color32::from_rgb(255, 96, 96);
pub const GREEN: egui::Color32 = egui::Color32::from_rgb(96, 255, 96);
}
+41
View File
@@ -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();
}
}
}
+96
View File
@@ -0,0 +1,96 @@
use anyhow::Result;
use std::path::PathBuf;
use windows::{
Win32::{
NetworkManagement::WindowsFirewall::{
INetFwPolicy2, INetFwRule, NET_FW_ACTION_BLOCK, NET_FW_IP_PROTOCOL_ANY,
NET_FW_IP_PROTOCOL_TCP, NET_FW_IP_PROTOCOL_UDP, NET_FW_RULE_DIR_IN,
NET_FW_RULE_DIR_OUT, NetFwPolicy2, NetFwRule,
},
System::Com::{CLSCTX_INPROC_SERVER, CoCreateInstance},
},
core::BSTR,
};
#[derive(Debug)]
pub struct Firewall {
policy: INetFwPolicy2,
}
impl Default for Firewall {
fn default() -> Self {
Self {
policy: unsafe { CoCreateInstance(&NetFwPolicy2, None, CLSCTX_INPROC_SERVER) }.unwrap(),
}
}
}
impl Firewall {
pub fn add(
&self,
name: &str,
mode: RuleMode,
direction: RuleDirection,
protocol: RuleProtocol,
) -> Result<()> {
let add_rule = || {
let rules = unsafe { self.policy.Rules() }?;
unsafe { rules.Remove(&BSTR::from(name)) }?;
let rule: INetFwRule =
unsafe { CoCreateInstance(&NetFwRule, None, CLSCTX_INPROC_SERVER) }?;
unsafe { rule.SetName(&BSTR::from(name)) }?;
match mode {
RuleMode::Executable(exe) => {
unsafe { rule.SetApplicationName(&BSTR::from(exe.to_string_lossy().as_ref())) }?
}
RuleMode::Address(ip) => unsafe { rule.SetRemoteAddresses(&BSTR::from(ip)) }?,
}
match direction {
RuleDirection::In => unsafe { rule.SetDirection(NET_FW_RULE_DIR_IN) }?,
RuleDirection::Out => unsafe { rule.SetDirection(NET_FW_RULE_DIR_OUT) }?,
}
unsafe { rule.SetEnabled(true.into()) }?;
unsafe { rule.SetAction(NET_FW_ACTION_BLOCK) }?;
match protocol {
RuleProtocol::Any => unsafe { rule.SetProtocol(NET_FW_IP_PROTOCOL_ANY.0) }?,
RuleProtocol::Tcp => unsafe { rule.SetProtocol(NET_FW_IP_PROTOCOL_TCP.0) }?,
RuleProtocol::Udp => unsafe { rule.SetProtocol(NET_FW_IP_PROTOCOL_UDP.0) }?,
}
unsafe { rules.Add(&rule) }?;
Ok(())
};
add_rule().inspect_err(|e| log::warn!("Failed to add rule '{name}': {e}"))
}
pub fn remove(&self, name: &str) -> Result<()> {
let remove_rule = || {
let rules = unsafe { self.policy.Rules() }?;
unsafe { rules.Remove(&BSTR::from(name)) }?;
Ok(())
};
remove_rule().inspect_err(|e| log::warn!("Failed to remove rule '{name}': {e}"))
}
pub fn is_blocked(&self, name: &str) -> Result<bool> {
let rules = unsafe { self.policy.Rules() }?;
let rule_exists = unsafe { rules.Item(&BSTR::from(name)) }.is_ok();
Ok(rule_exists)
}
}
pub enum RuleMode {
Executable(PathBuf),
Address(String),
}
pub enum RuleDirection {
In,
Out,
}
pub enum RuleProtocol {
Any,
#[allow(unused)]
Tcp,
Udp,
}
+64
View File
@@ -0,0 +1,64 @@
use std::{
fs::File,
io::Write,
sync::{LazyLock, Mutex},
time::SystemTime,
};
pub struct Logger {
file: Mutex<File>,
}
static LOGGER: LazyLock<Logger> = LazyLock::new(|| Logger {
file: File::options()
.create(true)
.append(true)
.open(crate::util::consts::path::APP_LOG.as_path())
.unwrap()
.into(),
});
impl log::Log for Logger {
fn enabled(&self, metadata: &log::Metadata) -> bool {
metadata.level() <= log::Level::Info
}
fn flush(&self) {}
fn log(&self, record: &log::Record) {
if self.enabled(record.metadata()) {
Self::log_to_stderr(record);
self.log_to_file(record);
}
}
}
impl Logger {
pub fn init(level: log::LevelFilter) {
log::set_logger(&*LOGGER)
.map(|()| log::set_max_level(level))
.unwrap();
}
fn log_to_file(&self, record: &log::Record) {
let mut file = self.file.lock().unwrap();
writeln!(
file,
"[{}][{}] {}",
humantime::format_rfc3339_seconds(SystemTime::now()),
record.level(),
record.args()
)
.unwrap();
file.flush().unwrap();
}
fn log_to_stderr(record: &log::Record) {
eprintln!(
"[{}][{}] {}",
humantime::format_rfc3339_seconds(SystemTime::now()),
record.level(),
record.args()
);
}
}
+42
View File
@@ -0,0 +1,42 @@
use crate::{
features::launch::Platform,
gui::{app, settings::Settings},
util::consts::path,
};
use serde::{Deserialize, Serialize};
use std::{
fs::{self, File},
io::Write,
};
#[derive(Serialize, Deserialize)]
pub struct PersistentState {
pub launcher: Platform,
pub anti_afk_enabled: bool,
pub settings: Settings,
}
impl PersistentState {
pub fn get() -> Option<Self> {
fs::read_to_string(path::APP_CONFIG.as_path())
.ok()
.and_then(|config| serde_json::from_str::<Self>(&config).ok())
}
pub fn set(&self) {
let mut config_file = File::create(path::APP_CONFIG.as_path()).unwrap();
let json = serde_json::to_string_pretty(&self).unwrap();
config_file.write_all(json.as_bytes()).unwrap();
}
pub fn apply_to(self, app: &mut app::App) {
let Self {
launcher,
anti_afk_enabled,
settings,
} = self;
app.launch.selected = launcher;
app.anti_afk.enabled = anti_afk_enabled;
app.settings = settings;
}
}
+127
View File
@@ -0,0 +1,127 @@
use std::{
ffi::{OsStr, OsString},
os::windows::{ffi::OsStringExt, process::CommandExt},
path::{Path, PathBuf},
process::Command,
};
use windows::{
Win32::System::{
Diagnostics::ToolHelp::{
CreateToolhelp32Snapshot, PROCESSENTRY32W, Process32FirstW, Process32NextW,
TH32CS_SNAPPROCESS,
},
Threading::{
CREATE_NO_WINDOW, OpenProcess, PROCESS_NAME_WIN32, PROCESS_QUERY_LIMITED_INFORMATION,
QueryFullProcessImageNameW,
},
},
core::PWSTR,
};
use crate::util::consts::game::{EXE_ENHANCED, EXE_LEGACY};
#[derive(Clone, Debug)]
pub struct Process {
pid: u32,
name: OsString,
exe: Option<PathBuf>,
}
impl Process {
pub const fn pid(&self) -> u32 {
self.pid
}
pub fn name(&self) -> &OsStr {
&self.name
}
pub fn exe(&self) -> Option<&Path> {
self.exe.as_deref()
}
pub fn kill(&self) -> bool {
let mut taskkill = Command::new("taskkill.exe");
taskkill.creation_flags(CREATE_NO_WINDOW.0);
taskkill.arg("/F").arg("/PID").arg(self.pid.to_string());
match taskkill.output() {
Ok(output) => output.status.success(),
Err(_) => false,
}
}
}
#[derive(Debug, Default)]
pub struct SystemInfo {
processes: Vec<Process>,
}
impl SystemInfo {
pub fn refresh(&mut self) {
let mut processes = Vec::new();
let snapshot_handle = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) }.unwrap();
let mut process_entry = PROCESSENTRY32W {
dwSize: size_of::<PROCESSENTRY32W>() as u32,
..Default::default()
};
unsafe { Process32FirstW(snapshot_handle, &raw mut process_entry) }.unwrap();
let exe_full_path = get_exe_full_path(&process_entry);
processes.push(Process {
pid: process_entry.th32ProcessID,
name: wide_array_to_os_string(&process_entry.szExeFile),
exe: exe_full_path,
});
while unsafe { Process32NextW(snapshot_handle, &raw mut process_entry) }.is_ok() {
let exe_full_path = get_exe_full_path(&process_entry);
processes.push(Process {
pid: process_entry.th32ProcessID,
name: wide_array_to_os_string(&process_entry.szExeFile),
exe: exe_full_path,
});
}
self.processes = processes;
}
pub fn get_game_exe_path(&mut self) -> Option<&Path> {
self.refresh();
self.processes()
.iter()
.find(|p| p.name() == EXE_ENHANCED || p.name() == EXE_LEGACY)
.and_then(|p| p.exe())
}
pub fn processes(&self) -> &[Process] {
&self.processes
}
}
fn get_exe_full_path(process_entry: &PROCESSENTRY32W) -> Option<PathBuf> {
let process_handle_result = unsafe {
OpenProcess(
PROCESS_QUERY_LIMITED_INFORMATION,
false,
process_entry.th32ProcessID,
)
};
process_handle_result.map_or(None, |process_handle| {
let mut exe_name = [0u16; 260];
let mut dw_size = exe_name.len() as u32;
let image_name_result = unsafe {
QueryFullProcessImageNameW(
process_handle,
PROCESS_NAME_WIN32,
PWSTR(exe_name.as_mut_ptr()),
&raw mut dw_size,
)
};
match image_name_result {
Ok(()) => Some(PathBuf::from(wide_array_to_os_string(&exe_name))),
Err(_) => None,
}
})
}
fn wide_array_to_os_string(wide: &[u16]) -> OsString {
let null_pos = wide.iter().position(|&x| x == 0).unwrap_or(wide.len());
OsString::from_wide(&wide[..null_pos])
}
+44
View File
@@ -0,0 +1,44 @@
use windows::Win32::UI::{
Input::KeyboardAndMouse::{GetAsyncKeyState, VIRTUAL_KEY},
WindowsAndMessaging::{
CURSOR_SHOWING, CURSORINFO, GetCursorInfo, GetForegroundWindow, GetWindowTextW,
},
};
pub fn is_cursor_visible() -> bool {
let mut ci = CURSORINFO {
cbSize: u32::try_from(std::mem::size_of::<CURSORINFO>()).unwrap(),
..Default::default()
};
unsafe { GetCursorInfo(&raw mut ci) }.unwrap();
ci.flags == CURSOR_SHOWING
}
pub fn is_window_focused(target_title: &str) -> bool {
let mut buffer = [0; 512];
let hwnd = unsafe { GetForegroundWindow() };
let length = unsafe { GetWindowTextW(hwnd, &mut buffer) };
let current_title = String::from_utf16_lossy(&buffer[..length as usize]);
current_title == target_title
}
pub fn is_any_key_pressed(keys: &[VIRTUAL_KEY]) -> bool {
keys.iter()
.any(|&key| unsafe { GetAsyncKeyState(i32::from(key.0)) } & i16::MIN != 0)
}
pub fn is_system_theme_light() -> bool {
use winreg::RegKey;
let hkcu = RegKey::predef(winreg::enums::HKEY_CURRENT_USER);
let Ok(subkey) =
hkcu.open_subkey("Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize")
else {
return true;
};
let Ok(apps_use_light_theme): Result<u32, std::io::Error> =
subkey.get_value("AppsUseLightTheme")
else {
return true;
};
apps_use_light_theme == 1
}