├── .github └── workflows │ ├── ci.yml │ └── downloads.yml ├── .gitignore ├── Cargo.toml ├── LICENSE.md ├── README.md ├── assets ├── logo.png └── run.ico ├── autorun-shared ├── Cargo.toml └── src │ ├── config.rs │ ├── lib.rs │ └── top.rs ├── autorun ├── Cargo.toml ├── build.rs └── src │ ├── configs │ ├── mod.rs │ └── settings.toml │ ├── cross.rs │ ├── fs │ ├── mod.rs │ └── path.rs │ ├── hooks │ ├── dumper.rs │ ├── lazy.rs │ ├── mod.rs │ └── scripthook.rs │ ├── lib.rs │ ├── logging │ └── mod.rs │ ├── lua │ ├── env.rs │ ├── err.rs │ └── mod.rs │ ├── plugins │ ├── mod.rs │ └── serde.rs │ ├── ui │ ├── console │ │ ├── commands.rs │ │ ├── mod.rs │ │ ├── palette.rs │ │ └── tray.rs │ └── mod.rs │ └── version.rs ├── examples ├── autorun.lua └── hook.lua ├── fields.lua └── rustfmt.toml /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - 'autorun/src/**' 7 | - 'autorun-shared/src/**' 8 | workflow_dispatch: 9 | 10 | env: 11 | CARGO_TERM_COLOR: always 12 | 13 | jobs: 14 | build64: 15 | runs-on: windows-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Build DLL 19 | run: | 20 | rustup default nightly 21 | cd autorun 22 | cargo build --release --verbose 23 | 24 | # In the future make this run on an x86 machine https://github.com/actions/runner/issues/423 25 | build86: 26 | runs-on: windows-latest 27 | steps: 28 | - uses: actions/checkout@v4 29 | - name: Build DLL 30 | run: | 31 | rustup default nightly 32 | rustup target add i686-pc-windows-msvc 33 | cd autorun 34 | cargo build --release --verbose --target=i686-pc-windows-msvc 35 | -------------------------------------------------------------------------------- /.github/workflows/downloads.yml: -------------------------------------------------------------------------------- 1 | name: Download 2 | 3 | on: 4 | push: 5 | paths: 6 | - 'autorun/**' 7 | - 'autorun-shared/**' 8 | workflow_dispatch: 9 | 10 | env: 11 | CARGO_TERM_COLOR: always 12 | 13 | jobs: 14 | upload64: 15 | runs-on: windows-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Build DLL 19 | run: | 20 | rustup default nightly 21 | cd autorun 22 | cargo build --release --verbose 23 | cd .. 24 | move target/release/autorun.dll gmsv_autorun_win64.dll 25 | 26 | - name: Upload DLL 27 | uses: actions/upload-artifact@v4 28 | with: 29 | name: Windows 64 Bit 30 | path: gmsv_autorun_win64.dll 31 | 32 | # In the future make this run on an x86 machine https://github.com/actions/runner/issues/423 33 | upload86: 34 | runs-on: windows-latest 35 | steps: 36 | - uses: actions/checkout@v4 37 | - name: Build DLL 38 | run: | 39 | rustup default nightly 40 | rustup target add i686-pc-windows-msvc 41 | cd autorun 42 | cargo build --release --verbose --target=i686-pc-windows-msvc 43 | cd .. 44 | move target/i686-pc-windows-msvc/release/autorun.dll gmsv_autorun_win32.dll 45 | 46 | - name: Upload DLL 47 | uses: actions/upload-artifact@v4 48 | with: 49 | name: Windows 32 Bit 50 | path: gmsv_autorun_win32.dll 51 | 52 | upload64_ubuntu: 53 | runs-on: ubuntu-latest 54 | steps: 55 | - uses: actions/checkout@v4 56 | - name: Build DLL 57 | run: | 58 | cd autorun 59 | cargo build --release --verbose 60 | cd .. 61 | mv target/release/libautorun.so gmsv_autorun_linux64.dll 62 | 63 | - name: Upload DLL 64 | uses: actions/upload-artifact@v4 65 | with: 66 | name: Linux 64 Bit 67 | path: gmsv_autorun_linux64.dll 68 | 69 | upload86_ubuntu: 70 | runs-on: ubuntu-latest 71 | steps: 72 | - uses: actions/checkout@v4 73 | - name: Build DLL 74 | run: | 75 | sudo apt-get update && sudo apt-get install gcc-multilib 76 | rustup target add i686-unknown-linux-gnu 77 | cd autorun 78 | cargo build --release --verbose --target=i686-unknown-linux-gnu 79 | cd .. 80 | mv target/i686-unknown-linux-gnu/release/libautorun.so gmsv_autorun_linux32.dll 81 | 82 | - name: Upload DLL 83 | uses: actions/upload-artifact@v4 84 | with: 85 | name: Linux 32 Bit 86 | path: gmsv_autorun_linux32.dll 87 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | .vscode 4 | 5 | *.exe 6 | *.dll 7 | *.log 8 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "autorun", 4 | "autorun-shared" 5 | ] 6 | exclude = ["assets"] 7 | 8 | [profile.release] 9 | panic = "abort" 10 | opt-level = "z" 11 | lto = "fat" -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Autorun](assets/logo.png)](https://github.com/Vurv78/Autorun-rs) 2 | # [![Release Shield](https://img.shields.io/github/v/release/Vurv78/Autorun-rs)](https://github.com/Vurv78/Autorun-rs/releases/latest) [![License](https://img.shields.io/github/license/Vurv78/Autorun-rs?color=red)](https://opensource.org/licenses/Apache-2.0) [![CI](https://github.com/Vurv78/Autorun-rs/workflows/Download/badge.svg)](https://github.com/Vurv78/Autorun-rs/actions/workflows/downloads.yml) [![github/Vurv78](https://img.shields.io/discord/824727565948157963?label=Discord&logo=discord&logoColor=ffffff&labelColor=7289DA&color=2c2f33)](https://discord.gg/yXKMt2XUXm) 3 | 4 | # ⚠️ Warning 5 | 6 | **If you are on 1.2.0 upgrade to [`1.2.5`](https://github.com/Vurv78/Autorun-rs/releases/tag/v1.2.5) ASAP** 7 | 8 | Some sore losers have been exploiting an issue that was fixed in 1.2.4 and the fix is now in 1.2.5 9 | 10 | It would allow people to create lua files in locations outside of the Autorun directory, potentially filling your drive with garbage data. 11 | 12 | ## Features 13 | * Dumping all lua scripts to ``C:\Users\\autorun\lua_dumps\\..`` (asynchronously to avoid i/o lag) 14 | * Runtime lua loading through ``lua_run`` and ``lua_openscript`` in an external console 15 | * Supports both 32* and 64 bit branches (*See [#22](https://github.com/Vurv78/Autorun-rs/issues/22)) 16 | * Running a script before autorun (``autorun.lua``), to detour and bypass any 'anticheats' 17 | * Scripthook, stop & run scripts before anything runs on you, gives information & functions to assist in a safe separate lua environment 18 | * File logging (to ``autorun/logs``) 19 | * Plugin system (``autorun/plugins``) 20 | * [Settings using TOML](autorun/src/configs/settings.toml) 21 | 22 | ## 🤔 Usage 23 | ### 🧩 Menu Plugin 24 | Autorun can also be used as a menu plugin / required from lua automatically from the menu state. 25 | 1. Put [the dll](#%EF%B8%8F-downloading) ``gmsv_autorun_win.dll`` file into your ``garrysmod/lua/bin`` folder. 26 | 2. Add ``require("autorun")`` at the bottom of ``garrysmod/lua/menu/menu.lua`` 27 | **It will now run automatically when garrysmod loads at the menu.** 28 | 29 | ### 💉 Injecting 30 | The traditional (but more inconvenient) method to use this is to just inject it. 31 | 1. Get an injector (Make sure it's compatible to inject 32/64 bit code depending on your use). 32 | 2. Inject [the dll](#%EF%B8%8F-downloading) into gmod while you're in the menu 33 | 34 | ## 📜 Scripthook 35 | Autorun features scripthook, which means we'll run your script before any other garrysmod script executes to verify if you want the code to run by running your own hook script. 36 | *This runs in a separate environment from ``_G``, so to modify globals, do ``_G.foo = bar`` 37 | 38 | Also note that if you are running in ``autorun.lua`` Functions like ``http.Fetch`` & ``file.Write`` won't exist. 39 | Use their C counterparts (``HTTP`` and ``file.Open``) 40 | 41 | __See an example project using the scripthook [here](https://github.com/Vurv78/Safety).__ 42 | 43 | ### 📁 File Structure 44 | ```golo 45 | C:\Users\\autorun 46 | ├── \autorun.lua # Runs *once* before autorun 47 | ├── \hook.lua # Runs for every script 48 | ├── \lua_dumps\ # Each server gets a folder with their IP as the name. 49 | │ ├── \192.168.1.55_27015\ 50 | │ └── \X.Y.Z.W_PORT\ 51 | ├── \logs\ # Logs are saved here 52 | │ └── YYYY-MM-DD.log 53 | ├── \bin\ # Store binary modules to be used with Autorun.requirebin 54 | │ └── gmcl_vistrace_win64.dll 55 | ├── \plugins\ # Folder for Autorun plugins, same behavior as above autorun and hook.lua, but meant for plugin developers. 56 | │ └── \Safety\ 57 | │ ├── \src\ 58 | | | ├── autorun.lua 59 | | | └── hook.lua 60 | │ └── plugin.toml 61 | ├── settings.toml # See autorun/src/configs/settings.toml 62 | └── ... 63 | ``` 64 | 65 | ### 🗃️ Fields 66 | You can find what is passed to the scripthook environment in [fields.lua](fields.lua) as an EmmyLua definitions file. 67 | This could be used with something like a vscode lua language server extension for intellisense 👍 68 | 69 | ### ✍️ Examples 70 | __hook.lua__ 71 | This file runs before every single lua script run on your client from addons and servers. 72 | You can ``return true`` to not run the script, or a string to replace it. 73 | ```lua 74 | -- Replace all 'while true do end' scripts with 'while false do end' 😎 75 | local script = Autorun.CODE 76 | if script:find("while true do end") then 77 | Autorun.log("Found an evil script!") 78 | return string.Replace(script, "while true do end", "while false do end") 79 | end 80 | ``` 81 | 82 | You can find more [here](examples) 83 | 84 | ## ⬇️ Downloading 85 | ### 🦺 Stable 86 | You can get a 'stable' release from [the releases](https://github.com/Vurv78/Autorun-rs/releases/latest). 87 | ### 🩸 Bleeding Edge 88 | You can get the absolute latest download (from code in the repo) in [the Github Actions tab](https://github.com/Vurv78/Autorun-rs/actions/workflows/downloads.yml) 89 | Note it may not work as expected (but I'd advise to try this out before trying to report an issue to see if it has been fixed) 90 | 91 | __If you are using this as a menu plugin 🧩, make sure the DLL is named ``gmsv_autorun_win.dll``__ 92 | 93 | ## 🛠️ Building 94 | You may want to build this yourself if you want to make changes / contribute (or don't trust github actions for whatever reason..) 95 | 1. [Setup Rust & Cargo](https://www.rust-lang.org/learn/get-started) 96 | 2. Use ``build_win_32.bat`` or ``build_win_64.bat``. 97 | -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thevurv/Autorun-rs/720ab4eb6927a25c7304f4804da8498c12f1a6e7/assets/logo.png -------------------------------------------------------------------------------- /assets/run.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thevurv/Autorun-rs/720ab4eb6927a25c7304f4804da8498c12f1a6e7/assets/run.ico -------------------------------------------------------------------------------- /autorun-shared/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "autorun-shared" 3 | description = "Shared autorun functionality" 4 | version = "0.1.1" 5 | authors = ["Vurv78 "] 6 | edition = "2021" 7 | publish = false 8 | 9 | [features] 10 | default = [] -------------------------------------------------------------------------------- /autorun-shared/src/config.rs: -------------------------------------------------------------------------------- 1 | pub const IP: &str = "127.0.0.1:3005"; 2 | -------------------------------------------------------------------------------- /autorun-shared/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod top; 2 | pub use top::*; 3 | 4 | mod config; 5 | pub use config::*; 6 | -------------------------------------------------------------------------------- /autorun-shared/src/top.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | // I know this is a waste to contain 'server' but I want it to be able to be used with GetLuaInterface 4 | #[repr(u8)] 5 | #[derive(Clone, Copy, Debug)] 6 | pub enum Realm { 7 | Client = 0, 8 | Server = 1, 9 | Menu = 2, 10 | } 11 | 12 | impl fmt::Display for Realm { 13 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 14 | write!( 15 | f, 16 | "{}", 17 | match self { 18 | Realm::Client => "client", 19 | Realm::Server => "server", 20 | Realm::Menu => "menu", 21 | } 22 | ) 23 | } 24 | } 25 | 26 | impl From for Realm { 27 | fn from(realm: u8) -> Self { 28 | match realm { 29 | 0 => Realm::Client, 30 | 1 => Realm::Server, 31 | 2 => Realm::Menu, 32 | _ => panic!("Invalid realm"), 33 | } 34 | } 35 | } 36 | 37 | impl From for u8 { 38 | fn from(r: Realm) -> u8 { 39 | match r { 40 | Realm::Client => 0, 41 | Realm::Server => 1, 42 | Realm::Menu => 2, 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /autorun/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "autorun" 3 | version = "1.2.5" 4 | authors = ["Vurv78 "] 5 | edition = "2021" 6 | repository = "https://github.com/Vurv78/Autorun-rs" 7 | publish = false 8 | 9 | [lib] 10 | crate-type = ["cdylib"] 11 | 12 | [features] 13 | default = ["executor", "inject", "plugins", "colors", "http"] 14 | 15 | executor = [] # Executor inside console 16 | inject = [] # Allow injection through DllMain export 17 | plugins = [] 18 | colors = [] # Colors in external console 19 | http = ["tinyget"] # To check for updates and download plugins in the future. 20 | 21 | [dependencies] 22 | autorun-shared = { path = "../autorun-shared" } 23 | rglua = { version = "3.0.0" } 24 | 25 | lupat = { git = "https://github.com/Vurv78/lupat" } 26 | detour = { version = "0.8", default-features = false } 27 | 28 | # Error handling 29 | thiserror = "1.0" 30 | fs-err = "2.7" 31 | 32 | # Settings 33 | serde = { version = "1.0", features = ["derive"] } 34 | toml = "0.5" 35 | 36 | # Global Mutable Variables 37 | once_cell = "1.10" 38 | 39 | # Misc 40 | home = "0.5" 41 | libloading = "0.7" # For Autorun.requirebin 42 | 43 | # Console 44 | time = "0.3" 45 | colored = "2" 46 | 47 | # Windows specific 48 | [target.'cfg(windows)'.dependencies] 49 | trayicon = "=0.1.3" 50 | winapi = { version = "0.3", features = ["wincontypes", "wincon", "processenv", "consoleapi"] } 51 | tinyget = { version = "1.0", features = ["https"], optional = true } 52 | 53 | [target.'cfg(unix)'.dependencies] 54 | winit = "0.26" 55 | 56 | [build-dependencies] 57 | cfg_aliases = "0.1" 58 | -------------------------------------------------------------------------------- /autorun/build.rs: -------------------------------------------------------------------------------- 1 | use cfg_aliases::cfg_aliases; 2 | 3 | fn main() { 4 | cfg_aliases! { 5 | win64: { all(target_os = "windows", target_arch = "x86_64") }, 6 | linux64: { all(target_os = "linux", target_arch = "x86_64") }, 7 | 8 | executor: { all(feature = "executor", win64) }, 9 | inject: { all(feature = "inject", windows) }, 10 | plugins: { all(feature = "plugins", windows) }, 11 | colors: { all(feature = "colors", windows) }, 12 | http: { all(feature = "http", windows) } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /autorun/src/configs/mod.rs: -------------------------------------------------------------------------------- 1 | use fs_err as fs; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | #[derive(Debug, Deserialize, Serialize, Default, PartialEq)] 5 | pub struct Settings { 6 | pub autorun: AutorunSettings, 7 | pub filesteal: FileSettings, 8 | pub logging: LoggerSettings, 9 | pub plugins: PluginSettings, 10 | } 11 | 12 | impl Settings { 13 | pub fn color_enabled(&self) -> bool { 14 | #[allow(deprecated)] 15 | !self 16 | .autorun 17 | .no_color 18 | .unwrap_or_else(|| self.autorun.nocolor.unwrap_or(false)) 19 | } 20 | } 21 | 22 | #[derive(Debug, Deserialize, Serialize, PartialEq)] 23 | pub struct AutorunSettings { 24 | pub hide: bool, 25 | #[deprecated(since = "1.2.3", note = "Use `no_color` instead")] 26 | pub nocolor: Option, 27 | pub no_color: Option, 28 | pub check_version: bool, 29 | } 30 | 31 | impl Default for AutorunSettings { 32 | fn default() -> Self { 33 | #[allow(deprecated)] 34 | Self { 35 | hide: false, 36 | 37 | nocolor: None, 38 | 39 | no_color: Some(false), 40 | check_version: true, 41 | } 42 | } 43 | } 44 | 45 | #[derive(Debug, Deserialize, Serialize, PartialEq)] 46 | pub struct FileSettings { 47 | pub enabled: bool, 48 | pub format: String, 49 | } 50 | 51 | impl Default for FileSettings { 52 | fn default() -> Self { 53 | Self { 54 | enabled: true, 55 | format: "".to_owned(), 56 | } 57 | } 58 | } 59 | 60 | #[derive(Debug, Deserialize, Serialize, PartialEq)] 61 | pub struct LoggerSettings { 62 | pub enabled: bool, 63 | } 64 | 65 | impl Default for LoggerSettings { 66 | fn default() -> Self { 67 | Self { enabled: true } 68 | } 69 | } 70 | 71 | #[derive(Debug, Deserialize, Serialize, PartialEq)] 72 | pub struct PluginSettings { 73 | pub enabled: bool, 74 | } 75 | 76 | impl Default for PluginSettings { 77 | fn default() -> Self { 78 | Self { enabled: true } 79 | } 80 | } 81 | 82 | use crate::fs::SETTINGS_PATH; 83 | 84 | use once_cell::sync::Lazy; 85 | pub static SETTINGS: Lazy = Lazy::new(|| { 86 | let settings_file = crate::fs::in_autorun(SETTINGS_PATH); 87 | let default_settings = include_str!("settings.toml"); 88 | 89 | if settings_file.exists() { 90 | match fs::read_to_string(&settings_file) { 91 | Ok(content) => match toml::from_str(&content) { 92 | Ok(settings) => settings, 93 | Err(why) => { 94 | eprintln!("Failed to parse your autorun/settings.toml file ({why}). Using default settings."); 95 | Settings::default() 96 | } 97 | }, 98 | Err(why) => { 99 | eprintln!("Failed to read your settings file ({why}). Using default settings!"); 100 | Settings::default() 101 | } 102 | } 103 | } else { 104 | // No settings file, create file with default settings, and use that. 105 | if let Err(why) = fs::write(settings_file, default_settings) { 106 | eprintln!("Failed to create default settings file file ({why})"); 107 | } 108 | Settings::default() 109 | } 110 | }); 111 | -------------------------------------------------------------------------------- /autorun/src/configs/settings.toml: -------------------------------------------------------------------------------- 1 | [autorun] 2 | # Whether to hide on startup 3 | hide = false 4 | # Whether to disable terminal colors 5 | no_color = false 6 | # Whether to check if Autorun is outdated on startup 7 | check_version = true 8 | 9 | [plugins] 10 | # Whether to run saved plugins 11 | enabled = true 12 | 13 | [logging] 14 | # Whether to disable saving log files to disk in autorun/logs 15 | # Will still print info/errors to your console 16 | enabled = true 17 | 18 | [filesteal] 19 | # Whether to save lua files from detours in autorun/lua_dumps 20 | enabled = false 21 | 22 | # Format to use when saving folders 23 | # Valid tags: 24 | # IP Address of the server (with port) 25 | # Hostname of the server (Currently will just return CLIENT, so don't use.) 26 | format = "" 27 | -------------------------------------------------------------------------------- /autorun/src/cross.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | hooks::{self, HookingError}, 3 | logging, ui, 4 | }; 5 | 6 | #[cfg(plugins)] 7 | use crate::plugins::{self, PluginError}; 8 | 9 | #[cfg(http)] 10 | use crate::version; 11 | 12 | use fs_err as fs; 13 | use logging::*; 14 | 15 | #[derive(Debug, thiserror::Error)] 16 | pub enum StartError { 17 | #[error("Failed to start logger `{0}`")] 18 | LoggingStart(#[from] logging::LogInitError), 19 | 20 | #[error("Failed to hook functions `{0}`")] 21 | HookError(#[from] HookingError), 22 | 23 | #[cfg(plugins)] 24 | #[error("Failed to start plugins `{0}`")] 25 | PluginError(#[from] PluginError), 26 | 27 | #[error("Program panicked!")] 28 | Panic, 29 | 30 | #[error("Failed to create essential directory `{0}`")] 31 | IO(#[from] std::io::Error), 32 | } 33 | 34 | pub fn startup() -> Result<(), StartError> { 35 | // Catch all potential panics to avoid crashing gmod. 36 | // Will simply report the error and not do anything. 37 | let res: Result, _> = std::panic::catch_unwind(|| { 38 | use crate::fs as afs; 39 | 40 | // /autorun/ 41 | let base = afs::base(); 42 | if !base.exists() { 43 | fs::create_dir(&base)?; 44 | } 45 | 46 | // Make sure all essential directories exist 47 | for p in [ 48 | afs::INCLUDE_DIR, 49 | afs::LOG_DIR, 50 | afs::BIN_DIR, 51 | afs::DUMP_DIR, 52 | afs::PLUGIN_DIR, 53 | ] { 54 | let path = base.join(p); 55 | if !path.exists() { 56 | fs::create_dir(&path)?; 57 | } 58 | } 59 | 60 | // Make sure settings exist or create them 61 | // If invalid, will panic inside of here to pass the error to the user anyway. 62 | once_cell::sync::Lazy::force(&crate::configs::SETTINGS); 63 | 64 | logging::init()?; 65 | 66 | debug!("Starting: UI"); 67 | ui::init(); 68 | 69 | debug!("Starting: Hooks"); 70 | hooks::init()?; 71 | 72 | #[cfg(plugins)] 73 | { 74 | debug!("Starting: Plugins"); 75 | plugins::init()?; 76 | } 77 | 78 | debug!("Finished Startup!"); 79 | 80 | #[cfg(http)] 81 | version::check(); 82 | 83 | Ok(()) 84 | }); 85 | 86 | match res { 87 | Err(_why) => Err(StartError::Panic), 88 | Ok(res) => res, 89 | } 90 | } 91 | 92 | #[derive(Debug, thiserror::Error)] 93 | pub enum CleanupError { 94 | #[error("Failed to unhook functions '{0}'")] 95 | HookError(#[from] detour::Error), 96 | } 97 | 98 | pub fn cleanup() -> Result<(), CleanupError> { 99 | hooks::cleanup()?; 100 | 101 | Ok(()) 102 | } 103 | -------------------------------------------------------------------------------- /autorun/src/fs/mod.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | use fs_err as fs; 4 | 5 | pub const DUMP_DIR: &str = "lua_dumps"; 6 | pub const LOG_DIR: &str = "logs"; 7 | pub const INCLUDE_DIR: &str = "scripts"; 8 | pub const PLUGIN_DIR: &str = "plugins"; 9 | pub const BIN_DIR: &str = "bin"; 10 | 11 | pub const AUTORUN_PATH: &str = "autorun.lua"; 12 | pub const HOOK_PATH: &str = "hook.lua"; 13 | pub const SETTINGS_PATH: &str = "settings.toml"; 14 | 15 | mod path; 16 | pub use path::FSPath; 17 | 18 | pub fn in_autorun>(path: S) -> PathBuf { 19 | home::home_dir() 20 | .expect("Couldn't get your home directory!") 21 | .join("autorun") 22 | .join(path.as_ref()) 23 | } 24 | 25 | pub fn base() -> PathBuf { 26 | home::home_dir() 27 | .expect("Couldn't get your home directory!") 28 | .join("autorun") 29 | } 30 | 31 | pub fn read_to_string>(path: P) -> std::io::Result { 32 | use std::io::Read; 33 | 34 | let mut file = fs::File::open(in_autorun(path.as_ref()))?; 35 | let mut contents = String::new(); 36 | file.read_to_string(&mut contents)?; 37 | 38 | Ok(contents) 39 | } 40 | 41 | // Reads a directory at a path local to the 'autorun' directory, 42 | // And then returns results *also* truncated to be local to the 'autorun' directory 43 | pub fn traverse_dir, F: FnMut(&FSPath, fs::DirEntry)>( 44 | path: P, 45 | mut rt: F, 46 | ) -> std::io::Result<()> { 47 | let p = in_autorun(path.as_ref()); 48 | let ar_base = base(); 49 | 50 | for entry in fs::read_dir(&p)?.flatten() { 51 | let path = entry.path(); 52 | let path = path.strip_prefix(&ar_base).unwrap_or(&path); 53 | 54 | rt(&FSPath::from(path), entry); 55 | } 56 | 57 | Ok(()) 58 | } 59 | 60 | pub fn create_dir(path: &FSPath) -> std::io::Result<()> { 61 | fs::create_dir(in_autorun(path)) 62 | } 63 | 64 | pub fn create_file(path: &FSPath) -> std::io::Result { 65 | fs::File::create(in_autorun(path)) 66 | } 67 | 68 | pub fn remove_dir(path: &FSPath) -> std::io::Result<()> { 69 | fs::remove_dir_all(in_autorun(path)) 70 | } 71 | -------------------------------------------------------------------------------- /autorun/src/fs/path.rs: -------------------------------------------------------------------------------- 1 | // Class abstracting over a PathBuf 2 | 3 | use super::in_autorun; 4 | use std::{ 5 | ops::Deref, 6 | path::{Path, PathBuf}, 7 | }; 8 | 9 | #[derive(Debug)] 10 | #[repr(transparent)] 11 | pub struct FSPath(PathBuf); 12 | 13 | // Implement everything from the original PathBuf type, except they call in_autorun for every call to the filesystem. 14 | impl FSPath { 15 | pub fn from>(p: P) -> Self { 16 | Self(p.as_ref().to_path_buf()) 17 | } 18 | 19 | pub fn is_dir(&self) -> bool { 20 | in_autorun(&self.0).is_dir() 21 | } 22 | 23 | pub fn exists(&self) -> bool { 24 | in_autorun(&self.0).exists() 25 | } 26 | 27 | pub fn join(&self, path: impl AsRef) -> Self { 28 | Self(self.0.join(path)) 29 | } 30 | 31 | pub fn to_owned(&self) -> Self { 32 | Self(self.0.clone()) 33 | } 34 | 35 | pub fn parent(&self) -> Option { 36 | self.0.parent().map(|x| Self(x.to_path_buf())) 37 | } 38 | 39 | pub fn pop(&mut self) -> bool { 40 | self.0.pop() 41 | } 42 | } 43 | 44 | impl Deref for FSPath { 45 | type Target = PathBuf; 46 | 47 | fn deref(&self) -> &Self::Target { 48 | &self.0 49 | } 50 | } 51 | 52 | impl AsRef for FSPath { 53 | fn as_ref(&self) -> &Path { 54 | self.0.as_ref() 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /autorun/src/hooks/dumper.rs: -------------------------------------------------------------------------------- 1 | use fs_err as fs; 2 | use std::{ 3 | ffi::CStr, 4 | path::PathBuf, 5 | sync::{Arc, Mutex}, 6 | time::Duration, 7 | }; 8 | 9 | use crate::{configs::SETTINGS, fs as afs}; 10 | use once_cell::sync::Lazy; 11 | 12 | use super::DispatchParams; 13 | 14 | struct DumpEntry { 15 | path: PathBuf, 16 | content: String, 17 | } 18 | 19 | static DUMP_QUEUE: Lazy>>> = 20 | Lazy::new(|| Arc::new(Mutex::new(Vec::new()))); 21 | 22 | fn fix_path(str: &str) -> Option { 23 | let mut buf = String::new(); 24 | 25 | let mut dots = 0; 26 | 27 | if str.is_empty() { 28 | return Some("unknown".to_string()) 29 | } 30 | 31 | for char in str.chars() { 32 | match char { 33 | ':' | '*' | '?' | '"' | '<' | '>' | '|' => { 34 | dots = 0; 35 | buf.push('_') 36 | } 37 | 38 | '/' | '\\' => { 39 | // gmod doesn't seem to allow directory traversal like this anyway? 40 | if dots >= 2 { 41 | return None; 42 | } 43 | 44 | dots = 0; 45 | buf.push(char) 46 | } 47 | 48 | '.' => { 49 | dots += 1; 50 | buf.push(char); 51 | } 52 | 53 | _ => { 54 | dots = 0; 55 | buf.push(char) 56 | } 57 | } 58 | } 59 | 60 | Some(buf) 61 | } 62 | 63 | /// Will only be run if filesteal is enabled. 64 | pub fn dump(params: &mut DispatchParams) { 65 | if params.path.len() < 1000 { 66 | // Ignore paths that are ridiculously long 67 | if let Ok(mut queue) = DUMP_QUEUE.try_lock() { 68 | let mut fmt = SETTINGS.filesteal.format.clone(); 69 | 70 | if fmt.contains("") { 71 | let ip = unsafe { CStr::from_ptr(params.ip) }; 72 | let ip = ip.to_string_lossy(); 73 | 74 | fmt = fmt.replace("", &ip); 75 | } 76 | 77 | if fmt.contains("") { 78 | let hostname = params.net.GetName(); 79 | let hostname = unsafe { CStr::from_ptr(hostname) }; 80 | let hostname = hostname.to_string_lossy(); 81 | 82 | fmt = fmt.replace("", &hostname); 83 | } 84 | 85 | let (code, _) = params.get_code(); 86 | let code = unsafe { CStr::from_ptr(code) }; 87 | let code = code.to_string_lossy().to_string(); 88 | 89 | if let Some(fmt) = fix_path(&fmt) { 90 | if let Some(path_clean) = fix_path(params.path) { 91 | queue.push(DumpEntry { 92 | path: PathBuf::from(&fmt).join(path_clean).with_extension("lua"), 93 | content: code, 94 | }); 95 | } 96 | } 97 | } 98 | } 99 | } 100 | 101 | const QUEUE_COOLDOWN: Duration = Duration::from_millis(300); 102 | 103 | pub fn queue() { 104 | // Same deal as the lua executor. Run in a separate thread and endlessly loop pulling latest files to dump 105 | loop { 106 | std::thread::sleep(QUEUE_COOLDOWN); 107 | 108 | if let Ok(mut queue) = DUMP_QUEUE.try_lock() { 109 | if !queue.is_empty() { 110 | // Handle 15 files at a time max 111 | // 15 files every 300 ms is around 50 files per sec, not bad 112 | let len = 15.min(queue.len()); 113 | let dump_dir = &*afs::in_autorun(afs::DUMP_DIR); 114 | for entry in queue.drain(..len) { 115 | let path = dump_dir.join(entry.path); 116 | let content = entry.content; 117 | 118 | let p = path.parent().unwrap_or(&path); 119 | if !p.exists() { 120 | if let Err(why) = fs::create_dir_all(p) { 121 | debug!("Failed to create directory {}: {}", p.display(), why); 122 | } 123 | } 124 | 125 | if let Err(why) = fs::write(&path, content) { 126 | error!("Failed to write to {}: {}", path.display(), why); 127 | } 128 | } 129 | } 130 | } else { 131 | debug!("Failed to lock dump queue"); 132 | } 133 | } 134 | } 135 | 136 | /// Create async queue to dump files 137 | pub fn start_queue() { 138 | std::thread::spawn(queue); 139 | } 140 | -------------------------------------------------------------------------------- /autorun/src/hooks/lazy.rs: -------------------------------------------------------------------------------- 1 | macro_rules! lazy_detour { 2 | // Lazy Path 3 | ( $(#[$m:meta])* $vis:vis static $name:ident : $t:ty = ($target:expr, $tour:expr) ; $($rest:tt)* ) => { 4 | $(#[$m])* 5 | $vis static $name: once_cell::sync::Lazy< detour::GenericDetour< $t > > = once_cell::sync::Lazy::new(|| unsafe { 6 | match detour::GenericDetour::new( $target, $tour ) { 7 | Ok(b) => { 8 | b.enable().expect( concat!("Failed to enable detour '", stringify!($name), "'") ); 9 | b 10 | }, 11 | Err(why) => panic!( concat!("Failed to create hook '", stringify!($name), "' {}"), why) 12 | } 13 | }); 14 | lazy_detour!( $($rest)* ); 15 | }; 16 | () => (); 17 | } 18 | pub(crate) use lazy_detour; 19 | -------------------------------------------------------------------------------- /autorun/src/hooks/mod.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::CStr; 2 | use std::sync::{ 3 | atomic::{AtomicU64, Ordering}, 4 | MutexGuard, 5 | }; 6 | 7 | use crate::{configs::SETTINGS, logging, lua}; 8 | 9 | use logging::*; 10 | use rglua::interface; 11 | use rglua::prelude::*; 12 | 13 | mod dumper; 14 | pub mod lazy; 15 | mod scripthook; 16 | use lazy::lazy_detour; 17 | 18 | // Make our own static detours because detours.rs is lame and locked theirs behind nightly. :) 19 | lazy_detour! { 20 | pub static LUAL_LOADBUFFERX_H: extern "C" fn(LuaState, *const i8, SizeT, *const i8, *const i8) -> i32 = ( 21 | { 22 | *LUA_SHARED_RAW.get:: i32>(b"luaL_loadbufferx") 23 | .expect("Failed to get luaL_loadbufferx") 24 | }, 25 | loadbufferx_h 26 | ); 27 | 28 | #[cfg(executor)] 29 | pub static PAINT_TRAVERSE_H: PaintTraverseFn = ( 30 | { 31 | let vgui = iface!(Panel).expect("Failed to get Panel interface"); 32 | std::mem::transmute::<_, PaintTraverseFn>( 33 | (vgui.vtable as *mut *mut c_void) 34 | .offset(41) 35 | .read(), 36 | ) 37 | }, 38 | paint_traverse_h 39 | ); 40 | } 41 | 42 | #[cfg(executor)] 43 | use rglua::interface::Panel; 44 | 45 | #[cfg(executor)] 46 | type PaintTraverseFn = extern "fastcall" fn(&'static Panel, usize, bool, bool); 47 | 48 | static CONNECTED: AtomicU64 = AtomicU64::new(99999); 49 | 50 | pub struct DispatchParams<'a> { 51 | ip: LuaString, 52 | 53 | code: LuaString, 54 | code_len: usize, 55 | 56 | identifier: LuaString, 57 | 58 | startup: bool, 59 | path: &'a str, 60 | #[allow(unused)] 61 | engine: &'a mut interface::EngineClient, 62 | net: &'a mut interface::NetChannelInfo, 63 | } 64 | 65 | impl<'a> DispatchParams<'a> { 66 | pub fn set_code(&mut self, code: LuaString, code_len: usize) { 67 | self.code = code; 68 | self.code_len = code_len; 69 | } 70 | 71 | pub fn get_code(&self) -> (LuaString, usize) { 72 | (self.code, self.code_len) 73 | } 74 | } 75 | 76 | extern "C" fn loadbufferx_h( 77 | l: LuaState, 78 | mut code: LuaString, 79 | mut code_len: SizeT, 80 | identifier: LuaString, 81 | mode: LuaString, 82 | ) -> i32 { 83 | if let Ok(engine) = iface!(EngineClient) { 84 | let do_run; 85 | if engine.IsConnected() { 86 | let net = engine.GetNetChannelInfo(); 87 | 88 | if let Some(net) = unsafe { net.as_mut() } { 89 | let ip = net.GetAddress(); 90 | let mut startup = false; 91 | 92 | // TODO: It'd be great to hook net connections instead of doing this. 93 | // However, this works fine for now. 94 | let curtime = net.GetTimeConnected() as u64; 95 | if curtime < CONNECTED.load(Ordering::Relaxed) { 96 | debug!("Curtime is less than last time connected, assuming startup"); 97 | startup = true; 98 | 99 | if let Err(why) = close_dylibs() { 100 | debug!("Failed to close dynamic libs: {why}"); 101 | } 102 | } 103 | 104 | // Awful 105 | CONNECTED.store(curtime, Ordering::Relaxed); 106 | 107 | let path = unsafe { CStr::from_ptr(identifier) }; 108 | let path = &path.to_string_lossy()[1..]; // Remove the @ from the beginning of the path 109 | 110 | // There's way too many params here 111 | let mut params = DispatchParams { 112 | ip, 113 | 114 | code, 115 | code_len, 116 | 117 | identifier, 118 | startup, 119 | path, 120 | 121 | engine, 122 | net, 123 | }; 124 | 125 | do_run = dispatch(l, &mut params); 126 | if do_run { 127 | (code, code_len) = params.get_code(); 128 | } else { 129 | return 0; 130 | } 131 | } 132 | } 133 | } 134 | 135 | unsafe { LUAL_LOADBUFFERX_H.call(l, code, code_len, identifier, mode) } 136 | } 137 | 138 | pub fn dispatch(l: LuaState, params: &mut DispatchParams) -> bool { 139 | let mut do_run = true; 140 | 141 | scripthook::execute(l, params, &mut do_run); 142 | if SETTINGS.filesteal.enabled { 143 | dumper::dump(params); 144 | } 145 | 146 | do_run 147 | } 148 | 149 | #[cfg(executor)] 150 | extern "fastcall" fn paint_traverse_h( 151 | this: &'static Panel, 152 | panel_id: usize, 153 | force_repaint: bool, 154 | force_allow: bool, 155 | ) { 156 | unsafe { 157 | PAINT_TRAVERSE_H.call(this, panel_id, force_repaint, force_allow); 158 | } 159 | 160 | if let Ok(ref mut queue) = lua::SCRIPT_QUEUE.try_lock() { 161 | if !queue.is_empty() { 162 | let (realm, script) = queue.remove(0); 163 | 164 | match lua::get_state(realm) { 165 | Ok(state) => match lua::dostring(state, &script) { 166 | Err(why) => error!("{why}"), 167 | Ok(_) => info!("Script of len #{} ran successfully.", script.len()), 168 | }, 169 | Err(why) => error!("{why}"), 170 | } 171 | } 172 | } 173 | } 174 | 175 | #[derive(Debug, thiserror::Error)] 176 | enum CloseLibs { 177 | #[error("Failed to acquire mutex (Report this on github)")] 178 | Mutex(#[from] std::sync::PoisonError>>), 179 | } 180 | 181 | /// Closes all previously loaded dylibs from Autorun.requirebin 182 | fn close_dylibs() -> Result<(), CloseLibs> { 183 | let mut libs = lua::LOADED_LIBS.lock()?; 184 | 185 | for lib in libs.drain(..) { 186 | let _ = lib.close(); 187 | } 188 | 189 | Ok(()) 190 | } 191 | 192 | #[derive(Debug, thiserror::Error)] 193 | pub enum HookingError { 194 | #[error("Failed to hook function: {0}")] 195 | Detour(#[from] detour::Error), 196 | 197 | #[error("Failed to get interface")] 198 | Interface(#[from] rglua::interface::Error), 199 | } 200 | 201 | pub fn init() -> Result<(), HookingError> { 202 | use once_cell::sync::Lazy; 203 | 204 | Lazy::force(&LUAL_LOADBUFFERX_H); 205 | 206 | #[cfg(executor)] 207 | Lazy::force(&PAINT_TRAVERSE_H); 208 | 209 | dumper::start_queue(); 210 | 211 | Ok(()) 212 | } 213 | 214 | pub fn cleanup() -> Result<(), detour::Error> { 215 | unsafe { 216 | LUAL_LOADBUFFERX_H.disable()?; 217 | 218 | #[cfg(executor)] 219 | PAINT_TRAVERSE_H.disable()?; 220 | } 221 | 222 | if let Err(why) = close_dylibs() { 223 | debug!("Failed to close dynamic libs: {why}"); 224 | } 225 | 226 | Ok(()) 227 | } 228 | -------------------------------------------------------------------------------- /autorun/src/hooks/scripthook.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | configs::SETTINGS, 3 | fs::{self as afs, AUTORUN_PATH, HOOK_PATH}, 4 | lua::{self, AutorunEnv}, 5 | }; 6 | 7 | #[cfg(plugins)] 8 | use crate::plugins; 9 | 10 | use fs_err as fs; 11 | use rglua::prelude::*; 12 | 13 | use super::DispatchParams; 14 | 15 | pub fn execute(l: LuaState, params: &mut DispatchParams, do_run: &mut bool) { 16 | if params.startup { 17 | // autorun.lua 18 | let env = AutorunEnv { 19 | is_autorun_file: true, 20 | startup: params.startup, 21 | 22 | identifier: params.identifier, 23 | code: params.code, 24 | code_len: params.code_len, 25 | 26 | ip: params.ip, 27 | 28 | #[cfg(plugins)] 29 | plugin: None, 30 | }; 31 | 32 | #[cfg(plugins)] 33 | if let Err(why) = plugins::call_autorun(l, &env) { 34 | error!("Failed to call plugins (autorun): {why}"); 35 | } 36 | 37 | // This will only run once when HAS_AUTORAN is false, setting it to true. 38 | // Will be reset by JoinServer. 39 | let full_path = afs::in_autorun(AUTORUN_PATH); 40 | if let Ok(script) = fs::read_to_string(&full_path) { 41 | if let Err(why) = lua::run_env(l, script, AUTORUN_PATH, &env) { 42 | error!("{why}"); 43 | } 44 | } else { 45 | debug!( 46 | "Couldn't read your autorun script file at [{}]", 47 | full_path.display() 48 | ); 49 | } 50 | } 51 | 52 | { 53 | // hook.lua 54 | let env = AutorunEnv { 55 | is_autorun_file: false, 56 | startup: params.startup, 57 | 58 | identifier: params.identifier, 59 | 60 | code: params.code, 61 | code_len: params.code_len, 62 | 63 | ip: params.ip, 64 | 65 | #[cfg(plugins)] 66 | plugin: None, 67 | }; 68 | 69 | #[cfg(plugins)] 70 | if SETTINGS.plugins.enabled { 71 | match plugins::call_hook(l, &env, do_run) { 72 | Err(why) => { 73 | error!("Failed to call plugins (hook): {why}"); 74 | } 75 | Ok(Some((code, len))) => { 76 | params.set_code(code, len); 77 | } 78 | Ok(_) => (), 79 | } 80 | } 81 | 82 | if let Ok(script) = afs::read_to_string(HOOK_PATH) { 83 | match lua::run_env(l, script, HOOK_PATH, &env) { 84 | Ok(top) => { 85 | // If you return ``true`` in your hook.lua file, then don't run the Autorun.CODE that is about to run. 86 | match lua_type(l, top + 1) { 87 | rglua::lua::TBOOLEAN => { 88 | if lua_toboolean(l, top + 1) != 0 { 89 | *do_run = false; 90 | } 91 | } 92 | rglua::lua::TSTRING => { 93 | // lua_tolstring sets len to new length automatically. 94 | let mut len: usize = 0; 95 | let newcode = lua_tolstring(l, top + 1, &mut len); 96 | params.set_code(newcode, len); 97 | } 98 | _ => (), 99 | } 100 | lua_settop(l, top); 101 | } 102 | Err(_why) => (), 103 | } 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /autorun/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Rust-Analyzer has false positives with detours-rs 2 | // https://github.com/rust-analyzer/rust-analyzer/issues/9576 3 | #![allow(unused_unsafe)] 4 | use rglua::prelude::*; 5 | 6 | mod configs; 7 | mod fs; 8 | 9 | #[macro_use] 10 | mod logging; 11 | 12 | mod cross; 13 | mod hooks; 14 | mod lua; 15 | 16 | #[cfg(plugins)] 17 | mod plugins; 18 | mod ui; 19 | 20 | #[cfg(http)] 21 | mod version; 22 | 23 | use logging::error; 24 | 25 | #[no_mangle] 26 | #[cfg(inject)] 27 | extern "system" fn DllMain(_: *const u8, reason: u32, _: *const u8) -> u32 { 28 | use winapi::um::winnt::{DLL_PROCESS_ATTACH, DLL_PROCESS_DETACH}; 29 | 30 | match reason { 31 | DLL_PROCESS_ATTACH => { 32 | if let Err(why) = cross::startup() { 33 | error!("Failed to start: {why}"); 34 | } 35 | } 36 | DLL_PROCESS_DETACH => { 37 | if let Err(why) = cross::cleanup() { 38 | error!("Failed to cleanup: {why}"); 39 | } 40 | } 41 | _ => (), 42 | } 43 | 44 | 1 45 | } 46 | 47 | #[gmod_open] 48 | #[allow(unused)] 49 | pub fn main(l: LuaState) -> i32 { 50 | // DllMain is called prior to this even if Autorun is used as a binary module. 51 | // So only initialize what we haven't already. 52 | #[cfg(not(feature = "inject"))] 53 | if let Err(why) = cross::startup() { 54 | printgm!(l, "Failed to start Autorun: `{why}`"); 55 | error!("Failed to start Autorun: `{why}`"); 56 | } 57 | 58 | 0 59 | } 60 | 61 | #[gmod_close] 62 | pub fn close(_l: LuaState) -> i32 { 63 | #[cfg(not(feature = "inject"))] 64 | if let Err(why) = cross::cleanup() { 65 | error!("Failed to cleanup at gmod13_close: {why}"); 66 | } 67 | 0 68 | } 69 | -------------------------------------------------------------------------------- /autorun/src/logging/mod.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use fs_err as fs; 4 | use once_cell::sync::Lazy; 5 | use thiserror::Error; 6 | 7 | use crate::fs::{self as afs, LOG_DIR}; 8 | 9 | #[derive(Error, Debug)] 10 | pub enum LogInitError { 11 | #[error("Failed to create log file: {0}")] 12 | File(#[from] std::io::Error), 13 | } 14 | 15 | pub static LOG_PATH: Lazy = Lazy::new(|| { 16 | let t = time::OffsetDateTime::now_utc(); 17 | 18 | // YYYY-MM-DD 19 | let filename = format!("{:04}-{:02}-{:02}.log", t.year(), t.month() as u8, t.day()); 20 | afs::in_autorun(LOG_DIR).join(filename) 21 | }); 22 | 23 | pub fn init() -> Result<(), LogInitError> { 24 | let handle = fs::OpenOptions::new() 25 | .create(true) 26 | .append(true) 27 | .open(&*LOG_PATH); 28 | 29 | if let Ok(mut handle) = handle { 30 | use std::io::Write; 31 | if let Err(why) = writeln!( 32 | handle, 33 | "[INFO]: Logging started at {} in version {}\n", 34 | time::OffsetDateTime::now_utc(), 35 | env!("CARGO_PKG_VERSION") 36 | ) { 37 | eprintln!("Failed to write initial log message {why}"); 38 | } 39 | } 40 | 41 | Ok(()) 42 | } 43 | 44 | macro_rules! log { 45 | ($severity:literal, $msg:expr) => { 46 | let handle = fs_err::OpenOptions::new() 47 | .create(true) 48 | .append(true) 49 | .open(&*$crate::logging::LOG_PATH); 50 | 51 | match handle { 52 | Ok(mut handle) => { 53 | use std::io::Write; 54 | let _ = writeln!(handle, concat!("[", $severity, "]: {}"), $msg); 55 | } 56 | Err(why) => { 57 | eprintln!("Failed to open log file: {why}"); 58 | } 59 | } 60 | }; 61 | } 62 | 63 | pub(crate) use log; 64 | 65 | macro_rules! warning { 66 | ($($arg:tt)+) => { 67 | if $crate::configs::SETTINGS.logging.enabled { 68 | $crate::ui::printwarning!( normal, $($arg)+ ); 69 | $crate::logging::log!( "WARN", format!( $($arg)+ ) ); 70 | } 71 | }; 72 | } 73 | 74 | pub(crate) use warning; 75 | 76 | macro_rules! trace { 77 | ( $($arg:tt)+ ) => { 78 | () 79 | }; 80 | } 81 | 82 | pub(crate) use trace; 83 | 84 | macro_rules! info { 85 | ( $($arg:tt)+ ) => { 86 | if $crate::configs::SETTINGS.logging.enabled { 87 | $crate::ui::printinfo!( normal, $($arg)+ ); 88 | $crate::logging::log!( "INFO", format!( $($arg)+ ) ); 89 | } 90 | }; 91 | } 92 | pub(crate) use info; 93 | 94 | // Print to stderr 95 | macro_rules! error { 96 | ( $($arg:tt)+ ) => { 97 | if $crate::configs::SETTINGS.logging.enabled { 98 | $crate::ui::printerror!( normal, $($arg)+ ); 99 | $crate::logging::log!( "ERROR", format!( $($arg)+ ) ); 100 | } 101 | }; 102 | } 103 | pub(crate) use error; 104 | 105 | // Only prints when in a debug build. 106 | #[cfg(debug_assertions)] 107 | macro_rules! debug { 108 | ( $($arg:tt)+ ) => { 109 | if $crate::configs::SETTINGS.logging.enabled { 110 | $crate::ui::printdebug!( normal, $($arg)+ ); 111 | $crate::logging::log!( "DEBUG", format!( $($arg)+ ) ); 112 | } 113 | }; 114 | } 115 | 116 | // We are in a release build, don't print anything. 117 | #[cfg(not(debug_assertions))] 118 | macro_rules! debug { 119 | ( $($arg:tt)+ ) => { 120 | { 121 | // Stupid hack to get rust to shut up about not using what's passed to these macros 122 | // Since debug! is only disabled on release builds. 123 | // Hopefully this doesn't affect compile time since it uses _ :/ 124 | let _ = format!( $($arg)+ ); 125 | } 126 | }; 127 | } 128 | 129 | pub(crate) use debug; 130 | -------------------------------------------------------------------------------- /autorun/src/lua/env.rs: -------------------------------------------------------------------------------- 1 | use colored::Colorize; 2 | use once_cell::sync::Lazy; 3 | use rglua::prelude::*; 4 | use std::{ 5 | ffi::CStr, 6 | mem::MaybeUninit, 7 | path::PathBuf, 8 | sync::{Arc, Mutex}, 9 | }; 10 | 11 | use crate::{ 12 | fs::{self as afs, FSPath, BIN_DIR, INCLUDE_DIR}, 13 | logging::*, 14 | lua::{self, err}, 15 | }; 16 | 17 | #[lua_function] 18 | pub fn log(l: LuaState) -> i32 { 19 | let s = luaL_checkstring(l, 1); 20 | let level = luaL_optinteger(l, 2, 3); // INFO by default 21 | 22 | let msg = unsafe { CStr::from_ptr(s).to_string_lossy() }; 23 | match level { 24 | 1 => error!("{msg}"), 25 | 2 => warning!("{msg}"), 26 | 3 => info!("{msg}"), 27 | 4 => debug!("{msg}"), 28 | 5 => trace!("{msg}"), 29 | _ => luaL_argerror(l, 2, err::INVALID_LOG_LEVEL), 30 | } 31 | 32 | 0 33 | } 34 | 35 | #[lua_function] 36 | // Works like MsgC in lua (except also adds a newline.) 37 | pub fn print(l: LuaState) -> i32 { 38 | let nargs = lua_gettop(l); 39 | 40 | // Buffer for the whole message to be printed. 41 | let mut total_buf = String::new(); 42 | 43 | // Buffer that is re-used for every color found 44 | let mut buf = String::new(); 45 | 46 | let mut color: Option<(u8, u8, u8)> = None; 47 | for i in 1..=nargs { 48 | match lua_type(l, i) { 49 | lua::TTABLE => { 50 | lua_rawgeti(l, i, 1); 51 | if lua_isnumber(l, -1) == 0 { 52 | // Not a color 53 | let s = lua_tostring(l, i); 54 | let s = unsafe { CStr::from_ptr(s).to_string_lossy() }; 55 | buf.push_str(&s); 56 | 57 | lua_pop(l, 1); 58 | continue; 59 | } 60 | 61 | let r = luaL_optinteger(l, -1, 255) as u8; 62 | 63 | lua_rawgeti(l, i, 2); 64 | let g = luaL_optinteger(l, -1, 255) as u8; 65 | 66 | lua_rawgeti(l, i, 3); 67 | let b = luaL_optinteger(l, -1, 255) as u8; 68 | 69 | if let Some(col) = color { 70 | // Take all previous text 71 | let str = buf.truecolor(col.0, col.1, col.2); 72 | buf = String::new(); 73 | total_buf.push_str(&format!("{str}")); 74 | } 75 | 76 | color = Some((r, g, b)); 77 | } 78 | lua::TFUNCTION | lua::TUSERDATA | lua::TLIGHTUSERDATA | lua::TTHREAD => { 79 | let s = lua_topointer(l, i); 80 | buf.push_str(&format!("{:p}", s)); 81 | } 82 | _ => { 83 | let s = lua_tostring(l, i); 84 | let s = unsafe { CStr::from_ptr(s).to_string_lossy() }; 85 | buf.push_str(&s); 86 | } 87 | } 88 | } 89 | 90 | if let Some(col) = color { 91 | let str = buf.truecolor(col.0, col.1, col.2); 92 | total_buf.push_str(&format!("{str}")); 93 | } 94 | 95 | println!("{total_buf}"); 96 | 97 | 0 98 | } 99 | 100 | #[derive(Debug, thiserror::Error)] 101 | enum RequireError { 102 | #[error("Failed to require file: {0}")] 103 | IO(#[from] std::io::Error), 104 | 105 | #[error("Failed to load dynamic library: {0}")] 106 | Libloading(#[from] libloading::Error), 107 | 108 | #[error("Failed to find gmod13_open or autorun_open symbols in library")] 109 | SymbolNotFound, 110 | 111 | #[error("File does not exist: {0}")] 112 | DoesNotExist(String), 113 | } 114 | 115 | // Gets function at the stack level given (assuming there is one there..) 116 | fn get_func(l: LuaState, level: u32) { 117 | let mut ar = MaybeUninit::uninit(); 118 | 119 | if lua_getstack(l, level as i32, ar.as_mut_ptr()) == 0 { 120 | luaL_argerror(l, 1, cstr!("invalid level")); 121 | } 122 | 123 | lua_getinfo(l, cstr!("f"), ar.as_mut_ptr()); 124 | 125 | if lua_isnil(l, -1) { 126 | luaL_error( 127 | l, 128 | cstr!("no function environment for tail call at level %d"), 129 | level, 130 | ); 131 | } 132 | } 133 | 134 | // Pushes the fenv onto the stack 135 | fn push_fenv(l: LuaState) -> bool { 136 | get_func(l, 1); 137 | 138 | if lua_iscfunction(l, -1) == 0 { 139 | lua_getfenv(l, -1); 140 | 141 | return true; 142 | } 143 | lua_pop(l, 1); // pop func 144 | 145 | false 146 | } 147 | 148 | fn get_current_path(l: LuaState) -> Option { 149 | if push_fenv(l) { 150 | lua_getfield(l, -1, cstr!("Autorun")); 151 | 152 | if lua_istable(l, -1) { 153 | lua_getfield(l, -1, cstr!("PATH")); 154 | if lua_isstring(l, -1) == 1 { 155 | let file_path = lua_tostring(l, -1); 156 | let file_path = unsafe { CStr::from_ptr(file_path) }; 157 | let file_path = file_path.to_string_lossy(); 158 | 159 | lua_pop(l, 3); // pop PATH, Autorun and fenv 160 | return Some(FSPath::from(file_path.to_string())); 161 | } else { 162 | luaL_error(l, cstr!("Bad call: Autorun.PATH is not a string")); 163 | } 164 | } else { 165 | luaL_error(l, cstr!("Bad call: Autorun table not found")); 166 | } 167 | } 168 | 169 | None 170 | } 171 | 172 | fn get_relative>(l: LuaState, path: P) -> Option { 173 | let p = path.as_ref(); 174 | 175 | let current = get_current_path(l)?; 176 | Some(current.parent().unwrap_or(current).join(p)) 177 | } 178 | 179 | // https://github.com/lua/lua/blob/eadd8c7178c79c814ecca9652973a9b9dd4cc71b/loadlib.c#L657 180 | #[lua_function] 181 | pub fn require(l: LuaState) -> Result { 182 | use rglua::prelude::*; 183 | 184 | let raw_path = luaL_checkstring(l, 1); 185 | let path_name = unsafe { CStr::from_ptr(raw_path) }; 186 | let path_name = path_name.to_string_lossy(); 187 | 188 | let mut path = PathBuf::from(path_name.as_ref()); 189 | 190 | if path.file_name().is_none() { 191 | luaL_error(l, cstr!("Malformed require path: '%s'"), raw_path); 192 | } 193 | 194 | // Make sure extension is always .lua or omitted 195 | match path.extension() { 196 | Some(ext) if ext == "lua" => (), 197 | Some(_) => { 198 | luaL_error( 199 | l, 200 | cstr!("Malformed require path: '%s' (needs .lua file extension)"), 201 | raw_path, 202 | ); 203 | } 204 | None => { 205 | path.set_extension("lua"); 206 | } 207 | } 208 | 209 | let path = get_relative(l, &path).unwrap_or_else(|| FSPath::from(INCLUDE_DIR).join(&path)); 210 | 211 | if !path.exists() { 212 | luaL_error( 213 | l, 214 | cstr!("File does not exist in autorun/scripts or relative: '%s'"), 215 | raw_path, 216 | ); 217 | } 218 | 219 | let script = afs::read_to_string(path)?; 220 | let top = lua_gettop(l); 221 | 222 | if let Err(why) = lua::compile(l, &script) { 223 | let err = format!("Compile error when requiring file {path_name}: {why}\0"); 224 | let err_c = err.as_bytes(); 225 | 226 | luaL_error(l, err_c.as_ptr().cast()); 227 | } 228 | 229 | get_func(l, 1); 230 | if lua_iscfunction(l, -1) == 0 { 231 | lua_getfenv(l, -1); 232 | lua_remove(l, -2); 233 | 234 | lua_setfenv(l, -2); 235 | } 236 | 237 | if let Err(why) = lua::pcall(l) { 238 | let err = format!("Error when requiring file {path_name}: {why}\0"); 239 | let err_c = err.as_bytes(); 240 | 241 | luaL_error(l, err_c.as_ptr().cast()); 242 | } 243 | 244 | Ok(lua_gettop(l) - top) 245 | } 246 | 247 | pub static LOADED_LIBS: Lazy>>> = 248 | Lazy::new(|| Arc::new(Mutex::new(vec![]))); 249 | 250 | #[lua_function] 251 | /// Example usage: require("vistrace") (No extensions or anything.) 252 | pub fn requirebin(l: LuaState) -> Result { 253 | use std::env::consts::{DLL_EXTENSION, DLL_PREFIX}; 254 | 255 | let dlname = luaL_checkstring(l, 1); 256 | let dlname = unsafe { CStr::from_ptr(dlname) }; 257 | let dlname = dlname.to_string_lossy(); 258 | 259 | let binpath = afs::in_autorun(BIN_DIR); 260 | let mut path = binpath 261 | .join(DLL_PREFIX) 262 | .join(dlname.as_ref()) 263 | .with_extension(DLL_EXTENSION); 264 | 265 | if !path.exists() { 266 | let os_prefix = if cfg!(windows) { 267 | "win" 268 | } else if cfg!(target_os = "macos") { 269 | "osx" 270 | } else { 271 | "linux" 272 | }; 273 | 274 | let arch = if cfg!(target_pointer_width = "32") { 275 | "32" 276 | } else { 277 | "64" 278 | }; 279 | 280 | let altpath = binpath.join(format!("gmcl_{dlname}_{os_prefix}{arch}.{DLL_EXTENSION}")); 281 | 282 | if altpath.exists() { 283 | path = altpath; 284 | } else { 285 | let altpath = binpath.join(format!("gmsv_{dlname}_{os_prefix}{arch}.{DLL_EXTENSION}")); 286 | if altpath.exists() { 287 | path = altpath; 288 | } else { 289 | return Err(RequireError::DoesNotExist(path.display().to_string())); 290 | } 291 | } 292 | } 293 | 294 | let lib = unsafe { libloading::Library::new(path)? }; 295 | 296 | // Api may be changed. 297 | type AutorunEntry = extern "C" fn(l: LuaState) -> c_int; 298 | type Gmod13Entry = extern "C" fn(l: LuaState) -> c_int; 299 | type LuaEntry = extern "C" fn(l: LuaState) -> c_int; 300 | 301 | let n_symbols; 302 | if let Ok(autorun_sym) = unsafe { lib.get::(b"autorun_open\0") } { 303 | n_symbols = autorun_sym(l); 304 | } else if let Ok(gmod13_sym) = unsafe { lib.get::(b"gmod13_open\0") } { 305 | n_symbols = gmod13_sym(l); 306 | } else if let Ok(lua_sym) = unsafe { lib.get::(b"lua_open\0") } { 307 | n_symbols = lua_sym(l); 308 | } else { 309 | return Err(RequireError::SymbolNotFound); 310 | } 311 | 312 | if let Ok(mut libs) = LOADED_LIBS.try_lock() { 313 | libs.push(lib); 314 | } 315 | 316 | Ok(n_symbols) 317 | } 318 | 319 | #[derive(Debug, thiserror::Error)] 320 | enum ReadError { 321 | #[error("Cannot be called inside a C function")] 322 | CFunction, 323 | 324 | #[error("Failed to read file: {0}")] 325 | IO(#[from] std::io::Error), 326 | } 327 | 328 | #[lua_function] 329 | pub fn read(l: LuaState) -> Result { 330 | let path_name_raw = luaL_checkstring(l, 1); 331 | let path_name = unsafe { CStr::from_ptr(path_name_raw) }; 332 | let path = path_name.to_string_lossy(); 333 | let path = std::path::Path::new(path.as_ref()); 334 | 335 | if path.extension().is_none() { 336 | luaL_error( 337 | l, 338 | cstr!("Malformed file name: %s (Missing extension)\0"), 339 | path_name_raw, 340 | ); 341 | } 342 | 343 | if push_fenv(l) { 344 | lua_getfield(l, -1, cstr!("Autorun")); 345 | if lua_istable(l, -1) { 346 | lua_getfield(l, -1, cstr!("PATH")); 347 | 348 | let current_path = luaL_checkstring(l, -1); 349 | let current_path = unsafe { CStr::from_ptr(current_path) }; 350 | let current_path = current_path.to_string_lossy(); 351 | let mut current_path = FSPath::from(current_path.to_string()); 352 | current_path.pop(); // Pop to current directory instead of file 353 | 354 | lua_pop(l, 1); 355 | 356 | // First try to retrieve local to current file. 357 | let mut total_path = current_path.join(path); 358 | if !total_path.exists() { 359 | lua_getfield(l, -1, cstr!("Plugin")); 360 | 361 | if lua_istable(l, -1) { 362 | // It's a plugin 363 | lua_getfield(l, -1, cstr!("DIR")); 364 | let plugin_dir = luaL_checkstring(l, -1); 365 | let plugin_dir = unsafe { CStr::from_ptr(plugin_dir) }; 366 | let plugin_dir = plugin_dir.to_string_lossy(); 367 | 368 | let data_path = afs::FSPath::from(afs::PLUGIN_DIR) 369 | .join(plugin_dir.to_string()) 370 | .join("data") 371 | .join(path); 372 | 373 | if data_path.exists() { 374 | total_path = data_path; 375 | } else { 376 | luaL_error(l, cstr!("File not found: %s"), path_name_raw); 377 | } 378 | } else { 379 | lua_pop(l, 1); 380 | } 381 | } 382 | 383 | let contents = afs::read_to_string(total_path)?; 384 | let contents_bytes = contents.as_bytes(); 385 | lua_pushlstring(l, contents_bytes.as_ptr() as *mut _, contents.len()); 386 | Ok(1) 387 | } else { 388 | luaL_error(l, cstr!("Bad call: Autorun table not found")); 389 | } 390 | } else { 391 | Err(ReadError::CFunction) 392 | } 393 | } 394 | -------------------------------------------------------------------------------- /autorun/src/lua/err.rs: -------------------------------------------------------------------------------- 1 | use rglua::cstr; 2 | 3 | // Error Messages 4 | pub const INVALID_LOG_LEVEL: *const i8 = 5 | cstr!("Invalid log level (Should be 1-5, 1 being Error, 5 being Trace)"); 6 | -------------------------------------------------------------------------------- /autorun/src/lua/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, path::Path}; 2 | 3 | use crate::{hooks, logging::*, lua}; 4 | 5 | use autorun_shared::Realm; 6 | use rglua::prelude::*; 7 | 8 | mod env; 9 | mod err; 10 | 11 | #[cfg(executor)] 12 | use once_cell::sync::Lazy; 13 | #[cfg(executor)] 14 | use std::sync::{Arc, Mutex}; 15 | #[cfg(executor)] 16 | type LuaScript = Vec<(autorun_shared::Realm, String)>; 17 | 18 | // Scripts waiting to be ran in painttraverse 19 | #[cfg(executor)] 20 | pub static SCRIPT_QUEUE: Lazy>> = 21 | Lazy::new(|| Arc::new(Mutex::new(Vec::new()))); 22 | 23 | pub use env::LOADED_LIBS; 24 | 25 | pub struct AutorunEnv { 26 | // Whether this is the autorun.lua file 27 | pub is_autorun_file: bool, 28 | 29 | // Whether this is running before autorun 30 | pub startup: bool, 31 | 32 | pub ip: LuaString, 33 | 34 | // Name/Path of the file being run 35 | pub identifier: LuaString, 36 | 37 | pub code: LuaString, 38 | pub code_len: usize, 39 | 40 | #[cfg(plugins)] 41 | pub plugin: Option, 42 | } 43 | 44 | // Functions to interact with lua without triggering the detours 45 | pub fn compile>(l: LuaState, code: S) -> Result<(), Cow<'static, str>> { 46 | let s = code.as_ref(); 47 | unsafe { 48 | if hooks::LUAL_LOADBUFFERX_H.call( 49 | l, 50 | s.as_ptr().cast(), 51 | s.len(), 52 | cstr!("@RunString"), 53 | cstr!("bt"), 54 | ) != OK 55 | { 56 | return Err(get_lua_error(l)); 57 | } 58 | } 59 | 60 | Ok(()) 61 | } 62 | 63 | // Helpers 64 | 65 | pub unsafe fn get_lua_error(l: LuaState) -> Cow<'static, str> { 66 | let err = lua_tostring(l, -1); 67 | lua_pop(l, 1); 68 | 69 | let err = std::ffi::CStr::from_ptr(err); 70 | err.to_string_lossy() 71 | } 72 | 73 | pub fn dostring>(l: LuaState, script: S) -> Result<(), Cow<'static, str>> { 74 | compile(l, script)?; 75 | pcall(l)?; 76 | Ok(()) 77 | } 78 | 79 | pub fn pcall(l: LuaState) -> Result<(), Cow<'static, str>> { 80 | if lua_pcall(l, 0, lua::MULTRET, 0) != OK { 81 | unsafe { 82 | return Err(get_lua_error(l)); 83 | } 84 | } 85 | Ok(()) 86 | } 87 | 88 | #[inline(always)] 89 | pub fn get_state(realm: Realm) -> Result { 90 | let engine = iface!(LuaShared)?; 91 | 92 | let iface = unsafe { engine.GetLuaInterface(realm.into()).as_mut() } 93 | .ok_or(rglua::interface::Error::AsMut)?; 94 | 95 | Ok(iface.base.cast()) 96 | } 97 | 98 | #[derive(Debug, thiserror::Error)] 99 | pub enum LuaEnvError { 100 | #[error("Failed to compile lua code '{0}'")] 101 | Compile(String), 102 | 103 | #[error("Error during lua runtime '{0}'")] 104 | Runtime(String), 105 | } 106 | 107 | #[derive(Debug, thiserror::Error)] 108 | pub enum RunError { 109 | #[error("Failed to get LUASHARED003 interface")] 110 | NoInterface(#[from] rglua::interface::Error), 111 | 112 | #[cfg(executor)] 113 | #[error("Failed to get lua interface")] 114 | NoLuaInterface, 115 | } 116 | 117 | #[cfg(executor)] 118 | pub fn run(realm: Realm, code: String) -> Result<(), RunError> { 119 | // Check if lua state is valid for instant feedback 120 | let lua = iface!(LuaShared)?; 121 | let cl = lua.GetLuaInterface(realm.into()); 122 | if cl.is_null() { 123 | return Err(RunError::NoLuaInterface); 124 | } else { 125 | debug!("Got {realm} interface for run"); 126 | } 127 | 128 | match &mut SCRIPT_QUEUE.try_lock() { 129 | Ok(script_queue) => script_queue.push((realm, code)), 130 | Err(why) => error!("Failed to lock script queue mutex. {why}"), 131 | }; 132 | 133 | Ok(()) 134 | } 135 | 136 | /// Runs a lua script after running the provided preparation closure (to add variables to the env, etc) 137 | pub fn run_prepare, F: Fn(LuaState)>( 138 | l: LuaState, 139 | script: S, 140 | func: F, 141 | ) -> Result { 142 | let top = lua_gettop(l); 143 | 144 | if let Err(why) = lua::compile(l, script) { 145 | return Err(LuaEnvError::Compile(why.to_string())); 146 | } 147 | 148 | // stack = {} 149 | lua_createtable(l, 0, 0); // stack[1] = {} 150 | lua_createtable(l, 0, 0); // stack[2] = {} 151 | 152 | func(l); 153 | 154 | lua_setfield(l, -2, cstr!("Autorun")); // stack[1].Autorun = table.remove(stack, 2) 155 | 156 | // Create a metatable to make the env inherit from _G 157 | lua_createtable(l, 0, 1); // stack[2] = {} 158 | lua_pushvalue(l, GLOBALSINDEX); // stack[3] = _G 159 | lua_setfield(l, -2, cstr!("__index")); // stack[2].__index = table.remove(stack, 3) 160 | lua_setmetatable(l, -2); // setmetatable(stack[1], table.remove(stack, 2)) 161 | 162 | lua_setfenv(l, -2); // setfenv(l, table.remove(stack, 1)) 163 | 164 | if let Err(why) = lua::pcall(l) { 165 | return Err(LuaEnvError::Runtime(why.to_string())); 166 | } 167 | 168 | Ok(top) 169 | } 170 | 171 | // Runs lua, but inside of the `autorun` environment. 172 | pub fn run_env_prep, F: Fn(LuaState), P: AsRef>( 173 | l: LuaState, 174 | script: S, 175 | path: P, 176 | env: &AutorunEnv, 177 | prep: &Option, 178 | ) -> Result { 179 | let path = path.as_ref(); 180 | run_prepare(l, script, |l| { 181 | // Autorun located at -2 182 | 183 | lua_pushstring(l, env.identifier); // stack[3] = identifier 184 | lua_setfield(l, -2, cstr!("NAME")); // stack[2].NAME = table.remove(stack, 3) 185 | 186 | lua_pushinteger(l, env.code_len as LuaInteger); // stack[3] = code_len 187 | lua_setfield(l, -2, cstr!("CODE_LEN")); // stack[2].CODE_LEN = table.remove(stack, 3) 188 | 189 | lua_pushlstring(l, env.code, env.code_len); // stack[3] = identifier 190 | lua_setfield(l, -2, cstr!("CODE")); // stack[2].CODE = table.remove(stack, 3) 191 | 192 | lua_pushstring(l, env.ip); 193 | lua_setfield(l, -2, cstr!("IP")); // stack[2].IP = table.remove(stack, 3) 194 | 195 | // If this is running before autorun, set Autorun.STARTUP to true. 196 | lua_pushboolean(l, i32::from(env.startup)); // stack[3] = startup 197 | lua_setfield(l, -2, cstr!("STARTUP")); // stack[2].STARTUP = table.remove(stack, 3) 198 | 199 | let path_str = path.display().to_string(); 200 | let path_bytes = path_str.as_bytes(); 201 | lua_pushlstring(l, path_bytes.as_ptr().cast(), path_str.len()); 202 | lua_setfield(l, -2, cstr!("PATH")); // stack[2].PATH = table.remove(stack, 3) 203 | 204 | let fns = reg! [ 205 | "log" => env::log, 206 | "require" => env::require, 207 | "requirebin" => env::requirebin, 208 | "print" => env::print, 209 | "readFile" => env::read 210 | ]; 211 | 212 | luaL_register(l, std::ptr::null_mut(), fns.as_ptr()); 213 | 214 | if let Some(f) = &prep { 215 | f(l); 216 | } 217 | }) 218 | } 219 | 220 | pub fn run_env, P: AsRef>( 221 | l: LuaState, 222 | script: S, 223 | path: P, 224 | env: &AutorunEnv, 225 | ) -> Result { 226 | run_env_prep::(l, script, path, env, &None) 227 | } 228 | -------------------------------------------------------------------------------- /autorun/src/plugins/mod.rs: -------------------------------------------------------------------------------- 1 | use rglua::prelude::*; 2 | 3 | use crate::fs::{self as afs, FSPath, PLUGIN_DIR}; 4 | use crate::lua::{self, AutorunEnv, LuaEnvError}; 5 | use crate::{logging::*, ui::printcol}; 6 | use fs_err as fs; 7 | 8 | use std::borrow::Cow; 9 | use std::ffi::{CString, OsStr}; 10 | use std::path::Path; 11 | 12 | mod serde; 13 | pub use self::serde::{PluginMetadata, PluginToml}; 14 | 15 | #[derive(Debug, thiserror::Error)] 16 | pub enum PluginError { 17 | #[error("IO Error: {0}")] 18 | IO(#[from] std::io::Error), 19 | 20 | #[error("{0}")] 21 | LuaEnv(#[from] LuaEnvError), 22 | 23 | #[error("Failed to parse plugin.toml: {0}")] 24 | Parsing(toml::de::Error), 25 | 26 | #[error("Could not find plugin.toml")] 27 | NoToml, 28 | } 29 | 30 | #[non_exhaustive] 31 | pub enum PluginLanguage { 32 | // In the future there could be more languages like Teal or Expressive 33 | Lua, 34 | } 35 | 36 | impl Default for PluginLanguage { 37 | fn default() -> Self { 38 | Self::Lua 39 | } 40 | } 41 | 42 | #[derive(Debug)] 43 | pub struct Plugin { 44 | data: serde::PluginToml, 45 | 46 | /// Path to plugin's directory local to autorun directory 47 | dir: FSPath, 48 | } 49 | 50 | // Result of running hook.lua 51 | #[derive(PartialEq)] 52 | pub enum HookRet { 53 | Stop, 54 | /// Replace running code 55 | Replace(LuaString, usize), 56 | Continue, 57 | } 58 | 59 | impl Plugin { 60 | pub fn get_name(&self) -> &String { 61 | &self.data.plugin.name 62 | } 63 | 64 | pub fn get_author(&self) -> &String { 65 | &self.data.plugin.author 66 | } 67 | 68 | pub fn get_version(&self) -> &String { 69 | &self.data.plugin.version 70 | } 71 | 72 | pub fn get_description(&self) -> &Option { 73 | &self.data.plugin.description 74 | } 75 | 76 | pub fn get_settings(&self) -> &toml::Value { 77 | &self.data.settings 78 | } 79 | 80 | pub fn get_dir(&self) -> &FSPath { 81 | &self.dir 82 | } 83 | 84 | pub fn has_file>(&self, name: N) -> bool { 85 | let name = name.as_ref(); 86 | let path = self.dir.join(name); 87 | path.exists() 88 | } 89 | 90 | fn push_settings(&self, l: LuaState) { 91 | match self.get_settings().as_table() { 92 | Some(tbl) => { 93 | lua_createtable(l, 0, tbl.len() as i32); 94 | 95 | fn push_value(l: LuaState, v: &toml::Value) { 96 | match v { 97 | toml::Value::String(s) => { 98 | let bytes = s.as_bytes(); 99 | lua_pushlstring(l, bytes.as_ptr().cast(), bytes.len()); 100 | } 101 | toml::Value::Integer(n) => lua_pushinteger(l, *n as LuaInteger), 102 | toml::Value::Boolean(b) => lua_pushboolean(l, *b as i32), 103 | 104 | toml::Value::Float(f) => lua_pushnumber(l, *f), 105 | 106 | toml::Value::Array(arr) => { 107 | lua_createtable(l, arr.len() as i32, 0); 108 | 109 | for (i, v) in arr.iter().enumerate() { 110 | push_value(l, v); 111 | lua_rawseti(l, -2, i as i32 + 1); 112 | } 113 | } 114 | 115 | toml::Value::Table(tbl) => { 116 | lua_createtable(l, 0, tbl.len() as i32); 117 | 118 | for (k, v) in tbl.iter() { 119 | if let Ok(k) = CString::new(k.as_bytes()) { 120 | push_value(l, v); 121 | lua_setfield(l, -2, k.as_ptr()); 122 | } 123 | } 124 | } 125 | 126 | toml::Value::Datetime(time) => { 127 | // Just pass a string, smh 128 | let time = time.to_string(); 129 | let bytes = time.as_bytes(); 130 | lua_pushlstring(l, bytes.as_ptr() as _, bytes.len()); 131 | } 132 | } 133 | } 134 | 135 | for (k, v) in tbl.iter() { 136 | let k = match CString::new(k.as_bytes()) { 137 | Ok(k) => k, 138 | Err(_) => continue, 139 | }; 140 | 141 | push_value(l, v); 142 | lua_setfield(l, -2, k.as_ptr()); 143 | } 144 | } 145 | None => lua_createtable(l, 0, 0), 146 | } 147 | } 148 | 149 | pub fn run_lua, P: AsRef>( 150 | &self, 151 | l: LuaState, 152 | src: S, 153 | path: P, 154 | env: &AutorunEnv, 155 | ) -> Result { 156 | lua::run_env_prep( 157 | l, 158 | src, 159 | path, 160 | env, 161 | &Some(|l| { 162 | lua_createtable(l, 0, 4); 163 | 164 | let name = self.get_name(); 165 | if let Ok(name) = CString::new(name.as_bytes()) { 166 | lua_pushstring(l, name.as_ptr()); 167 | lua_setfield(l, -2, cstr!("NAME")); 168 | } 169 | 170 | let version = self.get_version(); 171 | if let Ok(version) = CString::new(version.as_bytes()) { 172 | lua_pushstring(l, version.as_ptr()); 173 | lua_setfield(l, -2, cstr!("VERSION")); 174 | } 175 | 176 | let author = self.get_author(); 177 | if let Ok(author) = CString::new(author.as_bytes()) { 178 | lua_pushstring(l, author.as_ptr()); 179 | lua_setfield(l, -2, cstr!("AUTHOR")); 180 | } 181 | 182 | let dir = self.get_dir(); 183 | let dirname = dir.file_name(); 184 | if let Some(d) = dirname { 185 | let d = d.to_string_lossy(); 186 | let bytes = d.as_bytes(); 187 | lua_pushlstring(l, bytes.as_ptr() as *mut _, bytes.len()); 188 | lua_setfield(l, -2, cstr!("DIR")); 189 | } 190 | 191 | if let Some(desc) = self.get_description() { 192 | if let Ok(desc) = CString::new(desc.as_bytes()) { 193 | lua_pushstring(l, desc.as_ptr()); 194 | lua_setfield(l, -2, cstr!("DESCRIPTION")); 195 | } 196 | } 197 | 198 | self.push_settings(l); 199 | lua_setfield(l, -2, cstr!("Settings")); 200 | 201 | lua_setfield(l, -2, cstr!("Plugin")); 202 | }), 203 | ) 204 | } 205 | 206 | /// dofile but if the ran code returns a boolean or string, will return that to Rust. 207 | pub fn dohook(&self, l: LuaState, env: &AutorunEnv) -> Result { 208 | let path = self.dir.join("src/hook.lua"); 209 | let src = afs::read_to_string(&path)?; 210 | let top = self.run_lua(l, src, &path, env)?; 211 | 212 | let ret = match lua_type(l, top + 1) { 213 | rglua::lua::TBOOLEAN => { 214 | if lua_toboolean(l, -1) != 0 { 215 | Ok(HookRet::Stop) 216 | } else { 217 | Ok(HookRet::Continue) 218 | } 219 | } 220 | 221 | rglua::lua::TSTRING => { 222 | let mut len: usize = 0; 223 | let code = lua_tolstring(l, top + 1, &mut len); 224 | Ok(HookRet::Replace(code, len)) 225 | } 226 | 227 | _ => Ok(HookRet::Continue), 228 | }; 229 | 230 | lua_settop(l, top); 231 | 232 | ret 233 | } 234 | 235 | pub fn dofile>( 236 | &self, 237 | l: LuaState, 238 | path: P, 239 | env: &AutorunEnv, 240 | ) -> Result<(), PluginError> { 241 | let path = self.dir.join(path); 242 | let src = afs::read_to_string(&path)?; 243 | self.run_lua(l, src, &path, env)?; 244 | Ok(()) 245 | } 246 | } 247 | 248 | /// Searches for plugin and makes sure they are all valid, if not, prints errors to the user. 249 | pub fn sanity_check() -> Result<(), PluginError> { 250 | afs::traverse_dir(PLUGIN_DIR, |path, _| { 251 | if path.is_dir() { 252 | let plugin_toml = path.join("plugin.toml"); 253 | 254 | let src_autorun = path.join("src/autorun.lua"); 255 | let src_hooks = path.join("src/hook.lua"); 256 | 257 | let path_name = path.file_name().map_or_else( 258 | || Cow::Owned(path.display().to_string()), 259 | OsStr::to_string_lossy, 260 | ); 261 | 262 | if plugin_toml.exists() && (src_autorun.exists() || src_hooks.exists()) { 263 | if let Ok(content) = afs::read_to_string(plugin_toml) { 264 | match toml::from_str::(&content) { 265 | Ok(_) => (), 266 | Err(why) => error!( 267 | "Failed to load plugin {}. plugin.toml failed to parse: '{}'", 268 | path_name, why 269 | ), 270 | } 271 | } else { 272 | error!( 273 | "Failed to load plugin {}. plugin.toml could not be read.", 274 | path_name 275 | ); 276 | } 277 | } else if plugin_toml.exists() { 278 | error!("Failed to load plugin {}. plugin.toml exists but no src/autorun.lua or src/hook.lua", path_name); 279 | } else { 280 | error!( 281 | "Failed to load plugin {}. plugin.toml does not exist", 282 | path_name 283 | ); 284 | } 285 | } 286 | })?; 287 | 288 | Ok(()) 289 | } 290 | 291 | // (Directory, PluginOrError) 292 | type PluginFS = (String, Result); 293 | 294 | /// Finds all valid plugins (has plugin.toml, src/autorun.lua or src/hook.lua) 295 | pub fn find() -> Result, PluginError> { 296 | let mut plugins = vec![]; 297 | 298 | afs::traverse_dir(PLUGIN_DIR, |path, _| { 299 | let path_name = path.file_name().map_or_else( 300 | || path.display().to_string(), 301 | |x| x.to_string_lossy().to_string(), 302 | ); 303 | 304 | if path.is_dir() { 305 | let plugin_toml = path.join("plugin.toml"); 306 | let res = if plugin_toml.exists() { 307 | if let Ok(content) = afs::read_to_string(plugin_toml) { 308 | match toml::from_str::(&content) { 309 | Ok(toml) => Ok(Plugin { 310 | data: toml, 311 | dir: path.to_owned(), 312 | }), 313 | Err(why) => Err(PluginError::Parsing(why)), 314 | } 315 | } else { 316 | Err(PluginError::NoToml) 317 | } 318 | } else { 319 | Err(PluginError::NoToml) 320 | }; 321 | 322 | plugins.push((path_name, res)); 323 | } 324 | })?; 325 | 326 | Ok(plugins) 327 | } 328 | 329 | /// Run ``autorun.lua`` in all plugins. 330 | pub fn call_autorun(l: LuaState, env: &AutorunEnv) -> Result<(), PluginError> { 331 | for (dirname, plugin) in find()? { 332 | match plugin { 333 | Ok(plugin) => { 334 | if plugin.has_file("src/autorun.lua") { 335 | if let Err(why) = plugin.dofile(l, "src/autorun.lua", env) { 336 | error!("Error in plugin '{}': [{}]", plugin.get_name(), why); 337 | }; 338 | } 339 | } 340 | Err(why) => { 341 | error!("Failed to load plugin @plugins/{dirname}: {}", why); 342 | } 343 | } 344 | } 345 | 346 | Ok(()) 347 | } 348 | 349 | /// Run ``hook.lua`` in all plugins. 350 | /// Does not print out any errors unlike `call_autorun`. 351 | pub fn call_hook( 352 | l: LuaState, 353 | env: &AutorunEnv, 354 | do_run: &mut bool, 355 | ) -> Result, PluginError> { 356 | for plugin in find()? { 357 | if let (_, Ok(plugin)) = plugin { 358 | // All of the plugin hook.lua will still run even if the first plugin returned a string or a boolean. 359 | // They will however have their return values ignored. 360 | if let Ok(plugin_ret) = plugin.dohook(l, env) { 361 | match plugin_ret { 362 | HookRet::Continue => (), 363 | HookRet::Replace(code, len) => { 364 | // Code to edit script and have other plugins ``hook.lua`` filess still run 365 | // Not sure if this should be the behavior so just having it abort. 366 | // (code, code_len) = (loc_code, loc_len); 367 | // env.set_code(code, code_len); 368 | 369 | return Ok(Some((code, len))); 370 | } 371 | HookRet::Stop => { 372 | *do_run = false; 373 | } 374 | } 375 | } 376 | } 377 | } 378 | Ok(None) 379 | } 380 | 381 | pub fn init() -> Result<(), PluginError> { 382 | let plugin_dir = afs::in_autorun(PLUGIN_DIR); 383 | if !plugin_dir.exists() { 384 | fs::create_dir(&plugin_dir)?; 385 | } 386 | 387 | sanity_check()?; 388 | 389 | let plugins = find()?; 390 | 391 | printcol!(WHITE, "Verifying plugins.."); 392 | if plugins.is_empty() { 393 | printcol!(WHITE, on_green, "{}", "No plugins found!"); 394 | } 395 | 396 | for plugin in plugins { 397 | match plugin { 398 | (name, Err(why)) => error!("Failed to verify plugin @plugins/{name}: {}", why), 399 | (_, Ok(plugin)) => info!("Verified plugin: {}", plugin.get_name()), 400 | } 401 | } 402 | 403 | Ok(()) 404 | } 405 | -------------------------------------------------------------------------------- /autorun/src/plugins/serde.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Deserialize, Serialize, Debug)] 4 | pub struct PluginToml { 5 | pub plugin: PluginMetadata, 6 | pub settings: toml::Value, 7 | } 8 | 9 | #[derive(Deserialize, Serialize, Debug)] 10 | pub struct PluginMetadata { 11 | pub name: String, // Name of the plugin to be displayed to the user 12 | pub author: String, // TODO: Maybe make this a list? 13 | pub version: String, 14 | pub description: Option, 15 | 16 | pub language: Option, 17 | pub version_required: Option, // Required version of Autorun for the plugin to run 18 | } 19 | -------------------------------------------------------------------------------- /autorun/src/ui/console/commands.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | #[cfg(executor)] 4 | use autorun_shared::Realm; 5 | 6 | use crate::{ 7 | configs::SETTINGS, 8 | fs as afs, lua, 9 | ui::console::palette::{formatcol, printcol, printerror}, 10 | }; 11 | use fs_err as fs; 12 | 13 | #[derive(Debug, thiserror::Error)] 14 | pub enum CommandError { 15 | #[error("Error while running lua: {0}")] 16 | Lua(#[from] lua::RunError), 17 | 18 | #[error("Error while running command: {0}")] 19 | IO(#[from] std::io::Error), 20 | 21 | #[error("Serializing error: {0}")] 22 | Ser(#[from] toml::ser::Error), 23 | } 24 | 25 | type CommandArgs<'a> = std::str::Split<'a, char>; 26 | type CommandList<'a> = HashMap<&'a str, Command<'a>>; 27 | 28 | pub struct Command<'a> { 29 | pub desc: &'a str, 30 | pub func: fn(&CommandList, CommandArgs, &str) -> Result<(), CommandError>, 31 | } 32 | 33 | macro_rules! command { 34 | ($desc:literal, $cls:expr) => { 35 | Command { 36 | desc: $desc, 37 | func: $cls, 38 | } 39 | }; 40 | } 41 | 42 | pub fn list<'a>() -> HashMap<&'a str, Command<'a>> { 43 | let mut commands: HashMap<&str, Command> = HashMap::new(); 44 | 45 | commands.insert( 46 | "help", 47 | command!("Prints out all of the commands", |cmds, mut args, _| { 48 | match args.next() { 49 | Some(cmd_name) if !cmd_name.trim().is_empty() => { 50 | if let Some(cmd) = cmds.get(cmd_name) { 51 | printcol!( 52 | CYAN, 53 | italic, 54 | "Help for {}:\n{}", 55 | formatcol!(YELLOW, bold, "{cmd_name}"), 56 | formatcol!(BRIGHT_GREEN, "{}", cmd.desc) 57 | ); 58 | } else { 59 | printerror!( 60 | normal, 61 | "Command not found: {}", 62 | formatcol!(YELLOW, bold, "{cmd_name}") 63 | ); 64 | } 65 | } 66 | 67 | _ => { 68 | println!("[{}]:", formatcol!(CYAN, "Commands")); 69 | 70 | for (name, cmd) in cmds.iter() { 71 | println!( 72 | "{}: {}", 73 | formatcol!(YELLOW, bold, "{}", name), 74 | formatcol!(BRIGHT_GREEN, "{}", cmd.desc) 75 | ); 76 | } 77 | } 78 | } 79 | 80 | Ok(()) 81 | }), 82 | ); 83 | 84 | #[cfg(executor)] 85 | commands.insert( 86 | "lua_run_cl", 87 | command!("Runs a lua script", |_, _, rest| { 88 | lua::run(Realm::Client, rest.to_owned())?; 89 | Ok(()) 90 | }), 91 | ); 92 | 93 | #[cfg(executor)] 94 | commands.insert( 95 | "lua_run_menu", 96 | command!("Runs a lua script from the menu", |_, _, rest| { 97 | lua::run(Realm::Menu, rest.to_owned())?; 98 | Ok(()) 99 | }), 100 | ); 101 | 102 | #[cfg(executor)] 103 | commands.insert( 104 | "lua_openscript_menu", 105 | command!("Opens a lua script from the menu", |_, mut args, _| { 106 | if let Some(rawpath) = args.next() { 107 | let mut path = std::path::PathBuf::from(rawpath); 108 | if path.extension().is_none() { 109 | path.set_extension("lua"); 110 | } 111 | 112 | if !path.exists() { 113 | path = afs::in_autorun(afs::INCLUDE_DIR).join(path); 114 | } 115 | let content = fs::read_to_string(path)?; 116 | lua::run(Realm::Menu, content)?; 117 | } else { 118 | printcol!( 119 | CYAN, 120 | "Usage: {} {}", 121 | formatcol!(YELLOW, "lua_openscript_menu"), 122 | formatcol!(BRIGHT_GREEN, "") 123 | ); 124 | } 125 | 126 | Ok(()) 127 | }), 128 | ); 129 | 130 | #[cfg(executor)] 131 | commands.insert( 132 | "lua_openscript_cl", 133 | command!("Opens a lua script from the menu", |_, mut args, _| { 134 | if let Some(rawpath) = args.next() { 135 | let mut path = std::path::PathBuf::from(rawpath); 136 | if path.extension().is_none() { 137 | path.set_extension("lua"); 138 | } 139 | 140 | if !path.exists() { 141 | path = afs::in_autorun(afs::INCLUDE_DIR).join(path); 142 | } 143 | let content = fs::read_to_string(path)?; 144 | lua::run(Realm::Client, content)?; 145 | } else { 146 | printcol!( 147 | CYAN, 148 | "Usage: {} {}", 149 | formatcol!(YELLOW, "lua_openscript_cl"), 150 | formatcol!(BRIGHT_GREEN, "") 151 | ); 152 | } 153 | 154 | Ok(()) 155 | }), 156 | ); 157 | 158 | commands.insert( 159 | "settings", 160 | command!("Prints out your current settings", |_, _, _| { 161 | printcol!(BRIGHT_BLUE, "{:#?}", *SETTINGS); 162 | Ok(()) 163 | }), 164 | ); 165 | 166 | commands.insert( 167 | "hide", 168 | command!("Hides the console", |_, _, _| { 169 | super::hide(); 170 | Ok(()) 171 | }), 172 | ); 173 | 174 | // Credit: https://stackoverflow.com/a/6487534/14076600 175 | // I had no idea clearing console was this bad on windows.. 176 | #[cfg(windows)] 177 | commands.insert( 178 | "clear", 179 | command!("Clears the console", |_, _, _| { 180 | use std::mem::MaybeUninit; 181 | use winapi::um::{ 182 | wincon::{ 183 | FillConsoleOutputAttribute, FillConsoleOutputCharacterA, 184 | GetConsoleScreenBufferInfo, SetConsoleCursorPosition, FOREGROUND_BLUE, 185 | FOREGROUND_GREEN, FOREGROUND_RED, 186 | }, 187 | wincontypes::COORD, 188 | }; 189 | 190 | let top_left = COORD { X: 0, Y: 0 }; 191 | let console = unsafe { 192 | winapi::um::processenv::GetStdHandle(winapi::um::winbase::STD_OUTPUT_HANDLE) 193 | }; 194 | 195 | let mut screen = MaybeUninit::uninit(); 196 | 197 | unsafe { 198 | GetConsoleScreenBufferInfo(console, screen.as_mut_ptr()); 199 | } 200 | 201 | let mut written = 0u32; 202 | let screen = unsafe { screen.assume_init() }; 203 | 204 | let len_u32 = (screen.dwSize.X as u32).wrapping_mul(screen.dwSize.Y as u32); 205 | 206 | unsafe { 207 | FillConsoleOutputCharacterA(console, b' ' as i8, len_u32, top_left, &mut written); 208 | 209 | FillConsoleOutputAttribute( 210 | console, 211 | FOREGROUND_GREEN | FOREGROUND_RED | FOREGROUND_BLUE, 212 | len_u32, 213 | top_left, 214 | &mut written, 215 | ); 216 | 217 | SetConsoleCursorPosition(console, top_left); 218 | } 219 | 220 | Ok(()) 221 | }), 222 | ); 223 | 224 | // General ``plugin`` command 225 | #[cfg(plugins)] 226 | commands.insert( 227 | "plugin", 228 | command!("General plugin command", |_, mut args, _| { 229 | use crate::plugins; 230 | if let Some(subcommand) = args.next() { 231 | match subcommand { 232 | "help" => { 233 | printcol!( 234 | WHITE, 235 | "[{}]:\n{}\n{}", 236 | formatcol!(CYAN, "Plugin Help"), 237 | formatcol!( 238 | RED, 239 | "Use {} to get a list of (would be) active plugins", 240 | formatcol!(YELLOW, "list") 241 | ), 242 | formatcol!( 243 | RED, 244 | "Use {} {} to create a new plugin", 245 | formatcol!(YELLOW, "new"), 246 | formatcol!(BRIGHT_GREEN, "") 247 | ) 248 | ); 249 | } 250 | 251 | "list" => { 252 | if let Ok(plugins) = plugins::find() { 253 | printcol!(WHITE, "[{}]:", formatcol!(CYAN, "Plugin List")); 254 | 255 | for (dirname, plugin) in plugins { 256 | printcol!( 257 | RED, 258 | "plugins/{dirname}: {}", 259 | match plugin { 260 | Ok(plugin) => { 261 | formatcol!( 262 | RED, 263 | // Safety 0.1.0 by Vurv 264 | "{} {} by {}", 265 | formatcol!(PURPLE, bold, "{}", plugin.get_name()), 266 | formatcol!(YELLOW, "{}", plugin.get_version()), 267 | formatcol!(BLUE, "{}", plugin.get_author()) 268 | ) 269 | } 270 | Err(why) => { 271 | formatcol!(WHITE, on_bright_red, "Malformed {}", why) 272 | } 273 | } 274 | ); 275 | } 276 | } else { 277 | printerror!(normal, "Failed to find any plugins"); 278 | } 279 | } 280 | 281 | "new" => { 282 | if let Some(plugin_name) = args.next() { 283 | use crate::fs as afs; 284 | 285 | let path = afs::FSPath::from(afs::PLUGIN_DIR).join(plugin_name); 286 | 287 | if plugin_name.trim().is_empty() { 288 | printerror!(normal, "Plugin name cannot be empty"); 289 | } else if path.extension().is_some() { 290 | printerror!( 291 | normal, 292 | "Malformed plugin name (did not expect file extension)" 293 | ); 294 | } else if path.exists() { 295 | printerror!( 296 | normal, 297 | "Cannot create plugin {}, path already exists", 298 | formatcol!(YELLOW, "{}", plugin_name) 299 | ); 300 | } else { 301 | use std::io::Write; 302 | afs::create_dir(&path)?; 303 | 304 | let mut plugin_toml = afs::create_file(&path.join("plugin.toml"))?; 305 | let plugin_struct = crate::plugins::PluginToml { 306 | // There's way too much to_owned here. 307 | // Need to refactor the structure to use borrowed slices 308 | plugin: crate::plugins::PluginMetadata { 309 | name: plugin_name.to_owned(), 310 | author: "You".to_owned(), 311 | version: "0.1.0".to_owned(), 312 | description: None, 313 | language: Some("lua".to_owned()), 314 | version_required: Some( 315 | env!("CARGO_PKG_VERSION").to_owned(), 316 | ), 317 | }, 318 | settings: toml::Value::Table(toml::map::Map::new()), 319 | }; 320 | write!(plugin_toml, "{}", toml::to_string(&plugin_struct)?)?; 321 | 322 | // emmylua definitions 323 | let mut fields = afs::create_file(&path.join("fields.lua"))?; 324 | write!(fields, "{}", include_str!("../../../../fields.lua"))?; 325 | 326 | let src = path.join("src"); 327 | afs::create_dir(&src)?; 328 | 329 | let mut autorun = afs::create_file(&src.join("autorun.lua"))?; 330 | writeln!(autorun, "-- Autorun.log(\"Hello, autorun.lua!\")")?; 331 | 332 | let mut hook = afs::create_file(&src.join("hook.lua"))?; 333 | writeln!(hook, "-- print(\"Hello, hook.lua!\")")?; 334 | } 335 | } else { 336 | printcol!( 337 | CYAN, 338 | "Usage: {} {}", 339 | formatcol!(YELLOW, "plugin new"), 340 | formatcol!(BRIGHT_GREEN, "") 341 | ); 342 | } 343 | } 344 | 345 | "remove" => { 346 | if let Some(plugin_name) = args.next() { 347 | use crate::fs as afs; 348 | 349 | let path = afs::FSPath::from(afs::PLUGIN_DIR).join(plugin_name); 350 | 351 | if plugin_name.trim().is_empty() { 352 | printerror!(normal, "Plugin name cannot be empty"); 353 | } else if path.extension().is_some() { 354 | printerror!( 355 | normal, 356 | "Malformed plugin name (did not expect file extension)" 357 | ); 358 | } else if !path.exists() { 359 | printerror!( 360 | normal, 361 | "Impossible to delete plugin {}, path doesn't exist", 362 | formatcol!(YELLOW, "{}", plugin_name) 363 | ); 364 | } else { 365 | afs::remove_dir(&path)?; 366 | } 367 | } else { 368 | printcol!( 369 | CYAN, 370 | "Usage: {} {}", 371 | formatcol!(YELLOW, "plugin remove"), 372 | formatcol!(BRIGHT_GREEN, "") 373 | ); 374 | } 375 | } 376 | 377 | other => { 378 | if other.trim().is_empty() { 379 | printcol!( 380 | CYAN, 381 | "Subcommands: [{}, {}, {}, {}]", 382 | formatcol!(BRIGHT_GREEN, "help"), 383 | formatcol!(BRIGHT_GREEN, "list"), 384 | formatcol!(BRIGHT_GREEN, "new"), 385 | formatcol!(BRIGHT_GREEN, "remove") 386 | ); 387 | } else { 388 | printcol!( 389 | CYAN, 390 | "Unknown subcommand: {} (Should be {}, {}, {} or {})", 391 | formatcol!(BRIGHT_GREEN, "{}", subcommand), 392 | formatcol!(BRIGHT_GREEN, "help"), 393 | formatcol!(BRIGHT_GREEN, "list"), 394 | formatcol!(BRIGHT_GREEN, "new"), 395 | formatcol!(BRIGHT_GREEN, "remove") 396 | ); 397 | } 398 | } 399 | } 400 | } else { 401 | printcol!( 402 | CYAN, 403 | "Usage: {} {}", 404 | formatcol!(YELLOW, "plugin"), 405 | formatcol!(BRIGHT_GREEN, "") 406 | ); 407 | } 408 | Ok(()) 409 | }), 410 | ); 411 | 412 | commands 413 | } 414 | -------------------------------------------------------------------------------- /autorun/src/ui/console/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{configs::SETTINGS, logging::*}; 2 | 3 | mod commands; 4 | #[cfg(windows)] 5 | mod tray; 6 | 7 | pub mod palette; 8 | 9 | use palette::{formatcol, printcol}; 10 | 11 | pub fn init() { 12 | unsafe { 13 | // Load this library before it starts spamming useless errors into our console. 14 | // https://github.com/Vurv78/Autorun-rs/issues/26 15 | let _ = libloading::Library::new("vaudio_speex"); 16 | // winapi::um::libloaderapi::LoadLibraryA(rglua::cstr!("vaudio_speex.dll")); 17 | 18 | #[cfg(windows)] 19 | winapi::um::consoleapi::AllocConsole(); 20 | }; 21 | 22 | #[cfg(windows)] 23 | if colored::control::set_virtual_terminal(true).is_err() { 24 | eprintln!("Failed to enable colored output"); 25 | } 26 | 27 | colored::control::set_override(SETTINGS.color_enabled()); 28 | 29 | let version = env!("CARGO_PKG_VERSION"); 30 | printcol!( 31 | BRIGHT_BLACK, 32 | "<====> {} {} {} <====>", 33 | formatcol!(CYAN, "Autorun"), 34 | formatcol!(RED, bold, "v{}", version), 35 | formatcol!( 36 | CYAN, 37 | "on {}", 38 | formatcol!(RED, bold, "{}", std::env::consts::ARCH) 39 | ) 40 | ); 41 | 42 | printcol!( 43 | BRIGHT_RED, 44 | bold, 45 | "Type {} for a list of commands", 46 | formatcol!(YELLOW, bold, "{}", "help") 47 | ); 48 | 49 | let hidden = SETTINGS.autorun.hide; 50 | 51 | std::thread::spawn(move || { 52 | if hidden { 53 | hide(); 54 | } 55 | 56 | start(); 57 | }); 58 | } 59 | 60 | fn start() { 61 | #[cfg(unix)] 62 | { 63 | use winit::{ 64 | event::{Event, WindowEvent}, 65 | event_loop::{ControlFlow, EventLoop}, 66 | window::WindowBuilder, 67 | }; 68 | 69 | std::thread::spawn(|| { 70 | let event_loop = EventLoop::new(); 71 | let window = { 72 | WindowBuilder::new() 73 | .with_title("Autorun") 74 | .build(&event_loop) 75 | .unwrap() 76 | }; 77 | 78 | event_loop.run(move |event, _, control_flow| { 79 | *control_flow = ControlFlow::Wait; 80 | 81 | match event { 82 | Event::WindowEvent { 83 | event: WindowEvent::CloseRequested, 84 | window_id, 85 | } if window_id == window.id() => *control_flow = ControlFlow::Exit, 86 | _ => (), 87 | } 88 | }); 89 | }); 90 | } 91 | 92 | let commands = commands::list(); 93 | 94 | let mut buffer = String::new(); 95 | loop { 96 | buffer.clear(); 97 | 98 | // Loop forever in this thread, since it is separate from Gmod, and take in user input. 99 | if let Err(why) = std::io::stdin().read_line(&mut buffer) { 100 | error!("{why}"); 101 | } else { 102 | let (cmd, rest) = buffer.split_once(' ').unwrap_or((buffer.trim_end(), "")); 103 | 104 | let rest_trim = rest.trim_end(); 105 | let args = rest_trim.split(' '); 106 | 107 | if let Some(cmd) = commands.get(cmd) { 108 | if let Err(why) = (cmd.func)(&commands, args, rest_trim) { 109 | crate::ui::printerror!(normal, "{why}"); 110 | } 111 | }; 112 | } 113 | } 114 | } 115 | 116 | pub fn hide() { 117 | #[cfg(windows)] 118 | if let Err(why) = tray::replace_window() { 119 | error!("Failed to hide window: {why}"); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /autorun/src/ui/console/palette.rs: -------------------------------------------------------------------------------- 1 | // Palette using iceberg-dark theme 2 | // https://windowsterminalthemes.dev 3 | type Rgb = (u8, u8, u8); 4 | 5 | macro_rules! palette { 6 | ( $name:ident : rgb($r:literal, $g:literal, $b:literal); $($rest:tt)* ) => { 7 | #[allow(unused)] 8 | pub const $name: Rgb = ($r, $g, $b); 9 | 10 | palette!( $($rest)* ); 11 | }; 12 | 13 | () => (); 14 | } 15 | 16 | palette! { 17 | BLACK: rgb(26, 33, 49); 18 | RED: rgb(226, 120, 120); 19 | GREEN: rgb(180, 190, 130); 20 | YELLOW: rgb(226, 164, 120); 21 | BLUE: rgb(132, 160, 198); 22 | PURPLE: rgb(160, 147, 199); 23 | CYAN: rgb(137, 184, 194); 24 | WHITE: rgb(198, 200, 209); 25 | BRIGHT_BLACK: rgb(107, 112, 137); 26 | BRIGHT_RED: rgb(233, 120, 120); 27 | BRIGHT_GREEN: rgb(192, 202, 142); 28 | BRIGHT_YELLOW: rgb(233, 176, 142); 29 | BRIGHT_BLUE: rgb(145, 172, 209); 30 | BRIGHT_PURPLE: rgb(172, 159, 210); 31 | BRIGHT_CYAN: rgb(149, 188, 206); 32 | BRIGHT_WHITE: rgb(210, 212, 221); 33 | BACKGROUND: rgb(22, 24, 33); 34 | FOREGROUND: rgb(198, 200, 209); 35 | SELECTION_BACKGROUND: rgb(198, 200, 209); 36 | CURSOR_COLOR: rgb(198, 200, 209); 37 | } 38 | 39 | /// println! macro using colors from the palette above and the ``colored`` crate. 40 | /// Made this since the crate has no real elegant ways to do this itself. 41 | /// printcol!(RED, "Error: {}", formatcol!(BLUE, "Foo")); 42 | macro_rules! printcol { 43 | ($name:ident, $msg:literal) => { 44 | println!( 45 | "{}", 46 | colored::Colorize::truecolor( 47 | $msg, 48 | $crate::ui::console::palette::$name.0, 49 | $crate::ui::console::palette::$name.1, 50 | $crate::ui::console::palette::$name.2 51 | ) 52 | ); 53 | }; 54 | 55 | ($name:ident, $fmt:literal, $($arg:tt)*) => { 56 | println!( 57 | "{}", 58 | colored::Colorize::truecolor( 59 | format!($fmt, $($arg)*).as_ref(), 60 | $crate::ui::console::palette::$name.0, 61 | $crate::ui::console::palette::$name.1, 62 | $crate::ui::console::palette::$name.2 63 | ) 64 | ); 65 | }; 66 | 67 | ($name:ident, $effect:ident, $fmt:literal, $($arg:tt)*) => { 68 | println!( 69 | "{}", 70 | colored::Colorize::truecolor( 71 | colored::Colorize::$effect( 72 | format!($fmt, $($arg)*).as_ref(), 73 | ), 74 | $crate::ui::console::palette::$name.0, 75 | $crate::ui::console::palette::$name.1, 76 | $crate::ui::console::palette::$name.2 77 | ) 78 | ); 79 | }; 80 | } 81 | 82 | /// format! macro using colors from the palette above and the ``colored`` crate. 83 | /// Made this since the crate has no real elegant ways to do this itself. 84 | /// formatcol!(RED, "Error: {}", formatcol!(BLUE, "Foo")); 85 | macro_rules! formatcol { 86 | ($name:ident, $msg:literal) => { 87 | format!( 88 | "{}", 89 | colored::Colorize::truecolor( 90 | $msg, 91 | $crate::ui::console::palette::$name.0, 92 | $crate::ui::console::palette::$name.1, 93 | $crate::ui::console::palette::$name.2 94 | ) 95 | ) 96 | }; 97 | 98 | ($name:ident, $effect:ident, $($arg:tt)+) => { 99 | format!( 100 | "{}", 101 | colored::Colorize::truecolor( 102 | colored::Colorize::$effect( 103 | std::fmt::format( format_args!( $($arg)+ ) ).as_ref(), 104 | ), 105 | $crate::ui::console::palette::$name.0, 106 | $crate::ui::console::palette::$name.1, 107 | $crate::ui::console::palette::$name.2 108 | ) 109 | ) 110 | }; 111 | 112 | ($name:ident, $($arg:tt)+) => { 113 | format!( 114 | "{}", 115 | colored::Colorize::truecolor( 116 | std::fmt::format( format_args!( $($arg)+ ) ).as_ref(), 117 | $crate::ui::console::palette::$name.0, 118 | $crate::ui::console::palette::$name.1, 119 | $crate::ui::console::palette::$name.2 120 | ) 121 | ) 122 | }; 123 | } 124 | 125 | /// ERROR foo bar 126 | macro_rules! printerror { 127 | ($effect:ident, $($arg:tt)+) => { 128 | println!( 129 | "{} {}", 130 | colored::Colorize::on_bright_red( colored::Colorize::white( colored::Colorize::bold(" ERROR ") ) ), 131 | $crate::ui::formatcol!(BRIGHT_WHITE, $effect, $($arg)+) 132 | ) 133 | }; 134 | } 135 | 136 | macro_rules! printwarning { 137 | ($effect:ident, $($arg:tt)+) => { 138 | println!( 139 | "{} {}", 140 | colored::Colorize::on_yellow( colored::Colorize::white( colored::Colorize::bold(" WARN ") ) ), 141 | $crate::ui::formatcol!(BRIGHT_WHITE, $effect, $($arg)+) 142 | ) 143 | }; 144 | } 145 | 146 | macro_rules! printinfo { 147 | ($effect:ident, $($arg:tt)+) => { 148 | println!( 149 | "{} {}", 150 | colored::Colorize::on_bright_blue( colored::Colorize::white( colored::Colorize::bold(" INFO ") ) ), 151 | $crate::ui::formatcol!(BRIGHT_WHITE, $effect, $($arg)+) 152 | ) 153 | }; 154 | } 155 | 156 | #[allow(unused)] 157 | macro_rules! printdebug { 158 | ($effect:ident, $($arg:tt)+) => { 159 | println!( 160 | "{} {}", 161 | colored::Colorize::on_purple( colored::Colorize::white( colored::Colorize::bold(" DEBUG ") ) ), 162 | $crate::ui::formatcol!(BRIGHT_WHITE, $effect, $($arg)+) 163 | ) 164 | }; 165 | } 166 | 167 | pub(crate) use {formatcol, printcol, printdebug, printerror, printinfo, printwarning}; 168 | -------------------------------------------------------------------------------- /autorun/src/ui/console/tray.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | mem::MaybeUninit, 3 | sync::atomic::{AtomicPtr, Ordering}, 4 | }; 5 | use trayicon::*; 6 | use winapi::um::{ 7 | wincon::GetConsoleWindow, 8 | winuser::{DispatchMessageA, GetMessageA, ShowWindow, TranslateMessage, SW_HIDE, SW_SHOW}, 9 | }; 10 | 11 | #[derive(Copy, Clone, Eq, PartialEq, Debug)] 12 | enum Events { 13 | Exit, 14 | } 15 | 16 | pub fn replace_window() -> Result<(), trayicon::Error> { 17 | let wind = unsafe { GetConsoleWindow() }; 18 | unsafe { ShowWindow(wind, SW_HIDE) }; 19 | 20 | let ptr = AtomicPtr::new(wind); 21 | 22 | let (send, recv) = std::sync::mpsc::channel::(); 23 | let icon = include_bytes!("../../../../assets/run.ico"); 24 | let _trayicon = TrayIconBuilder::new() 25 | .sender(send) 26 | .icon_from_buffer(icon) 27 | .tooltip("Open Autorun 🏃") 28 | .menu(MenuBuilder::new().item("Open Console", Events::Exit)) 29 | .build()?; 30 | 31 | let (send2, recv2) = std::sync::mpsc::channel::(); 32 | 33 | // Event loop 34 | let join = std::thread::spawn(move || { 35 | let mut i = recv.iter(); 36 | 37 | // Use if let since there's no other tray options right now. 38 | if let Some(m) = i.next() { 39 | match m { 40 | Events::Exit => { 41 | let window = ptr.load(Ordering::Relaxed); 42 | unsafe { ShowWindow(window, SW_SHOW) }; 43 | if let Err(why) = send2.send(true) { 44 | error!("Failed to send exit signal: {why}"); 45 | } 46 | } 47 | } 48 | } 49 | }); 50 | 51 | loop { 52 | if let Ok(true) = recv2.try_recv() { 53 | if join.join().is_err() { 54 | error!("Failed to join thread"); 55 | } 56 | 57 | break; 58 | } 59 | 60 | // Don't ask me. Windows black magic to get the message loop to work with tray icons. 61 | // Credit to example code from trayicon-rs. 62 | unsafe { 63 | let mut msg = MaybeUninit::uninit(); 64 | let bret = GetMessageA(msg.as_mut_ptr(), 0 as _, 0, 0); 65 | if bret > 0 { 66 | TranslateMessage(msg.as_ptr()); 67 | DispatchMessageA(msg.as_ptr()); 68 | } else { 69 | break; 70 | } 71 | } 72 | } 73 | 74 | Ok(()) 75 | } 76 | -------------------------------------------------------------------------------- /autorun/src/ui/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod console; 2 | #[allow(unused)] 3 | pub(crate) use console::palette::{ 4 | formatcol, printcol, printdebug, printerror, printinfo, printwarning, 5 | }; 6 | 7 | pub fn init() { 8 | console::init(); 9 | } 10 | -------------------------------------------------------------------------------- /autorun/src/version.rs: -------------------------------------------------------------------------------- 1 | const REPO: &str = env!("CARGO_PKG_REPOSITORY"); 2 | const RELEASES_ENDPOINT: &str = concat!(env!("CARGO_PKG_REPOSITORY"), "/releases/latest"); 3 | 4 | /// Spawns another thread to check if Autorun is up to date. 5 | pub fn check() { 6 | info!("Checking if Autorun is up to date..."); 7 | std::thread::spawn(|| { 8 | let req = tinyget::get(RELEASES_ENDPOINT).with_timeout(5); 9 | 10 | if let Ok(x) = req.send() { 11 | match x.as_str() { 12 | Err(why) => error!("Failed to get latest release: {why}"), 13 | Ok(raw) => { 14 | // String in the form of major.minor.patch-beta(n) where -beta(n) is optional. 15 | // We want to break apart the major, minor and patch parts, and parse as integers. 16 | const VERSION_PHRASE: &str = "Autorun-rs v"; 17 | if let Some(start) = raw.find(VERSION_PHRASE) { 18 | let pos = start + VERSION_PHRASE.len(); 19 | let after = &raw[pos..pos + 12]; // Don't think versions will get very long. 20 | 21 | fn parse_semver(s: &str) -> Option<(u8, u8, u8, Option)> { 22 | let mut s = s; 23 | 24 | let end = s.find('.')?; 25 | let major = &s[..end].parse::().ok()?; 26 | s = &s[end + 1..]; 27 | 28 | let end = s.find('.')?; 29 | let minor = &s[..end].parse::().ok()?; 30 | s = &s[end + 1..]; 31 | 32 | let end = s.find(|x: char| !x.is_numeric()).unwrap_or(s.len()); 33 | let patch = &s[..end].parse::().ok()?; 34 | 35 | let beta = if end <= s.len() { 36 | s = &s[end + 1..]; 37 | if s.contains("-beta") { 38 | let end = s.find(|x: char| !x.is_numeric())?; 39 | if let Ok(n) = &s[end + 1..].parse::() { 40 | Some(*n) 41 | } else { 42 | Some(0) 43 | } 44 | } else { 45 | None 46 | } 47 | } else { 48 | None 49 | }; 50 | 51 | Some((*major, *minor, *patch, beta)) 52 | } 53 | 54 | if let Some(latest_version) = parse_semver(after) { 55 | let semver = format!("{} ", env!("CARGO_PKG_VERSION")); 56 | if let Some(local_version) = parse_semver(&semver) { 57 | // Compare latest_version and local_version and see if local_version is outdated 58 | let outdated = { 59 | // 2.0.3 vs 3.1.2 60 | local_version.0 < latest_version.0 || 61 | 62 | // 1.3.2 vs 1.4.0 63 | local_version.1 < latest_version.1 || 64 | 65 | // 1.2.3 vs 1.2.4 66 | local_version.2 < latest_version.2 || 67 | 68 | // 1.2.3-beta vs 1.2.3 69 | local_version.3.is_some() && latest_version.3.is_none() 70 | }; 71 | if outdated { 72 | info!( 73 | "New update found: v{}.{}.{}! You are on v{}.{}.{}\nUpdate here! {}", 74 | latest_version.0, latest_version.1, latest_version.2, 75 | local_version.0, local_version.1, local_version.2, 76 | 77 | REPO 78 | ) 79 | } else { 80 | info!("Autorun is up to date."); 81 | } 82 | } 83 | } else { 84 | error!("Failed to parse semver in release. Report this on github :("); 85 | } 86 | } else { 87 | error!("Failed to parse latest release data"); 88 | } 89 | } 90 | } 91 | } else { 92 | error!("Failed to check for latest version") 93 | } 94 | }); 95 | } 96 | -------------------------------------------------------------------------------- /examples/autorun.lua: -------------------------------------------------------------------------------- 1 | local ERROR, WARN, INFO, DEBUG, TRACE = 1, 2, 3, 4, 5 2 | Autorun.log( "Connected to server " .. Autorun.IP, DEBUG ) 3 | 4 | -- Change your country flag to North Korea and Operating System to ``Other`` 5 | -- Note this is easy to detect since anticheats will see that the system functions are lua functions, not C ones. 6 | -- (You'd need to detour a lot more to make this undetected.) 7 | jit.os = "Other" 8 | function system.IsLinux() return true end 9 | function system.IsOSX() return false end 10 | function system.IsWindows() return false end 11 | function system.GetCountry() return "KP" end -------------------------------------------------------------------------------- /examples/hook.lua: -------------------------------------------------------------------------------- 1 | -- Replace all 'while true do end' scripts with 'while false do end' 😎 2 | local script = Autorun.CODE 3 | if script:find("while true do end") then 4 | Autorun.log("Found an evil script!") 5 | return string.Replace(script, "while true do end", "while false do end") 6 | end -------------------------------------------------------------------------------- /fields.lua: -------------------------------------------------------------------------------- 1 | -- Emmylua Autorun definition. 2 | -- Feel free to use in your own plugins. 3 | 4 | ---@class Autorun 5 | ---@field Plugin Plugin 6 | ---@field NAME string # Name of script running 7 | ---@field STARTUP boolean # True if script is running on autorun.lua 8 | ---@field CODE string # Source code of script 9 | ---@field CODE_LEN integer # Length of source code 10 | ---@field IP string # IP Address of server 11 | ---@field PATH string # Path to the currently running script, local to /autorun/. Shouldn't really be used (and definitely not modified.) 12 | Autorun = {} 13 | 14 | --- Logs a message to the Autorun console & Logging system (depending on severity) 15 | --- ## Levels 16 | --- * 5 - Trace 17 | --- * 4 - Debug 18 | --- * 3 - Info 19 | --- * 2 - Warning 20 | --- * 1 - Error 21 | --- 22 | --- ## Example 23 | --- Logs a warning to the console 24 | --- ```lua 25 | --- Autorun.log("Restricted access to xyz!", 2) 26 | --- ``` 27 | ---@param message string 28 | ---@param severity integer 29 | function Autorun.log(message, severity) end 30 | 31 | --- Requires a lua file relative to autorun/scripts OR to the currently running Autorun file. 32 | --- So if you do ``Autorun.require("foo.lua")`` inside of YourPlugin/src/autorun.lua, it will call YourPlugin/src/foo.lua. 33 | --- The require'd file will also contain the ``Autorun`` environment and can return a value to be used by the calling script. 34 | --- Pretty much gmod's include() function. 35 | --- ## Example 36 | --- ```lua 37 | --- local Ret = Autorun.require("bar.lua") 38 | --- ``` 39 | ---@param path string Path to file to require 40 | ---@return ... values Any values returned by the require'd file. 41 | function Autorun.require(path) end 42 | 43 | --- Prints any values to the Autorun console, with tables with 3 number values ( {1, 2, 3} ) being treated as colors. 44 | --- All text / values after these colors will be printed in the color specified. 45 | --- Pretty much glua's MsgC but adds a newline as well. 46 | ---@vararg string|{[1]: number, [2]: number, [3]: number}|number|userdata|lightuserdata 47 | function Autorun.print(...) end 48 | 49 | --- Requires a dynamic link library (.dll) from your autorun/bin folder. 50 | --- Make sure the DLLs are named correctly (e.g. gmcl_name_win.dll) 51 | --- ```lua 52 | --- local mybin = Autorun.requirebin("CHTTP") 53 | --- ``` 54 | ---@param path string Path to binary module 55 | ---@return ... values Any values returned by the binary module 56 | function Autorun.requirebin(path) end 57 | 58 | --- Reads a file relative to current path, OR inside of your plugin's /data/ folder. 59 | --- It can have any file extension, so you could read anything from .txt to .json, .lua, .exe, whatever. 60 | --- ```lua 61 | --- local data = Autorun.readFile("test.txt") -- (Reads autorun/plugins/MyPlugin/data/test.txt OR autorun/plugins/MyPlugin/src/test.txt) 62 | --- ``` 63 | ---@param path string Path to file 64 | ---@return string contents Contents of the file 65 | function Autorun.readFile(path) end 66 | 67 | ---@class Plugin 68 | ---@field Settings table # Key value pairs settings retrieved from plugin.toml 69 | ---@field VERSION string # Version of the plugin 70 | ---@field AUTHOR string # Author of the plugin 71 | ---@field NAME string # Display name of the plugin 72 | ---@field DESCRIPTION string # Description of the plugin 73 | ---@field DIR string # Plugin's directory name (non-display name) -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | hard_tabs = true --------------------------------------------------------------------------------