Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c3092026a | ||
|
|
69eda1103d | ||
|
|
19b1ade758 | ||
|
|
18f667a91a | ||
|
|
186371f835 | ||
|
|
ce2022db55 | ||
|
|
d992d6b8f6 | ||
|
|
b71dbbdf03 | ||
|
|
c85a169765 | ||
|
|
9df757c169 | ||
|
|
92cbec7934 | ||
|
|
41cfb12ea8 | ||
|
|
e328eb91f6 | ||
|
|
10c840ac4d | ||
|
|
2fc0aa2005 | ||
|
|
29f726b1bd | ||
|
|
16d3c2cd96 |
Generated
+242
-57
@@ -424,6 +424,12 @@ version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "bit-set"
|
||||
version = "0.8.0"
|
||||
@@ -654,6 +660,35 @@ dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cookie"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
"time",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cookie_store"
|
||||
version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2eac901828f88a5241ee0600950ab981148a18f2f756900ffba1b125ca6a3ef9"
|
||||
dependencies = [
|
||||
"cookie",
|
||||
"document-features",
|
||||
"idna",
|
||||
"indexmap",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"time",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.4"
|
||||
@@ -744,6 +779,15 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991"
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
|
||||
dependencies = [
|
||||
"powerfmt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
@@ -1106,6 +1150,12 @@ dependencies = [
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.5"
|
||||
@@ -1386,7 +1436,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gta-tools"
|
||||
version = "0.4.1"
|
||||
version = "0.4.2"
|
||||
dependencies = [
|
||||
"catppuccin-egui",
|
||||
"chrono",
|
||||
@@ -1395,11 +1445,13 @@ dependencies = [
|
||||
"egui_extras",
|
||||
"image",
|
||||
"open",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"static_vcruntime",
|
||||
"strum 0.27.1",
|
||||
"sysinfo",
|
||||
"ureq",
|
||||
"windows 0.61.1",
|
||||
"winreg",
|
||||
"winresource",
|
||||
@@ -1447,6 +1499,23 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
"itoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.63"
|
||||
@@ -1964,6 +2033,12 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
@@ -2499,6 +2574,12 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
@@ -2637,6 +2718,20 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"getrandom 0.2.15",
|
||||
"libc",
|
||||
"untrusted",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "1.1.0"
|
||||
@@ -2669,6 +2764,47 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df51b5869f3a441595eac5e8ff14d486ff285f7b8c0df8770e49c3b56351f0f0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pemfile"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.20"
|
||||
@@ -2715,6 +2851,12 @@ dependencies = [
|
||||
"tiny-skia",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.219"
|
||||
@@ -2951,6 +3093,12 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.100"
|
||||
@@ -2976,14 +3124,13 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "sysinfo"
|
||||
version = "0.34.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4b93974b3d3aeaa036504b8eefd4c039dced109171c1ae973f1dc63b2c7e4b2"
|
||||
source = "git+https://github.com/GuillaumeGomez/sysinfo?rev=bcbb7b2b389b8800ff457773f48b130e710458fc#bcbb7b2b389b8800ff457773f48b130e710458fc"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"memchr",
|
||||
"ntapi",
|
||||
"objc2-core-foundation",
|
||||
"windows 0.57.0",
|
||||
"windows 0.61.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3059,6 +3206,37 @@ dependencies = [
|
||||
"weezl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"itoa",
|
||||
"num-conv",
|
||||
"powerfmt",
|
||||
"serde",
|
||||
"time-core",
|
||||
"time-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time-core"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
|
||||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
|
||||
dependencies = [
|
||||
"num-conv",
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny-skia"
|
||||
version = "0.11.4"
|
||||
@@ -3221,6 +3399,45 @@ version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "ureq"
|
||||
version = "3.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7a3e9af6113ecd57b8c63d3cd76a385b2e3881365f1f489e54f49801d0c83ea"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"cookie_store",
|
||||
"flate2",
|
||||
"log",
|
||||
"percent-encoding",
|
||||
"rustls",
|
||||
"rustls-pemfile",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"ureq-proto",
|
||||
"utf-8",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ureq-proto"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fadf18427d33828c311234884b7ba2afb57143e6e7e69fda7ee883b624661e36"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"http",
|
||||
"httparse",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.4"
|
||||
@@ -3232,6 +3449,12 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utf-8"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||
|
||||
[[package]]
|
||||
name = "utf16_iter"
|
||||
version = "1.0.5"
|
||||
@@ -3492,6 +3715,15 @@ dependencies = [
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.26.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "weezl"
|
||||
version = "0.1.8"
|
||||
@@ -3632,16 +3864,6 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.57.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143"
|
||||
dependencies = [
|
||||
"windows-core 0.57.0",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.58.0"
|
||||
@@ -3674,18 +3896,6 @@ dependencies = [
|
||||
"windows-core 0.61.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.57.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d"
|
||||
dependencies = [
|
||||
"windows-implement 0.57.0",
|
||||
"windows-interface 0.57.0",
|
||||
"windows-result 0.1.2",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.58.0"
|
||||
@@ -3722,17 +3932,6 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.57.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.58.0"
|
||||
@@ -3755,17 +3954,6 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.57.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.58.0"
|
||||
@@ -3804,15 +3992,6 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.2.0"
|
||||
@@ -4414,6 +4593,12 @@ dependencies = [
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
|
||||
|
||||
[[package]]
|
||||
name = "zerovec"
|
||||
version = "0.10.4"
|
||||
|
||||
+4
-2
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "gta-tools"
|
||||
version = "0.4.1"
|
||||
version = "0.4.2"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
@@ -13,10 +13,12 @@ eframe = "0.31.1"
|
||||
egui_extras = { version = "0.31.1", features = ["image"] }
|
||||
image = { version = "0.25.6", default-features = false, features = ["png"] }
|
||||
open = "5.3.2"
|
||||
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.34.2"
|
||||
sysinfo = { git = "https://github.com/GuillaumeGomez/sysinfo", rev = "bcbb7b2b389b8800ff457773f48b130e710458fc" }
|
||||
ureq = { version = "3.0.11", features = ["json"] }
|
||||
windows = { version = "0.61.1", features = [
|
||||
"Win32_UI_Input_KeyboardAndMouse",
|
||||
"Win32_System_Threading",
|
||||
|
||||
@@ -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,4 @@
|
||||
more spaghetti than the nation of italy
|
||||
# GTA Tools
|
||||
A toolset of convenient things for GTA 5 Online.
|
||||
|
||||

|
||||

|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
set windows-shell := ["pwsh", "-NoLogo", "-NoProfileLoadTime", "-Command"]
|
||||
|
||||
build:
|
||||
install:
|
||||
cargo build --release
|
||||
upx .\target\release\gta-tools.exe --best
|
||||
|
||||
install: build
|
||||
cp .\target\release\gta-tools.exe ~\.cargo\bin
|
||||
cp .\target\release\gta-tools.exe ~\Documents
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use crate::{features, gui::App, util::countdown::Countdown};
|
||||
use crate::util::{
|
||||
consts::{ENHANCED, LEGACY},
|
||||
countdown::Countdown,
|
||||
};
|
||||
use std::time::{Duration, Instant};
|
||||
use sysinfo::System;
|
||||
use windows::Win32::{
|
||||
@@ -7,8 +10,6 @@ use windows::Win32::{
|
||||
};
|
||||
|
||||
pub const INTERVAL: Duration = Duration::from_secs(10);
|
||||
const ENHANCED: &str = "GTA5_Enhanced.exe";
|
||||
const LEGACY: &str = "GTA5.exe";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct EmptySession {
|
||||
@@ -22,7 +23,7 @@ impl Default for EmptySession {
|
||||
Self {
|
||||
disabled: false,
|
||||
interval: Instant::now(),
|
||||
countdown: Countdown::new(features::empty_session::INTERVAL.as_secs()),
|
||||
countdown: Countdown::new(INTERVAL.as_secs()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,22 +46,22 @@ fn get_gta_pid(sysinfo: &mut System) -> u32 {
|
||||
u32::MAX
|
||||
}
|
||||
|
||||
pub fn activate(app: &mut App) {
|
||||
let pid = get_gta_pid(&mut app.sysinfo);
|
||||
pub fn activate(handle: &mut HANDLE, sysinfo: &mut System) {
|
||||
let pid = get_gta_pid(sysinfo);
|
||||
if pid == u32::MAX {
|
||||
return;
|
||||
}
|
||||
unsafe {
|
||||
app.game_handle = OpenProcess(PROCESS_SUSPEND_RESUME, false, pid).unwrap();
|
||||
let _ = NtSuspendProcess(app.game_handle);
|
||||
*handle = OpenProcess(PROCESS_SUSPEND_RESUME, false, pid).unwrap();
|
||||
let _ = NtSuspendProcess(*handle);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deactivate(app: &mut App) {
|
||||
pub fn deactivate(handle: &mut HANDLE) {
|
||||
unsafe {
|
||||
if !app.game_handle.is_invalid() {
|
||||
let _ = NtResumeProcess(app.game_handle);
|
||||
let _ = CloseHandle(app.game_handle);
|
||||
if !handle.is_invalid() {
|
||||
let _ = NtResumeProcess(*handle);
|
||||
let _ = CloseHandle(*handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ pub struct ForceClose {
|
||||
impl Default for ForceClose {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
button_text: "Force close game".to_string(),
|
||||
button_text: "Force close game".to_owned(),
|
||||
prompting: false,
|
||||
interval: Instant::now(),
|
||||
}
|
||||
@@ -21,7 +21,7 @@ impl Default for ForceClose {
|
||||
|
||||
impl ForceClose {
|
||||
pub fn prompting(&mut self) {
|
||||
self.button_text = "Are you sure?".to_string();
|
||||
self.button_text = "Are you sure?".to_owned();
|
||||
self.prompting = true;
|
||||
self.interval = Instant::now();
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
use crate::util::consts::{ENHANCED, LEGACY};
|
||||
use std::{os::windows::process::CommandExt, path::Path, process::Command};
|
||||
use sysinfo::System;
|
||||
use windows::Win32::System::Threading::CREATE_NO_WINDOW;
|
||||
|
||||
const FILTER_NAME: &str = "[GTA Tools] Block all traffic for GTA V";
|
||||
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
|
||||
|
||||
fn get_game_exe_path(sysinfo: &mut System) -> Option<&Path> {
|
||||
sysinfo.refresh_all();
|
||||
@@ -24,7 +24,7 @@ pub fn block_all(sysinfo: &mut System) {
|
||||
let Some(exe_path) = get_game_exe_path(sysinfo) else {
|
||||
return;
|
||||
};
|
||||
let exe_path = exe_path.display().to_string();
|
||||
let exe_path = exe_path.display();
|
||||
Command::new("netsh")
|
||||
.args([
|
||||
"advfirewall",
|
||||
@@ -37,7 +37,7 @@ pub fn block_all(sysinfo: &mut System) {
|
||||
"protocol=ANY",
|
||||
&format!("program={exe_path}"),
|
||||
])
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.creation_flags(CREATE_NO_WINDOW.0)
|
||||
.spawn()
|
||||
.unwrap();
|
||||
Command::new("netsh")
|
||||
@@ -52,7 +52,7 @@ pub fn block_all(sysinfo: &mut System) {
|
||||
"protocol=ANY",
|
||||
&format!("program={exe_path}"),
|
||||
])
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.creation_flags(CREATE_NO_WINDOW.0)
|
||||
.spawn()
|
||||
.unwrap();
|
||||
}
|
||||
@@ -66,7 +66,7 @@ pub fn unblock_all() {
|
||||
"rule",
|
||||
&format!("name={FILTER_NAME}"),
|
||||
])
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.creation_flags(CREATE_NO_WINDOW.0)
|
||||
.spawn()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
+350
@@ -0,0 +1,350 @@
|
||||
use crate::{
|
||||
features::{
|
||||
self, anti_afk::AntiAfk, empty_session::EmptySession, force_close::ForceClose,
|
||||
launch::Launch,
|
||||
},
|
||||
gui::{
|
||||
settings::{self, Settings},
|
||||
tools,
|
||||
},
|
||||
util::{
|
||||
self,
|
||||
consts::{APP_STORAGE_PATH, ENHANCED, GTA_WINDOW_TITLE, LEGACY},
|
||||
meta::Meta,
|
||||
persistent_state::PersistentState,
|
||||
},
|
||||
};
|
||||
use eframe::egui;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
pub const WINDOW_SIZE: [f32; 2] = [240.0, 240.0];
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq)]
|
||||
pub enum Stage {
|
||||
#[default]
|
||||
Main,
|
||||
Settings,
|
||||
About,
|
||||
}
|
||||
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Flags {
|
||||
pub elevated: bool,
|
||||
debug: bool,
|
||||
closing: bool,
|
||||
current_frame: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct App {
|
||||
meta: Meta,
|
||||
pub settings: Settings,
|
||||
stage: Stage,
|
||||
pub flags: Flags,
|
||||
pub sysinfo: sysinfo::System,
|
||||
pub game_handle: windows::Win32::Foundation::HANDLE,
|
||||
pub launch: Launch,
|
||||
force_close: ForceClose,
|
||||
empty_session: EmptySession,
|
||||
anti_afk: AntiAfk,
|
||||
}
|
||||
|
||||
impl Default for App {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
meta: Meta::default(),
|
||||
settings: Settings::default(),
|
||||
stage: Stage::default(),
|
||||
flags: Flags::default(),
|
||||
sysinfo: sysinfo::System::new_all(),
|
||||
game_handle: windows::Win32::Foundation::HANDLE::default(),
|
||||
launch: Launch::default(),
|
||||
force_close: ForceClose::default(),
|
||||
empty_session: EmptySession::default(),
|
||||
anti_afk: AntiAfk::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for App {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
catppuccin_egui::set_theme(ctx, self.settings.theme.into());
|
||||
self.run_timers();
|
||||
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.selectable_value(&mut self.stage, Stage::Main, "Main");
|
||||
ui.selectable_value(&mut self.stage, Stage::Settings, "Settings");
|
||||
ui.selectable_value(&mut self.stage, Stage::About, "About");
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
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() {
|
||||
util::win::elevate(util::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_game(ctx, ui);
|
||||
self.show_session(ctx, ui);
|
||||
self.show_network(ctx, ui);
|
||||
}
|
||||
Stage::Settings => self.show_settings(ctx, ui),
|
||||
Stage::About => self.show_about(ctx, ui),
|
||||
});
|
||||
});
|
||||
if tools::check_debug_keycombo_pressed(ctx) {
|
||||
self.flags.debug = !self.flags.debug;
|
||||
}
|
||||
if tools::check_debug_viewport_close_button_pressed(ctx) {
|
||||
self.flags.debug = false;
|
||||
}
|
||||
if self.flags.debug {
|
||||
let main_rect = ctx.input(|i| {
|
||||
i.viewport()
|
||||
.clone()
|
||||
.outer_rect
|
||||
.unwrap_or(egui::Rect::EVERYTHING)
|
||||
});
|
||||
let position = [main_rect.right(), main_rect.min.y];
|
||||
ctx.show_viewport_immediate(
|
||||
egui::ViewportId::from_hash_of("debug_viewport"),
|
||||
egui::ViewportBuilder::default()
|
||||
.with_title("GTA Tools Debug")
|
||||
.with_minimize_button(false)
|
||||
.with_maximize_button(false)
|
||||
.with_inner_size(WINDOW_SIZE)
|
||||
.with_position(position)
|
||||
.with_icon(tools::load_icon()),
|
||||
|ctx, _class| {
|
||||
if tools::check_debug_keycombo_pressed(ctx) {
|
||||
self.flags.debug = !self.flags.debug;
|
||||
}
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
egui::ScrollArea::both()
|
||||
.auto_shrink([false, true])
|
||||
.show(ui, |ui| {
|
||||
self.show_debug(ctx, ui);
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
ctx.request_repaint_after(Duration::from_millis(100));
|
||||
if self.flags.closing {
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn show_game(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) {
|
||||
tools::header(ui, "Game");
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("Launch").clicked() {
|
||||
features::launch::launch(&self.launch.selected);
|
||||
}
|
||||
tools::build_combo_box::<features::launch::Platform>(
|
||||
ui,
|
||||
&mut self.launch.selected,
|
||||
"Launch",
|
||||
);
|
||||
});
|
||||
let force_close_button = ui.add_sized(
|
||||
[104.0, 0.0],
|
||||
egui::Button::new(&self.force_close.button_text),
|
||||
);
|
||||
if force_close_button.clicked() && !self.force_close.prompting {
|
||||
self.force_close.prompting();
|
||||
self.flags.current_frame = true;
|
||||
}
|
||||
if self.force_close.prompting
|
||||
&& self.force_close.interval.elapsed() <= Duration::from_secs(3)
|
||||
{
|
||||
if force_close_button.clicked() && !self.flags.current_frame {
|
||||
features::force_close::activate(&mut self.sysinfo);
|
||||
self.force_close = ForceClose::default();
|
||||
}
|
||||
} else {
|
||||
self.force_close = ForceClose::default();
|
||||
}
|
||||
if self.flags.current_frame {
|
||||
self.flags.current_frame = false;
|
||||
}
|
||||
}
|
||||
|
||||
fn show_session(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) {
|
||||
tools::header(ui, "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 util::win::is_window_focused(GTA_WINDOW_TITLE) {
|
||||
"GTA is focused."
|
||||
} else {
|
||||
"GTA is not focused!"
|
||||
})
|
||||
});
|
||||
}
|
||||
});
|
||||
if self.anti_afk.enabled && self.anti_afk.interval.elapsed() >= features::anti_afk::INTERVAL
|
||||
{
|
||||
self.anti_afk.activate();
|
||||
}
|
||||
}
|
||||
|
||||
fn show_network(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) {
|
||||
tools::header(ui, "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.label("Game's network access");
|
||||
ui.horizontal(|ui| {
|
||||
let available_width = label.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()
|
||||
{
|
||||
features::game_networking::block_all(&mut self.sysinfo);
|
||||
}
|
||||
if ui
|
||||
.add_sized([button_width, 18.0], egui::Button::new("Unblock"))
|
||||
.clicked()
|
||||
{
|
||||
features::game_networking::unblock_all();
|
||||
}
|
||||
});
|
||||
});
|
||||
response.response.on_disabled_hover_text(
|
||||
"This requires administrator.\nUse the Elevate button.",
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn show_about(&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"
|
||||
)));
|
||||
});
|
||||
}
|
||||
|
||||
fn show_settings(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Theme");
|
||||
tools::build_combo_box::<settings::Theme>(ui, &mut self.settings.theme, "Theme");
|
||||
});
|
||||
ui.checkbox(&mut self.settings.start_elevated, "Always start elevated");
|
||||
}
|
||||
|
||||
fn show_debug(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) {
|
||||
if ui.button("open storage path").clicked() {
|
||||
open::that_detached(APP_STORAGE_PATH.as_path()).unwrap();
|
||||
}
|
||||
ui.collapsing("anti afk", |ui| {
|
||||
ui.label(format!(
|
||||
"timer: {}",
|
||||
self.anti_afk.interval.elapsed().as_secs()
|
||||
));
|
||||
ui.label(format!(
|
||||
"can activate: {}",
|
||||
features::anti_afk::can_activate()
|
||||
));
|
||||
});
|
||||
ui.collapsing("sysinfo", |ui| {
|
||||
if ui.button("refresh all").clicked() {
|
||||
self.sysinfo.refresh_all();
|
||||
}
|
||||
let pid = self
|
||||
.sysinfo
|
||||
.processes()
|
||||
.iter()
|
||||
.find(|(_, p)| p.name() == ENHANCED || p.name() == LEGACY)
|
||||
.map_or_else(
|
||||
|| "no pid found!".to_owned(),
|
||||
|(pid, _)| pid.as_u32().to_string(),
|
||||
);
|
||||
ui.label(format!("gta pid: {pid}"));
|
||||
});
|
||||
ui.collapsing("app state", |ui| ui.label(format!("{self:#?}")));
|
||||
}
|
||||
|
||||
fn run_timers(&mut self) {
|
||||
if self.empty_session.disabled {
|
||||
self.empty_session.countdown.count();
|
||||
} else {
|
||||
self.empty_session.countdown.reset();
|
||||
}
|
||||
if self.empty_session.interval.elapsed() >= features::empty_session::INTERVAL {
|
||||
features::empty_session::deactivate(&mut self.game_handle);
|
||||
self.empty_session.disabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for App {
|
||||
fn drop(&mut self) {
|
||||
// save any persistent state to config file
|
||||
let persistent_state = PersistentState {
|
||||
launcher: self.launch.selected,
|
||||
settings: self.settings.clone(),
|
||||
};
|
||||
persistent_state.set();
|
||||
// make sure we are not suspending game
|
||||
features::empty_session::deactivate(&mut self.game_handle);
|
||||
}
|
||||
}
|
||||
+4
-440
@@ -1,440 +1,4 @@
|
||||
mod persistent_state;
|
||||
mod settings;
|
||||
|
||||
use crate::{
|
||||
features::{
|
||||
self, anti_afk::AntiAfk, empty_session::EmptySession, force_close::ForceClose,
|
||||
launch::Launch,
|
||||
},
|
||||
gui::{persistent_state::PersistentState, settings::Settings},
|
||||
util::{
|
||||
self,
|
||||
consts::{APP_STORAGE_PATH, ENHANCED, GTA_WINDOW_TITLE, LEGACY},
|
||||
},
|
||||
};
|
||||
use eframe::egui;
|
||||
use std::{
|
||||
fs,
|
||||
io::Write,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
const WINDOW_SIZE: [f32; 2] = [240.0, 245.0];
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq)]
|
||||
pub enum Stage {
|
||||
#[default]
|
||||
Main,
|
||||
Settings,
|
||||
About,
|
||||
}
|
||||
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Flags {
|
||||
elevated: bool,
|
||||
debug: bool,
|
||||
closing: bool,
|
||||
current_frame: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct App {
|
||||
settings: Settings,
|
||||
stage: Stage,
|
||||
flags: Flags,
|
||||
pub sysinfo: sysinfo::System,
|
||||
pub game_handle: windows::Win32::Foundation::HANDLE,
|
||||
launch: Launch,
|
||||
force_close: ForceClose,
|
||||
empty_session: EmptySession,
|
||||
anti_afk: AntiAfk,
|
||||
}
|
||||
|
||||
impl Default for App {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
settings: Settings::default(),
|
||||
stage: Stage::default(),
|
||||
flags: Flags::default(),
|
||||
sysinfo: sysinfo::System::new_all(),
|
||||
game_handle: windows::Win32::Foundation::HANDLE::default(),
|
||||
launch: Launch::default(),
|
||||
force_close: ForceClose::default(),
|
||||
empty_session: EmptySession::default(),
|
||||
anti_afk: AntiAfk::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for App {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
catppuccin_egui::set_theme(ctx, self.settings.theme.into());
|
||||
self.run_timers();
|
||||
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.selectable_value(&mut self.stage, Stage::Main, "Main");
|
||||
ui.selectable_value(&mut self.stage, Stage::Settings, "Settings");
|
||||
ui.selectable_value(&mut self.stage, Stage::About, "About");
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
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() {
|
||||
util::win::elevate(util::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_game(ctx, ui);
|
||||
self.show_session(ctx, ui);
|
||||
self.show_network(ctx, ui);
|
||||
}
|
||||
Stage::Settings => self.show_settings(ctx, ui),
|
||||
Stage::About => self.show_about(ctx, ui),
|
||||
});
|
||||
});
|
||||
if check_debug_keycombo_pressed(ctx) {
|
||||
self.flags.debug = !self.flags.debug;
|
||||
}
|
||||
if check_debug_viewport_close_button_pressed(ctx) {
|
||||
self.flags.debug = false;
|
||||
}
|
||||
if self.flags.debug {
|
||||
let main_rect = ctx.input(|i| {
|
||||
i.viewport()
|
||||
.clone()
|
||||
.outer_rect
|
||||
.unwrap_or(egui::Rect::EVERYTHING)
|
||||
});
|
||||
let position = [main_rect.right(), main_rect.min.y];
|
||||
ctx.show_viewport_immediate(
|
||||
egui::ViewportId::from_hash_of("debug_viewport"),
|
||||
egui::ViewportBuilder::default()
|
||||
.with_title("GTA Tools Debug")
|
||||
.with_minimize_button(false)
|
||||
.with_maximize_button(false)
|
||||
.with_inner_size(WINDOW_SIZE)
|
||||
.with_position(position)
|
||||
.with_icon(load_icon()),
|
||||
|ctx, _class| {
|
||||
if check_debug_keycombo_pressed(ctx) {
|
||||
self.flags.debug = !self.flags.debug;
|
||||
}
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
egui::ScrollArea::both()
|
||||
.auto_shrink([false, true])
|
||||
.show(ui, |ui| {
|
||||
self.show_debug(ctx, ui);
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
ctx.request_repaint_after(Duration::from_millis(100));
|
||||
if self.flags.closing {
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn show_game(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) {
|
||||
header(ui, "Game");
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("Launch").clicked() {
|
||||
features::launch::launch(&self.launch.selected);
|
||||
}
|
||||
build_combo_box::<features::launch::Platform>(ui, &mut self.launch.selected, "Launch");
|
||||
});
|
||||
let force_close_button = ui.add_sized(
|
||||
[104.0, 0.0],
|
||||
egui::Button::new(&self.force_close.button_text),
|
||||
);
|
||||
if force_close_button.clicked() && !self.force_close.prompting {
|
||||
self.force_close.prompting();
|
||||
self.flags.current_frame = true;
|
||||
}
|
||||
if self.force_close.prompting
|
||||
&& self.force_close.interval.elapsed() <= Duration::from_secs(3)
|
||||
{
|
||||
if force_close_button.clicked() && !self.flags.current_frame {
|
||||
features::force_close::activate(&mut self.sysinfo);
|
||||
self.force_close = ForceClose::default();
|
||||
}
|
||||
} else {
|
||||
self.force_close = ForceClose::default();
|
||||
}
|
||||
if self.flags.current_frame {
|
||||
self.flags.current_frame = false;
|
||||
}
|
||||
}
|
||||
|
||||
fn show_session(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) {
|
||||
header(ui, "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(self);
|
||||
}
|
||||
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 util::win::is_window_focused(GTA_WINDOW_TITLE) {
|
||||
"GTA is focused."
|
||||
} else {
|
||||
"GTA is not focused!"
|
||||
})
|
||||
});
|
||||
}
|
||||
});
|
||||
if self.anti_afk.enabled && self.anti_afk.interval.elapsed() >= features::anti_afk::INTERVAL
|
||||
{
|
||||
self.anti_afk.activate();
|
||||
}
|
||||
}
|
||||
|
||||
fn show_network(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) {
|
||||
header(ui, "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.label("Game's network access");
|
||||
ui.horizontal(|ui| {
|
||||
let available_width = label.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()
|
||||
{
|
||||
features::game_networking::block_all(&mut self.sysinfo);
|
||||
}
|
||||
if ui
|
||||
.add_sized([button_width, 18.0], egui::Button::new("Unblock"))
|
||||
.clicked()
|
||||
{
|
||||
features::game_networking::unblock_all();
|
||||
}
|
||||
});
|
||||
});
|
||||
response.response.on_disabled_hover_text(
|
||||
"This requires administrator.\nUse the Elevate button.",
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_self)]
|
||||
fn show_about(&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 love from ");
|
||||
ui.hyperlink_to("futile", "http://futile.eu");
|
||||
});
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
ui.label(format!(
|
||||
"v{} {}",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
if cfg!(debug_assertions) { "(dev)" } else { "" }
|
||||
));
|
||||
});
|
||||
});
|
||||
ui.add(egui::Image::new(egui::include_image!(
|
||||
"../../assets/icon.png"
|
||||
)));
|
||||
});
|
||||
}
|
||||
|
||||
fn show_settings(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Theme");
|
||||
build_combo_box::<settings::Theme>(ui, &mut self.settings.theme, "Theme");
|
||||
});
|
||||
ui.checkbox(&mut self.settings.start_elevated, "Always start elevated");
|
||||
}
|
||||
|
||||
fn show_debug(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) {
|
||||
if ui.button("open storage path").clicked() {
|
||||
open::that_detached(APP_STORAGE_PATH.as_path()).unwrap();
|
||||
}
|
||||
ui.collapsing("anti afk", |ui| {
|
||||
ui.label(format!(
|
||||
"timer: {}",
|
||||
self.anti_afk.interval.elapsed().as_secs()
|
||||
));
|
||||
ui.label(format!(
|
||||
"can activate: {}",
|
||||
features::anti_afk::can_activate()
|
||||
));
|
||||
});
|
||||
ui.collapsing("sysinfo", |ui| {
|
||||
if ui.button("refresh all").clicked() {
|
||||
self.sysinfo.refresh_all();
|
||||
}
|
||||
let pid = self
|
||||
.sysinfo
|
||||
.processes()
|
||||
.iter()
|
||||
.find(|(_, p)| p.name() == ENHANCED || p.name() == LEGACY)
|
||||
.map_or_else(
|
||||
|| "no pid found!".to_string(),
|
||||
|(pid, _)| pid.as_u32().to_string(),
|
||||
);
|
||||
ui.label(format!("gta pid: {pid}"));
|
||||
});
|
||||
ui.collapsing("app state", |ui| ui.label(format!("{self:#?}")));
|
||||
}
|
||||
|
||||
fn run_timers(&mut self) {
|
||||
if self.empty_session.disabled {
|
||||
self.empty_session.countdown.count();
|
||||
} else {
|
||||
self.empty_session.countdown.reset();
|
||||
}
|
||||
if self.empty_session.interval.elapsed() >= features::empty_session::INTERVAL {
|
||||
features::empty_session::deactivate(self);
|
||||
self.empty_session.disabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for App {
|
||||
fn drop(&mut self) {
|
||||
// save any persistent state to config file
|
||||
let persistent_state = PersistentState {
|
||||
launcher: self.launch.selected,
|
||||
settings: self.settings.clone(),
|
||||
};
|
||||
persistent_state.set();
|
||||
// make sure we are not suspending game
|
||||
features::empty_session::deactivate(self);
|
||||
}
|
||||
}
|
||||
|
||||
fn header(ui: &mut egui::Ui, text: &str) {
|
||||
ui.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_combo_box<E>(ui: &mut egui::Ui, current_value: &mut E, label: impl std::hash::Hash)
|
||||
where
|
||||
E: strum::IntoEnumIterator + std::fmt::Display + std::cmp::PartialEq + Copy,
|
||||
{
|
||||
egui::ComboBox::from_id_salt(label)
|
||||
.selected_text(current_value.to_string())
|
||||
.show_ui(ui, |ui| {
|
||||
for v in E::iter() {
|
||||
ui.selectable_value(current_value, v, v.to_string());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn check_debug_keycombo_pressed(ctx: &egui::Context) -> bool {
|
||||
ctx.input(|i| i.modifiers.all() && i.key_pressed(egui::Key::D))
|
||||
}
|
||||
|
||||
fn check_debug_viewport_close_button_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()
|
||||
})
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
fn panic_hook(panic_info: &std::panic::PanicHookInfo<'_>) {
|
||||
let log_path = APP_STORAGE_PATH.join("panic.log");
|
||||
let mut file = fs::File::options()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(log_path)
|
||||
.unwrap();
|
||||
let timestamp = chrono::Local::now().to_rfc3339();
|
||||
let backtrace = std::backtrace::Backtrace::force_capture();
|
||||
let message = format!("[{timestamp}]\n{panic_info}\n\n{backtrace}\n\n");
|
||||
file.write_all(message.as_bytes()).unwrap();
|
||||
}
|
||||
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
fn app_creator(
|
||||
cc: &eframe::CreationContext<'_>,
|
||||
) -> Result<Box<dyn eframe::App>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
std::panic::set_hook(Box::new(panic_hook));
|
||||
let mut app = Box::<App>::default();
|
||||
if let Some(persistent_state) = PersistentState::get() {
|
||||
app.launch.selected = persistent_state.launcher;
|
||||
app.settings = persistent_state.settings;
|
||||
}
|
||||
let elevated = util::win::is_elevated();
|
||||
if app.settings.start_elevated && !elevated {
|
||||
util::win::elevate(util::win::ElevationExitMethod::Forced);
|
||||
}
|
||||
app.flags.elevated = elevated;
|
||||
egui_extras::install_image_loaders(&cc.egui_ctx);
|
||||
cc.egui_ctx.style_mut(|style| {
|
||||
style.spacing.item_spacing = egui::vec2(4.0, 4.0);
|
||||
style.interaction.selectable_labels = false;
|
||||
});
|
||||
let mut fonts = egui::FontDefinitions::default();
|
||||
fonts.font_data.insert(
|
||||
"Ubuntu-Regular".to_string(),
|
||||
egui::FontData::from_static(include_bytes!("../../assets/Ubuntu-Regular.ttf")).into(),
|
||||
);
|
||||
fonts.families.insert(
|
||||
egui::FontFamily::Name("Ubuntu-Regular".into()),
|
||||
vec!["Ubuntu-Regular".to_string()],
|
||||
);
|
||||
cc.egui_ctx.set_fonts(fonts);
|
||||
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(load_icon()),
|
||||
centered: true,
|
||||
..Default::default()
|
||||
};
|
||||
eframe::run_native("GTA Tools", options, Box::new(app_creator)).unwrap();
|
||||
}
|
||||
mod app;
|
||||
pub mod run;
|
||||
pub mod settings;
|
||||
mod tools;
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
use crate::{
|
||||
gui::{
|
||||
app::{App, WINDOW_SIZE},
|
||||
tools,
|
||||
},
|
||||
util::{self, consts::APP_STORAGE_PATH, persistent_state::PersistentState},
|
||||
};
|
||||
use eframe::egui;
|
||||
use std::{fs::File, io::Write};
|
||||
|
||||
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 = chrono::Local::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, false);
|
||||
let backtrace = std::backtrace::Backtrace::force_capture();
|
||||
let message = format!("[{timestamp}]\n{panic_info}\nstack backtrace:\n{backtrace}\n");
|
||||
file.write_all(message.as_bytes()).unwrap();
|
||||
}
|
||||
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
fn app_creator(
|
||||
cc: &eframe::CreationContext<'_>,
|
||||
) -> Result<Box<dyn eframe::App>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
std::panic::set_hook(Box::new(panic_hook));
|
||||
let mut app = Box::<App>::default();
|
||||
if let Some(persistent_state) = PersistentState::get() {
|
||||
app.launch.selected = persistent_state.launcher;
|
||||
app.settings = persistent_state.settings;
|
||||
}
|
||||
let elevated = util::win::is_elevated();
|
||||
if app.settings.start_elevated && !elevated {
|
||||
util::win::elevate(util::win::ElevationExitMethod::Forced);
|
||||
}
|
||||
app.flags.elevated = elevated;
|
||||
egui_extras::install_image_loaders(&cc.egui_ctx);
|
||||
cc.egui_ctx.style_mut(|style| {
|
||||
style.spacing.item_spacing = egui::vec2(4.0, 4.0);
|
||||
style.interaction.selectable_labels = false;
|
||||
});
|
||||
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);
|
||||
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,49 @@
|
||||
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 header(ui: &mut egui::Ui, text: &str) {
|
||||
ui.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());
|
||||
});
|
||||
}
|
||||
|
||||
pub fn build_combo_box<E>(ui: &mut egui::Ui, current_value: &mut E, label: impl std::hash::Hash)
|
||||
where
|
||||
E: strum::IntoEnumIterator + std::fmt::Display + std::cmp::PartialEq + Copy,
|
||||
{
|
||||
egui::ComboBox::from_id_salt(label)
|
||||
.selected_text(current_value.to_string())
|
||||
.show_ui(ui, |ui| {
|
||||
E::iter().for_each(|v| {
|
||||
ui.selectable_value(current_value, v, v.to_string());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn check_debug_keycombo_pressed(ctx: &egui::Context) -> bool {
|
||||
ctx.input(|i| i.modifiers.all() && i.key_pressed(egui::Key::D))
|
||||
}
|
||||
|
||||
pub fn check_debug_viewport_close_button_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()
|
||||
})
|
||||
}
|
||||
+1
-1
@@ -5,5 +5,5 @@ mod gui;
|
||||
mod util;
|
||||
|
||||
fn main() {
|
||||
gui::run();
|
||||
gui::run::run();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
use semver::Version;
|
||||
|
||||
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::parse(env!("CARGO_PKG_VERSION")).unwrap(),
|
||||
download_url: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_latest_release() -> Option<Release> {
|
||||
let request_url = format!("{CODEBERG_ENDPOINT_ROOT}/repos/futile/gta-tools/releases/latest");
|
||||
let ureq: ureq::Agent = ureq::Agent::config_builder()
|
||||
.user_agent(APP_USER_AGENT)
|
||||
.build()
|
||||
.into();
|
||||
let mut response = ureq.get(request_url).call().ok()?;
|
||||
let json = response.body_mut().read_json::<serde_json::Value>().ok()?;
|
||||
let tag_name = json["tag_name"].as_str()?;
|
||||
let browser_download_url = json["assets"][0]["browser_download_url"].as_str()?;
|
||||
Some(Release {
|
||||
version: Version::parse(tag_name).expect("expected a valid semver pattern"),
|
||||
download_url: String::from(browser_download_url),
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
pub mod codeberg;
|
||||
pub mod consts;
|
||||
pub mod countdown;
|
||||
pub mod meta;
|
||||
pub mod persistent_state;
|
||||
pub mod win;
|
||||
|
||||
Reference in New Issue
Block a user