├── .cargo └── config.toml ├── .editorconfig ├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ └── docs.yml ├── .gitignore ├── .gitmodules ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── boneless ├── boneless.cmd ├── build.rs ├── dalamud ├── Channel.cs ├── DalamudPackager.targets ├── Grebuloff.Dalamud.csproj ├── Messages │ └── Hello.cs ├── Plugin.cs ├── grebuloff-dalamud.sln └── packages.lock.json ├── docs ├── .gitignore ├── README.md ├── babel.config.js ├── custom.css ├── docs │ ├── architecture │ │ ├── _category_.yml │ │ ├── dalamud.md │ │ ├── hlrt.md │ │ ├── index.md │ │ ├── injector.md │ │ ├── libhlrt.md │ │ ├── llrt.md │ │ └── ui.md │ ├── credits.md │ ├── getting-started.md │ └── index.md ├── docusaurus.config.js ├── package.json ├── sidebars.js ├── static │ ├── .nojekyll │ ├── CNAME │ └── img │ │ ├── grebuloff-icon.jpg │ │ └── grebuloff-social-card.jpg └── tsconfig.json ├── hlrt ├── .eslintignore ├── .eslintrc.cjs ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc.json ├── Cargo.toml ├── electron.vite.config.ts ├── package.json ├── patches │ └── build-if-changed@1.5.5.patch ├── pnpm-lock.yaml ├── src │ ├── main │ │ ├── index.ts │ │ ├── native │ │ │ ├── index.ts │ │ │ └── lib.rs │ │ ├── paint.ts │ │ └── rpc │ │ │ ├── client.ts │ │ │ ├── codec.ts │ │ │ └── messages.ts │ ├── preload │ │ └── index.ts │ └── renderer │ │ ├── index.html │ │ └── src │ │ ├── App.tsx │ │ ├── assets │ │ ├── App.css │ │ └── index.css │ │ ├── env.d.ts │ │ └── main.tsx ├── tsconfig.json ├── tsconfig.node.json └── tsconfig.web.json ├── injector ├── Cargo.toml └── src │ └── main.rs ├── loader ├── Cargo.toml ├── build.rs └── src │ └── lib.rs ├── macros ├── Cargo.toml └── src │ └── lib.rs ├── rpc ├── Cargo.toml └── src │ ├── lib.rs │ └── ui.rs ├── rust-toolchain.toml └── src ├── dalamud.rs ├── hooking ├── framework.rs ├── mod.rs ├── shaders │ ├── common.hlsli │ ├── ps.cso │ ├── ps.hlsl │ ├── vs.cso │ └── vs.hlsl ├── swapchain.rs └── wndproc.rs ├── lib.rs ├── resolvers ├── dalamud.rs ├── mod.rs └── native.rs ├── rpc ├── mod.rs └── ui.rs └── ui.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [alias] 2 | inject = "run -p grebuloff-injector inject" 3 | launch = "run -p grebuloff-injector launch" 4 | 5 | [build] 6 | target = "x86_64-pc-windows-msvc" 7 | out-dir = "dist" 8 | 9 | [unstable] 10 | unstable-options = true 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | end_of_line = lf 10 | 11 | [*.{js,jsx,cjs,mjs,ts,tsx,cts,mts,json,yml,yaml}] 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: avafloww 2 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy documentation to GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: 7 | - 'docs/**' 8 | - '.github/workflows/docs.yml' 9 | 10 | # Allows you to run this workflow manually from the Actions tab 11 | workflow_dispatch: 12 | 13 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 14 | permissions: 15 | contents: read 16 | pages: write 17 | id-token: write 18 | 19 | # Allow one concurrent deployment 20 | concurrency: 21 | group: "pages" 22 | cancel-in-progress: true 23 | 24 | jobs: 25 | # Single deploy job since we're just deploying 26 | deploy: 27 | environment: 28 | name: github-pages 29 | url: ${{ steps.deployment.outputs.page_url }} 30 | runs-on: ubuntu-latest 31 | defaults: 32 | run: 33 | working-directory: docs 34 | steps: 35 | - name: Checkout 36 | uses: actions/checkout@v3 37 | - name: Set up pnpm 38 | uses: pnpm/action-setup@v2 39 | - name: Set up Node.js 40 | uses: actions/setup-node@v3 41 | with: 42 | node-version: 18.x 43 | cache: pnpm 44 | - name: Install dependencies 45 | run: pnpm install --frozen-lockfile 46 | - name: Build 47 | run: pnpm run build 48 | - name: Setup Pages 49 | uses: actions/configure-pages@v3 50 | - name: Upload artifact 51 | uses: actions/upload-pages-artifact@v1 52 | with: 53 | path: 'docs/build' 54 | - name: Deploy to GitHub Pages 55 | id: deployment 56 | uses: actions/deploy-pages@v2 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bonelessrc.json 2 | 3 | deps/cef_binary* 4 | deps/cef-sys/cef_binary* 5 | local/ 6 | dist/ 7 | 8 | # Rust 9 | target/ 10 | 11 | # IntelliJ 12 | .idea/ 13 | *.iml 14 | 15 | # VSCode 16 | .vscode/ 17 | 18 | # macOS 19 | .DS_Store 20 | ._.* 21 | 22 | # .NET 23 | obj/ 24 | 25 | # Logs 26 | logs 27 | *.log 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | lerna-debug.log* 32 | .pnpm-debug.log* 33 | grebuloff*.log 34 | 35 | # Dependency directories 36 | node_modules/ 37 | 38 | # TypeScript cache 39 | *.tsbuildinfo 40 | 41 | # Optional npm cache directory 42 | .npm 43 | 44 | # Optional eslint cache 45 | .eslintcache 46 | 47 | # dotenv environment variable files 48 | .env 49 | .env.development.local 50 | .env.test.local 51 | .env.production.local 52 | .env.local 53 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "deps/FFXIVClientStructs"] 2 | path = deps/FFXIVClientStructs 3 | url = https://github.com/avafloww/FFXIVClientStructs.git 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | ######## 2 | # LLRT # 3 | ######## 4 | [package] 5 | name = "grebuloff-llrt" 6 | version = "0.1.0" 7 | edition = "2021" 8 | build = "build.rs" 9 | 10 | [lib] 11 | crate-type = ["cdylib"] 12 | 13 | [dependencies] 14 | log = { workspace = true } 15 | fern = { workspace = true } 16 | chrono = { workspace = true } 17 | tokio = { workspace = true } 18 | windows = { workspace = true } 19 | anyhow = { workspace = true } 20 | rmp = { workspace = true } 21 | rmp-serde = { workspace = true } 22 | serde = { workspace = true } 23 | serde_json = { workspace = true } 24 | bytes = { workspace = true } 25 | dll-syringe = { workspace = true, features = ["payload-utils"] } 26 | grebuloff-macros = { path = "macros" } 27 | grebuloff-rpc = { path = "rpc" } 28 | ffxiv_client_structs = { path = "deps/FFXIVClientStructs/rust/lib", features = ["async-resolution"] } 29 | ffxiv_client_structs_macros = { path = "deps/FFXIVClientStructs/rust/macros" } 30 | msgbox = "0.7.0" 31 | # deno_core = "0.191.0" 32 | # deno_ast = { version = "0.27.1", features = ["transpiling"] } 33 | inventory = "0.3.6" 34 | itertools = "0.10.5" 35 | rustc-hash = "1.1.0" 36 | retour = { version = "0.3.0", features = ["static-detour"] } 37 | uuid = { version = "1.4.0", features = ["v4", "fast-rng"] } 38 | async-trait = "0.1.71" 39 | 40 | [build-dependencies] 41 | chrono = { workspace = true } 42 | 43 | ############# 44 | # Workspace # 45 | ############# 46 | [workspace] 47 | members = [".", "macros", "injector", "loader", "rpc", "hlrt"] 48 | default-members = [".", "injector", "loader", "rpc", "hlrt"] 49 | 50 | [workspace.dependencies] 51 | dll-syringe = { version = "0.15.2", default-features = false } 52 | tokio = { version = "1.28.2", features = ["full"] } 53 | anyhow = { version = "1.0.71" } 54 | log = { version = "0.4" } 55 | fern = { version = "0.6.2" } 56 | chrono = { version = "0.4.26" } 57 | rmp = "0.8.11" 58 | rmp-serde = "1.1.1" 59 | serde = { version = "1.0.155", features = ["derive"] } 60 | serde_json = "1.0.96" 61 | bytes = "1.4.0" 62 | 63 | [workspace.dependencies.windows] 64 | version = "0.48.0" 65 | features = [ 66 | "Win32_Foundation", 67 | "Win32_Security", 68 | "Win32_System_Threading", 69 | "Win32_Security_Authorization", 70 | "Win32_UI_WindowsAndMessaging", 71 | "Win32_System_LibraryLoader", 72 | "Win32_System_ProcessStatus", 73 | "Win32_System_SystemServices", 74 | "Win32_Graphics_Gdi", 75 | "Win32_Graphics_Dxgi", 76 | "Win32_Graphics_Dxgi_Common", 77 | "Win32_Graphics_Direct3D11", 78 | "Win32_Graphics_Direct3D" 79 | ] 80 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: / 3 | sidebar_position: 1 4 | --- 5 | 6 | # Introduction 7 | 8 | Grebuloff is an experimental addon framework for Final Fantasy XIV. It introduces a new concept of what 9 | plugins can be, focusing on enabling creation of addons that are isolated, secure, stable, and add onto the vanilla 10 | game in an incremental fashion. 11 | 12 | The core of Grebuloff is built in Rust and TypeScript. Addons, while typically written in JavaScript or 13 | TypeScript, can be developed using any technology that can run on the V8 engine, including WebAssembly. 14 | 15 | ## How does Grebuloff relate to Dalamud? 16 | 17 | > Grebuloff is currently in a very early stage of development. If you are a new community developer looking 18 | > to make the Next Big Plugin, or an end-user looking for a wide ecosystem of addons for the game, 19 | > **you should use XIVLauncher & Dalamud**. 20 | 21 | **Grebuloff is _not_ a replacement for Dalamud.** These projects have entirely different design philosophies. 22 | Grebuloff can even run alongside Dalamud using a helper plugin, allowing you to use both frameworks at the 23 | same time; however, this feature, like everything else, is highly experimental. 24 | 25 | Dalamud plugins are able to extensively alter a running game, thanks to an extensive API and, where its API 26 | falls short, the ability to hook game functions and directly modify memory. However, this often can come 27 | at the cost of stability (especially during game patches) and security, as plugins have unscoped, unsandboxed 28 | access to your game and your computer. 29 | 30 | Grebuloff is intended to offer a safer, more isolated framework for addons. All addons run in an isolated 31 | V8 context, and only have access to the APIs they have explicitly requested and been granted access to. 32 | 33 | It's important to note that, since third-party tools are against Square Enix's Terms of Service, use of either 34 | Grebuloff or Dalamud carries risks of penalties to your account. Although both projects make efforts to mitigate 35 | this risk, the responsibility of account safety ultimately falls upon the user. 36 | 37 | ## License 38 | 39 | Grebuloff is licensed under LGPL-3.0. 40 | [Please refer to the `LICENSE` file for more details.](https://github.com/avafloww/Grebuloff/blob/main/LICENSE) 41 | 42 | Dependencies are licensed under their project's respective licenses. 43 | -------------------------------------------------------------------------------- /boneless: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | cd "$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 3 | pnpm boneless $@ 4 | -------------------------------------------------------------------------------- /boneless.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | pnpm boneless %* 3 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, process::Command}; 2 | 3 | struct Meta; 4 | impl Meta { 5 | fn version() { 6 | let out = Command::new("git") 7 | .arg("describe") 8 | .arg("--always") 9 | .arg("--dirty") 10 | .output() 11 | .unwrap(); 12 | println!( 13 | "cargo:rustc-env=GIT_DESCRIBE={}", 14 | String::from_utf8(out.stdout).unwrap() 15 | ); 16 | } 17 | 18 | fn timestamp() { 19 | let timestamp = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true); 20 | println!("cargo:rustc-env=BUILD_TIMESTAMP={}", timestamp); 21 | } 22 | } 23 | 24 | struct Build; 25 | impl Build { 26 | fn build_hlrt() { 27 | // only run `pnpm install` if hlrt/node_modules is absent 28 | if !std::path::Path::new("hlrt").join("node_modules").exists() { 29 | Command::new("cmd") 30 | .arg("/C") 31 | .arg("pnpm") 32 | .arg("install") 33 | .current_dir("hlrt") 34 | .spawn() 35 | .expect("failed to run `pnpm install` for HLRT"); 36 | } 37 | 38 | Command::new("cmd") 39 | .arg("/C") 40 | .arg("pnpm") 41 | .arg("maybe-build:js") 42 | .current_dir("hlrt") 43 | .spawn() 44 | .expect("failed to run `pnpm maybe-build:js` for HLRT"); 45 | } 46 | } 47 | 48 | fn main() -> Result<(), Box> { 49 | Meta::version(); 50 | Meta::timestamp(); 51 | 52 | Build::build_hlrt(); 53 | 54 | Ok(()) 55 | } 56 | -------------------------------------------------------------------------------- /dalamud/Channel.cs: -------------------------------------------------------------------------------- 1 | using System.IO.Pipes; 2 | 3 | namespace Grebuloff.Dalamud; 4 | 5 | public class Channel : IDisposable 6 | { 7 | public string Name { get; } 8 | private readonly CancellationTokenSource _cts = new(); 9 | private readonly NamedPipeServerStream _server; 10 | private StreamWriter? _writer; 11 | private StreamReader? _reader; 12 | 13 | public Channel() 14 | { 15 | // generate a random UUID 16 | Name = $"grebuloff-dalamud-{Guid.NewGuid()}"; 17 | _server = new NamedPipeServerStream( 18 | Name, 19 | PipeDirection.InOut, 20 | 1, 21 | PipeTransmissionMode.Message, 22 | PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly 23 | ); 24 | } 25 | 26 | private async void AwaitConnection() 27 | { 28 | await _server.WaitForConnectionAsync(_cts.Token); 29 | _writer = new StreamWriter(_server); 30 | _reader = new StreamReader(_server); 31 | } 32 | 33 | public void Dispose() 34 | { 35 | _cts.Cancel(); 36 | _reader?.Dispose(); 37 | _writer?.Dispose(); 38 | _server.Dispose(); 39 | } 40 | } -------------------------------------------------------------------------------- /dalamud/DalamudPackager.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /dalamud/Grebuloff.Dalamud.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Grebuloff Contributors 5 | 1.0.0 6 | Dalamud helper plugin to allow Grebuloff and Dalamud to play nicely together. 7 | https://github.com/avafloww/Grebuloff 8 | 9 | 10 | 11 | net7.0-windows 12 | enable 13 | enable 14 | true 15 | false 16 | false 17 | true 18 | true 19 | 20 | 21 | 22 | $(appdata)\XIVLauncher\addon\Hooks\dev\ 23 | 24 | 25 | 26 | 27 | build 28 | 29 | 30 | 31 | $(DalamudLibPath)FFXIVClientStructs.dll 32 | false 33 | compile 34 | 35 | 36 | $(DalamudLibPath)Newtonsoft.Json.dll 37 | false 38 | compile 39 | 40 | 41 | $(DalamudLibPath)Dalamud.dll 42 | false 43 | compile 44 | 45 | 46 | $(DalamudLibPath)ImGui.NET.dll 47 | false 48 | compile 49 | 50 | 51 | $(DalamudLibPath)ImGuiScene.dll 52 | false 53 | compile 54 | 55 | 56 | $(DalamudLibPath)Lumina.dll 57 | false 58 | compile 59 | 60 | 61 | $(DalamudLibPath)Lumina.Excel.dll 62 | false 63 | compile 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /dalamud/Messages/Hello.cs: -------------------------------------------------------------------------------- 1 | using MessagePack; 2 | 3 | namespace Grebuloff.Dalamud.Messages; 4 | 5 | [MessagePackObject] 6 | public class Hello 7 | { 8 | 9 | } -------------------------------------------------------------------------------- /dalamud/Plugin.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Game; 2 | using Dalamud.Plugin; 3 | 4 | namespace Grebuloff.Dalamud; 5 | 6 | public sealed class Plugin : IDalamudPlugin 7 | { 8 | public string Name => "Grebuloff Compatibility Layer"; 9 | 10 | public DalamudPluginInterface PluginInterface { get; private set; } 11 | public Framework Framework { get; private set; } 12 | 13 | public Plugin(DalamudPluginInterface pluginInterface, Framework framework) 14 | { 15 | PluginInterface = pluginInterface; 16 | Framework = framework; 17 | 18 | PluginInterface.UiBuilder.Draw += this.DrawUI; 19 | Framework.Update += OnFrameworkUpdate; 20 | } 21 | 22 | private void OnFrameworkUpdate(Framework framework) 23 | { 24 | } 25 | 26 | private void DrawUI() 27 | { 28 | 29 | } 30 | 31 | public void Dispose() 32 | { 33 | Framework.Update -= OnFrameworkUpdate; 34 | PluginInterface.UiBuilder.Draw -= this.DrawUI; 35 | } 36 | } -------------------------------------------------------------------------------- /dalamud/grebuloff-dalamud.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Grebuloff.Dalamud", "Grebuloff.Dalamud.csproj", "{726E199D-0E6E-4838-A595-222CC63D49F4}" 4 | EndProject 5 | Global 6 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 7 | Debug|Any CPU = Debug|Any CPU 8 | Release|Any CPU = Release|Any CPU 9 | EndGlobalSection 10 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 11 | {726E199D-0E6E-4838-A595-222CC63D49F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 12 | {726E199D-0E6E-4838-A595-222CC63D49F4}.Debug|Any CPU.Build.0 = Debug|Any CPU 13 | {726E199D-0E6E-4838-A595-222CC63D49F4}.Release|Any CPU.ActiveCfg = Release|Any CPU 14 | {726E199D-0E6E-4838-A595-222CC63D49F4}.Release|Any CPU.Build.0 = Release|Any CPU 15 | EndGlobalSection 16 | EndGlobal 17 | -------------------------------------------------------------------------------- /dalamud/packages.lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "dependencies": { 4 | "net7.0-windows7.0": { 5 | "DalamudPackager": { 6 | "type": "Direct", 7 | "requested": "[2.1.10, )", 8 | "resolved": "2.1.10", 9 | "contentHash": "S6NrvvOnLgT4GDdgwuKVJjbFo+8ZEj+JsEYk9ojjOR/MMfv1dIFpT8aRJQfI24rtDcw1uF+GnSSMN4WW1yt7fw==" 10 | }, 11 | "MessagePack": { 12 | "type": "Direct", 13 | "requested": "[2.6.100-alpha, )", 14 | "resolved": "2.6.100-alpha", 15 | "contentHash": "gAyxBDzMDLWVg8GGP3YS4M2WnpglwcZzEld9HEsqVYNsTtVELZidINr/TTxVyfnHXxyEiZy04czZDdq8Q02hQw==", 16 | "dependencies": { 17 | "MessagePack.Annotations": "2.6.100-alpha", 18 | "Microsoft.NET.StringTools": "17.4.0", 19 | "System.Runtime.CompilerServices.Unsafe": "6.0.0" 20 | } 21 | }, 22 | "MessagePack.Annotations": { 23 | "type": "Transitive", 24 | "resolved": "2.6.100-alpha", 25 | "contentHash": "hl8OTk87/i4nMHtBasV+GRQe8g3tze3l6NGYmL4XOQwCATKLAFAIKRbaxxWAPo0wk26UPqJ7oC+Lg8fLQNWLAQ==" 26 | }, 27 | "Microsoft.NET.StringTools": { 28 | "type": "Transitive", 29 | "resolved": "17.4.0", 30 | "contentHash": "06T6Hqfs3JDIaBvJaBRFFMIdU7oE0OMab5Xl8LKQjWPxBQr3BgVFKMQPTC+GsSEuYREWmK6g5eOd7Xqd9p1YCA==", 31 | "dependencies": { 32 | "System.Memory": "4.5.5", 33 | "System.Runtime.CompilerServices.Unsafe": "6.0.0" 34 | } 35 | }, 36 | "System.Memory": { 37 | "type": "Transitive", 38 | "resolved": "4.5.5", 39 | "contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==" 40 | }, 41 | "System.Runtime.CompilerServices.Unsafe": { 42 | "type": "Transitive", 43 | "resolved": "6.0.0", 44 | "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" 45 | } 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator. 4 | 5 | ### Installation 6 | 7 | ``` 8 | $ pnpm 9 | ``` 10 | 11 | ### Local Development 12 | 13 | ``` 14 | $ pnpm start 15 | ``` 16 | 17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ### Build 20 | 21 | ``` 22 | $ pnpm build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ### Deployment 28 | 29 | Using SSH: 30 | 31 | ``` 32 | $ USE_SSH=true pnpm deploy 33 | ``` 34 | 35 | Not using SSH: 36 | 37 | ``` 38 | $ GIT_USER= pnpm deploy 39 | ``` 40 | 41 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 42 | -------------------------------------------------------------------------------- /docs/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /docs/custom.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Any CSS included here will be global. The classic template 3 | * bundles Infima by default. Infima is a CSS framework designed to 4 | * work well for content-centric websites. 5 | */ 6 | 7 | /* You can override the default Infima variables here. */ 8 | :root { 9 | --ifm-color-primary: #2e8555; 10 | --ifm-color-primary-dark: #29784c; 11 | --ifm-color-primary-darker: #277148; 12 | --ifm-color-primary-darkest: #205d3b; 13 | --ifm-color-primary-light: #33925d; 14 | --ifm-color-primary-lighter: #359962; 15 | --ifm-color-primary-lightest: #3cad6e; 16 | --ifm-code-font-size: 95%; 17 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); 18 | } 19 | 20 | /* For readability concerns, you should choose a lighter palette in dark mode. */ 21 | [data-theme='dark'] { 22 | --ifm-color-primary: #25c2a0; 23 | --ifm-color-primary-dark: #21af90; 24 | --ifm-color-primary-darker: #1fa588; 25 | --ifm-color-primary-darkest: #1a8870; 26 | --ifm-color-primary-light: #29d5b0; 27 | --ifm-color-primary-lighter: #32d8b4; 28 | --ifm-color-primary-lightest: #4fddbf; 29 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); 30 | } 31 | -------------------------------------------------------------------------------- /docs/docs/architecture/_category_.yml: -------------------------------------------------------------------------------- 1 | label: Architecture 2 | position: 3 3 | -------------------------------------------------------------------------------- /docs/docs/architecture/dalamud.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 6 3 | --- 4 | 5 | # Dalamud Support Plugin 6 | 7 | > **Language:** C# 8 | 9 | The Dalamud Support Plugin is a Dalamud plugin that allows Grebuloff and Dalamud 10 | to run simultaneously. When the plugin is loaded by Dalamud, it will inject the 11 | [low-level runtime](/architecture/llrt) into the game process, starting the 12 | process of bootstrapping Grebuloff. 13 | 14 | When Grebuloff has been loaded through the support plugin, as opposed to through 15 | the [injector](/architecture/injector), function hooks will be handled through 16 | Dalamud, to avoid conflicts with other plugins. 17 | -------------------------------------------------------------------------------- /docs/docs/architecture/hlrt.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 4 3 | --- 4 | 5 | # High-Level Runtime (HLRT) 6 | 7 | > **Language:** TypeScript 8 | 9 | The High-Level Runtime (HLRT) is the core of Grebuloff. It runs in a privileged 10 | V8 isolate, and is responsible for much of Grebuloff's core functionality, 11 | including plugin management. 12 | 13 | HLRT communicates with the [low-level runtime](/architecture/llrt) to handle 14 | game communications, and with the [UI](/architecture/ui) to provide UI services 15 | to addons. 16 | -------------------------------------------------------------------------------- /docs/docs/architecture/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | # Architecture 6 | 7 | The chart below offers an overview of the general architecture of Grebuloff. 8 | You can learn more about each component by checking out the pages for each 9 | component in the sidebar. 10 | 11 | ```mermaid 12 | flowchart LR 13 | injector[" 14 | fab:fa-rust Injector 15 | grebuloff_injector.exe 16 | "] 17 | 18 | injector -- injects --> llrt 19 | web <-. IPC -.-> game 20 | 21 | subgraph game["ffxiv_dx11.exe"] 22 | llrt["Low-Level Runtime (LLRT)"] 23 | 24 | llrt <-. named pipe .-> dalamud 25 | dalamud["fas:fa-meteor Dalamud Support Plugin"] 26 | 27 | llrt -- bootstraps --> hlrt 28 | 29 | subgraph v8["fab:fa-js V8 JavaScript Engine"] 30 | libhlrt["fas:fa-book-open High-Level Runtime Library (libhlrt)"] 31 | 32 | subgraph v8_priv["Privileged/HLRT Isolate"] 33 | hlrt["fab:fa-js High-Level Runtime (HLRT)"] 34 | libhlrt["fas:fa-book-open High-Level Runtime Library (libhlrt)"] 35 | 36 | hlrt --> libhlrt 37 | end 38 | 39 | subgraph v8_unpriv_1["Unprivileged Isolate #1"] 40 | addon_1["fab:fa-js User Add-on/Script"] 41 | libhlrt_1["fas:fa-book-open High-Level Runtime Library (libhlrt)"] 42 | 43 | addon_1 --> libhlrt_1 44 | end 45 | 46 | subgraph v8_unpriv_n["Unprivileged Isolate #n"] 47 | addon_n["fab:fa-js User Add-on/Script"] 48 | libhlrt_n["fas:fa-book-open High-Level Runtime Library (libhlrt)"] 49 | 50 | addon_n --> libhlrt_n 51 | end 52 | end 53 | 54 | llrt <-. FFI -.-> v8 55 | end 56 | 57 | subgraph web["fas:fa-globe WebView2 Process"] 58 | ui["fab:fa-react User Interface (UI)"] 59 | end 60 | 61 | llrt <--> ui 62 | ``` 63 | -------------------------------------------------------------------------------- /docs/docs/architecture/injector.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # Injector 6 | 7 | > **Language:** Rust 8 | 9 | The injector injects the [low-level runtime](/architecture/llrt) into the game process. 10 | It is one of the two supported ways to load Grebuloff, the other being through 11 | the [Dalamud support plugin](/architecture/dalamud). 12 | -------------------------------------------------------------------------------- /docs/docs/architecture/libhlrt.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | # High-Level Runtime Library (libhlrt) 6 | 7 | > **Language:** TypeScript 8 | 9 | The High-Level Runtime Library (libhlrt) is the core JavaScript library that is 10 | present in every V8 isolate. It provides the core functionality (such as logging) 11 | that is used by the [high-level runtime](/architecture/hlrt) and user scripts. 12 | -------------------------------------------------------------------------------- /docs/docs/architecture/llrt.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | # Low-Level Runtime (LLRT) 6 | 7 | > **Language:** Rust 8 | 9 | The Low-Level Runtime (LLRT) is the entrypoint of Grebuloff to the game process. 10 | It is responsible for loading the [High-Level Runtime](/architecture/hlrt) into 11 | the game process. 12 | -------------------------------------------------------------------------------- /docs/docs/architecture/ui.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 5 3 | --- 4 | 5 | # User Interface (UI) 6 | 7 | > **Language:** TypeScript 8 | > 9 | > **Framework:** React 10 | 11 | The user interface of Grebuloff runs in the WebView2 instance that is spawned by 12 | the [low-level runtime](/architecture/llrt). It is responsible for rendering the 13 | UI of [high-level runtime](/architecture/hlrt) and any user add-ons. 14 | -------------------------------------------------------------------------------- /docs/docs/credits.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 100 3 | --- 4 | 5 | # Credits & Acknowledgements 6 | 7 | Without the work of these people and groups, this project would not be possible. 8 | 9 | Thanks to: 10 | 11 | - [The contributors to Grebuloff](https://github.com/avafloww/Grebuloff/graphs/contributors) 12 | - [goat](https://github.com/goaaats/) and all of the folks at [@goatcorp](https://github.com/goatcorp), for 13 | their tireless work on creating Dalamud & XIVLauncher, the projects that changed the game and inspired us all 14 | - [aers](https://github.com/aers), [Pohky](https://github.com/Pohky), [Caraxi](https://github.com/Caraxi), 15 | [daemitus](https://github.com/daemitus), 16 | and [all of the contributors](https://github.com/aers/FFXIVClientStructs/graphs/contributors) 17 | to [FFXIVClientStructs](https://github.com/aers/FFXIVClientStructs), for their extensive research into the 18 | game's internals 19 | - The community developers at [goat place](https://goat.place), also for their extensive research into the 20 | game's internals, as well as for entertaining my constant memery 21 | - [Deno](https://github.com/denoland/deno) and [MiniV8](https://github.com/SkylerLipthay/mini-v8) for 22 | providing excellent examples and code to embed V8 in Rust 23 | - Square Enix, for creating the critically acclaimed game that we all know and love 24 | -------------------------------------------------------------------------------- /docs/docs/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | # Getting Started 6 | 7 | Grebuloff is currently in a very early stage of development, and is not yet ready for general use. However, if you are 8 | interested in contributing to the project, or just want to try it out, this guide will help you get started. 9 | 10 | ## System Requirements 11 | 12 | :::info 13 | Rust nightly is required due to the use of the [dll-syringe](https://crates.io/crates/dll-syringe) crate, 14 | which depends on unstable Rust features. 15 | ::: 16 | 17 | :::note 18 | Rust nightly versions newer than `nightly-2023-06-02` currently fail to build Grebuloff due to an 19 | [issue](https://github.com/denoland/rusty_v8/issues/1248) in the V8 bindings that Grebuloff uses. 20 | 21 | This issue will be resolved once a new version of the V8 bindings is released on crates.io. 22 | ::: 23 | 24 | - [Node.js](https://nodejs.org/) 16+ 25 | - Use of the latest LTS version is recommended. 26 | - [pnpm](https://pnpm.io/) is also required. 27 | - [Rust](https://www.rust-lang.org/) 1.72.0-nightly-2023-06-02 28 | - Install using ```rustup toolchain install nightly-2023-06-02``` 29 | - [Visual Studio 2022](https://visualstudio.microsoft.com/vs/) (with C++ and .NET Desktop Development Workloads) 30 | 31 | ## Boneless 32 | 33 | Grebuloff uses a custom build script called Boneless. Boneless is a purpose-built (read: fully jank) build system 34 | that is designed to pull together all of the different moving parts of Grebuloff into a single build process. 35 | It's not pretty, but it works (most of the time). 36 | 37 | ### Building 38 | 39 | :::tip 40 | The command examples in this documentation assume you are running from a POSIX-like environment 41 | on Windows, such as Git Bash. If you aren't, you may need to replace forward slashes with backslashes, 42 | i.e. `./boneless` becomes `.\boneless`. 43 | ::: 44 | 45 | Boneless is exposed as a CLI tool in the root of the repository. To build all of the required components of Grebuloff, 46 | run the following command: 47 | 48 | ```shell 49 | $ ./boneless build 50 | Found rustc 1.72.0-nightly 51 | Found .NET 7.0.302 52 | 53 | (...) 54 | ``` 55 | 56 | Boneless will check your build environment for the required tools and dependencies, and will build all of the 57 | required components of Grebuloff. This process can take a while, especially the first time you run it. 58 | 59 | :::caution 60 | Building Grebuloff requires several gigabytes of free disk space. It is also recommended to have a fast 61 | internet connection, as several heavy dependencies, including a prebuilt copy of V8, will be downloaded. 62 | ::: 63 | 64 | ### Running 65 | 66 | Once the build process has completed, you can fake-launch the game and inject Grebuloff into it using the 67 | `launch` command: 68 | 69 | ```shell 70 | ./boneless launch 71 | ``` 72 | 73 | Note that, on the first launch, you will need to set your game path before running this command. 74 | You can set your game path using the `set-path` command: 75 | 76 | ```shell 77 | ./boneless set-path "/path/to/FINAL FANTASY XIV - A Realm Reborn/game/ffxiv_dx11.exe" 78 | ``` 79 | 80 | If all goes well, the game will launch, and... nothing will appear to happen. That's a good thing! 81 | Check for the existence of a `grebuloff.log` file in the `build` directory. If it exists, Grebuloff 82 | is running in your game! 83 | -------------------------------------------------------------------------------- /docs/docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: / 3 | sidebar_position: 1 4 | --- 5 | 6 | # Introduction 7 | 8 | Grebuloff is an experimental addon framework for Final Fantasy XIV. It introduces a new concept of what 9 | plugins can be, focusing on enabling creation of addons that are isolated, secure, stable, and add onto the vanilla 10 | game in an incremental fashion. 11 | 12 | The core of Grebuloff is built in Rust and TypeScript. Addons, while typically written in JavaScript or 13 | TypeScript, can be developed using any technology that can run on the V8 engine, including WebAssembly. 14 | 15 | ## How does Grebuloff relate to Dalamud? 16 | 17 | > Grebuloff is currently in a very early stage of development. If you are a new community developer looking 18 | > to make the Next Big Plugin, or an end-user looking for a wide ecosystem of addons for the game, 19 | > **you should use XIVLauncher & Dalamud**. 20 | 21 | **Grebuloff is _not_ a replacement for Dalamud.** These projects have entirely different design philosophies. 22 | Grebuloff can even run alongside Dalamud using a helper plugin, allowing you to use both frameworks at the 23 | same time; however, this feature, like everything else, is highly experimental. 24 | 25 | Dalamud plugins are able to extensively alter a running game, thanks to an extensive API and, where its API 26 | falls short, the ability to hook game functions and directly modify memory. However, this often can come 27 | at the cost of stability (especially during game patches) and security, as plugins have unscoped, unsandboxed 28 | access to your game and your computer. 29 | 30 | Grebuloff is intended to offer a safer, more isolated framework for addons. All addons run in an isolated 31 | V8 context, and only have access to the APIs they have explicitly requested and been granted access to. 32 | 33 | It's important to note that, since third-party tools are against Square Enix's Terms of Service, use of either 34 | Grebuloff or Dalamud carries risks of penalties to your account. Although both projects make efforts to mitigate 35 | this risk, the responsibility of account safety ultimately falls upon the user. 36 | 37 | ## License 38 | 39 | Grebuloff is licensed under LGPL-3.0. 40 | [Please refer to the `LICENSE` file for more details.](https://github.com/avafloww/Grebuloff/blob/main/LICENSE) 41 | 42 | Dependencies are licensed under their project's respective licenses. 43 | -------------------------------------------------------------------------------- /docs/docusaurus.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Note: type annotations allow type checking and IDEs autocompletion 3 | 4 | const lightCodeTheme = require('prism-react-renderer/themes/github'); 5 | const darkCodeTheme = require('prism-react-renderer/themes/dracula'); 6 | 7 | /** @type {import('@docusaurus/types').Config} */ 8 | const config = { 9 | title: 'Grebuloff', 10 | tagline: 'Grebuloff is an experimental addon framework for Final Fantasy XIV.', 11 | 12 | // Set the production url of your site here 13 | url: 'https://grebuloff.ava.dev', 14 | // Set the // pathname under which your site is served 15 | // For GitHub pages deployment, it is often '//' 16 | baseUrl: '/', 17 | 18 | // GitHub pages deployment config. 19 | // If you aren't using GitHub pages, you don't need these. 20 | organizationName: 'avafloww', // Usually your GitHub org/user name. 21 | projectName: 'Grebuloff', // Usually your repo name. 22 | 23 | onBrokenLinks: 'throw', 24 | onBrokenMarkdownLinks: 'warn', 25 | 26 | // Even if you don't use internalization, you can use this field to set useful 27 | // metadata like html lang. For example, if your site is Chinese, you may want 28 | // to replace "en" with "zh-Hans". 29 | i18n: { 30 | defaultLocale: 'en', 31 | locales: ['en'], 32 | }, 33 | 34 | presets: [ 35 | [ 36 | 'classic', 37 | /** @type {import('@docusaurus/preset-classic').Options} */ 38 | ({ 39 | docs: { 40 | routeBasePath: '/', 41 | sidebarPath: require.resolve('./sidebars.js'), 42 | // Please change this to your repo. 43 | // Remove this to remove the "edit this page" links. 44 | editUrl: 45 | 'https://github.com/avafloww/Grebuloff/tree/main/docs/', 46 | }, 47 | blog: false, 48 | theme: { 49 | customCss: require.resolve('./custom.css'), 50 | }, 51 | }), 52 | ], 53 | ], 54 | 55 | markdown: { 56 | mermaid: true, 57 | }, 58 | 59 | stylesheets: [ 60 | { 61 | href: 'https://use.fontawesome.com/releases/v5.15.4/css/all.css', 62 | integrity: 'sha384-DyZ88mC6Up2uqS4h/KRgHuoeGwBcD4Ng9SiP4dIRy0EXTlnuz47vAwmeGwVChigm', 63 | crossorigin: 'anonymous', 64 | } 65 | ], 66 | 67 | themes: ['@docusaurus/theme-mermaid'], 68 | 69 | themeConfig: 70 | /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ 71 | ({ 72 | // Replace with your project's social card 73 | image: 'img/grebuloff-social-card.jpg', 74 | navbar: { 75 | title: 'Grebuloff', 76 | logo: { 77 | alt: 'observe him', 78 | src: 'img/grebuloff-icon.jpg', 79 | }, 80 | items: [ 81 | { 82 | type: 'docSidebar', 83 | sidebarId: 'main', 84 | position: 'left', 85 | label: 'Documentation', 86 | }, 87 | { 88 | href: 'https://github.com/avafloww/Grebuloff', 89 | label: 'GitHub', 90 | position: 'right', 91 | }, 92 | ], 93 | }, 94 | prism: { 95 | theme: lightCodeTheme, 96 | darkTheme: darkCodeTheme, 97 | }, 98 | colorMode: { 99 | defaultMode: 'dark', 100 | disableSwitch: false, 101 | respectPrefersColorScheme: false, 102 | } 103 | }), 104 | }; 105 | 106 | module.exports = config; 107 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids", 15 | "typecheck": "tsc" 16 | }, 17 | "dependencies": { 18 | "@docusaurus/core": "2.4.1", 19 | "@docusaurus/preset-classic": "2.4.1", 20 | "@docusaurus/theme-mermaid": "^2.4.1", 21 | "@mdx-js/react": "^1.6.22", 22 | "clsx": "^1.2.1", 23 | "prism-react-renderer": "^1.3.5", 24 | "react": "^17.0.2", 25 | "react-dom": "^17.0.2" 26 | }, 27 | "devDependencies": { 28 | "@docusaurus/module-type-aliases": "2.4.1", 29 | "@tsconfig/docusaurus": "^1.0.5", 30 | "typescript": "^4.7.4" 31 | }, 32 | "browserslist": { 33 | "production": [ 34 | ">0.5%", 35 | "not dead", 36 | "not op_mini all" 37 | ], 38 | "development": [ 39 | "last 1 chrome version", 40 | "last 1 firefox version", 41 | "last 1 safari version" 42 | ] 43 | }, 44 | "engines": { 45 | "node": ">=16.14" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /docs/sidebars.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ 4 | const sidebars = { 5 | // By default, Docusaurus generates a sidebar from the docs folder structure 6 | main: [{ type: 'autogenerated', dirName: '.' }], 7 | }; 8 | 9 | module.exports = sidebars; 10 | -------------------------------------------------------------------------------- /docs/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avafloww/Grebuloff/345a8c7988b14b501c4c5c7258c056921e00fa32/docs/static/.nojekyll -------------------------------------------------------------------------------- /docs/static/CNAME: -------------------------------------------------------------------------------- 1 | grebuloff.ava.dev -------------------------------------------------------------------------------- /docs/static/img/grebuloff-icon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avafloww/Grebuloff/345a8c7988b14b501c4c5c7258c056921e00fa32/docs/static/img/grebuloff-icon.jpg -------------------------------------------------------------------------------- /docs/static/img/grebuloff-social-card.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avafloww/Grebuloff/345a8c7988b14b501c4c5c7258c056921e00fa32/docs/static/img/grebuloff-social-card.jpg -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // This file is not used in compilation. It is here just for a nice editor experience. 3 | "extends": "@tsconfig/docusaurus/tsconfig.json", 4 | "compilerOptions": { 5 | "baseUrl": "." 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /hlrt/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | out 3 | dist 4 | build 5 | .gitignore 6 | -------------------------------------------------------------------------------- /hlrt/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | commonjs: true, 6 | es6: true, 7 | node: true, 8 | }, 9 | parser: '@typescript-eslint/parser', 10 | parserOptions: { 11 | ecmaFeatures: { 12 | jsx: true, 13 | }, 14 | sourceType: 'module', 15 | ecmaVersion: 2021, 16 | }, 17 | plugins: ['@typescript-eslint'], 18 | extends: [ 19 | 'eslint:recommended', 20 | 'plugin:react/recommended', 21 | 'plugin:react/jsx-runtime', 22 | 'plugin:@typescript-eslint/recommended', 23 | 'plugin:@typescript-eslint/eslint-recommended', 24 | 'plugin:prettier/recommended', 25 | ], 26 | rules: { 27 | '@typescript-eslint/explicit-module-boundary-types': 'off', 28 | '@typescript-eslint/no-empty-function': [ 29 | 'error', 30 | { allow: ['arrowFunctions'] }, 31 | ], 32 | '@typescript-eslint/no-non-null-assertion': 'off', 33 | '@typescript-eslint/no-var-requires': 'off', 34 | }, 35 | overrides: [ 36 | { 37 | files: ['*.js'], 38 | rules: { 39 | '@typescript-eslint/explicit-function-return-type': 'off', 40 | }, 41 | }, 42 | ], 43 | }; 44 | -------------------------------------------------------------------------------- /hlrt/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | out 4 | *.log* 5 | .bic_cache 6 | electron.vite.config.*.mjs 7 | target 8 | *.node 9 | -------------------------------------------------------------------------------- /hlrt/.npmrc: -------------------------------------------------------------------------------- 1 | # blame Electron 2 | node-linker=hoisted 3 | shamefully-hoist=true 4 | -------------------------------------------------------------------------------- /hlrt/.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | out 3 | node_modules 4 | pnpm-lock.yaml 5 | tsconfig.json 6 | tsconfig.*.json 7 | -------------------------------------------------------------------------------- /hlrt/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "jsxSingleQuote": true, 5 | "trailingComma": "all", 6 | "tabWidth": 2, 7 | "useTabs": false 8 | } -------------------------------------------------------------------------------- /hlrt/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "grebuloff-hlrt-native" 3 | version = "0.1.0" 4 | edition = "2021" 5 | exclude = ["*.node"] 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | path = "src/main/native/lib.rs" 10 | 11 | [dependencies] 12 | anyhow = { workspace = true } 13 | tokio = { workspace = true } 14 | 15 | [dependencies.neon] 16 | version = "0.10" 17 | default-features = false 18 | features = ["napi-6", "promise-api", "task-api", "channel-api"] 19 | -------------------------------------------------------------------------------- /hlrt/electron.vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { defineConfig, externalizeDepsPlugin, swcPlugin } from 'electron-vite'; 3 | import react from '@vitejs/plugin-react'; 4 | 5 | export default defineConfig({ 6 | main: { 7 | plugins: [externalizeDepsPlugin(), swcPlugin()], 8 | }, 9 | preload: { 10 | plugins: [externalizeDepsPlugin(), swcPlugin()], 11 | }, 12 | renderer: { 13 | resolve: { 14 | alias: { 15 | '@renderer': resolve('src/renderer/src'), 16 | }, 17 | }, 18 | plugins: [react()], 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /hlrt/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grebuloff-hlrt", 3 | "version": "0.0.0", 4 | "description": "Grebuloff High-Level Runtime (HLRT)", 5 | "main": "./out/main/index.js", 6 | "private": true, 7 | "packageManager": "pnpm@8.6.7", 8 | "scripts": { 9 | "preinstall": "npx only-allow pnpm", 10 | "format": "prettier --write .", 11 | "lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix", 12 | "typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false", 13 | "typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false", 14 | "typecheck": "pnpm run --parallel \"/^typecheck:.*$/\"", 15 | "start": "electron-vite preview", 16 | "dev": "electron-vite dev --sourcemap", 17 | "dev:renderer": "cross-env LLRT_PIPE_ID=dev electron-vite dev --sourcemap --rendererOnly", 18 | "build:all": "pnpm run build:native && pnpm run build:js", 19 | "build": "pnpm run typecheck && electron-vite build && electron-packager . --platform=win32 --out=../dist --executable-name=grebuloff-hlrt --overwrite", 20 | "build:native": "cargo-cp-artifact -ac grebuloff-hlrt-native out/main/native/native.node -- cargo build --message-format=json-render-diagnostics", 21 | "maybe-build:js": "build-if-changed" 22 | }, 23 | "dependencies": { 24 | "@electron-toolkit/utils": "^1.0.2", 25 | "class-transformer": "^0.5.1", 26 | "classnames": "^2.3.2", 27 | "msgpackr": "^1.9.5", 28 | "react": "^18.2.0", 29 | "react-dom": "^18.2.0", 30 | "reflect-metadata": "^0.1.13" 31 | }, 32 | "devDependencies": { 33 | "@swc/core": "^1.3.67", 34 | "@types/node": "^20.3.3", 35 | "@types/react": "^18.2.14", 36 | "@types/react-dom": "^18.2.6", 37 | "@typescript-eslint/eslint-plugin": "^5.61.0", 38 | "@typescript-eslint/parser": "^5.61.0", 39 | "@vitejs/plugin-react": "^4.0.1", 40 | "build-if-changed": "^1.5.5", 41 | "cross-env": "^7.0.3", 42 | "electron": "^25.2.0", 43 | "electron-packager": "^17.1.1", 44 | "electron-vite": "^1.0.24", 45 | "eslint": "^8.44.0", 46 | "eslint-config-prettier": "^8.8.0", 47 | "eslint-plugin-prettier": "^4.2.1", 48 | "eslint-plugin-react": "^7.32.2", 49 | "prettier": "^2.8.8", 50 | "typescript": "^5.1.6", 51 | "vite": "^4.3.9", 52 | "cargo-cp-artifact": "^0.1" 53 | }, 54 | "pnpm": { 55 | "patchedDependencies": { 56 | "build-if-changed@1.5.5": "patches/build-if-changed@1.5.5.patch" 57 | } 58 | }, 59 | "bic": [ 60 | "src" 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /hlrt/patches/build-if-changed@1.5.5.patch: -------------------------------------------------------------------------------- 1 | diff --git a/CHANGELOG.md b/CHANGELOG.md 2 | deleted file mode 100644 3 | index cb8bc80d7e68e5704d7d7e43529bc608c2902728..0000000000000000000000000000000000000000 4 | diff --git a/lib/gitignore.js b/lib/gitignore.js 5 | index 15c2c620b28ba9ea907d833e511ebfe9047d9f0e..eef9128dfa7535bd56a0f5922dc5c001e9814bc7 100644 6 | --- a/lib/gitignore.js 7 | +++ b/lib/gitignore.js 8 | @@ -20,6 +20,7 @@ class GitIgnore { 9 | this.matchRootGlobs = recrawl_1.createMatcher(rootGlobs); 10 | } 11 | test(file, name) { 12 | + return false; 13 | if (!path_1.isAbsolute(file)) { 14 | throw Error('Expected an absolute path'); 15 | } 16 | diff --git a/lib/index.js b/lib/index.js 17 | index 5e2615dae3c000e72ed93613efb9279aabee42f2..f441e21a1e0d7dbaa98915e3b95d4dce4debbafb 100644 18 | --- a/lib/index.js 19 | +++ b/lib/index.js 20 | @@ -195,7 +195,7 @@ function getLines(data) { 21 | .split(/\r?\n/); 22 | } 23 | function getRunner(root) { 24 | - return fs.isFile(path_1.join(root, 'package-lock.json')) ? 'npm' : 'yarn'; 25 | + return 'pnpm'; 26 | } 27 | function filterTruthy(changed) { 28 | return changed.filter(Boolean); -------------------------------------------------------------------------------- /hlrt/src/main/index.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { app, BrowserWindow } from 'electron'; 3 | import { join } from 'path'; 4 | import { optimizer } from '@electron-toolkit/utils'; 5 | import { RpcClient } from './rpc/client'; 6 | 7 | // force a scale factor of 1, even on high-DPI displays, as we will control scaling ourselves 8 | app.commandLine.appendSwitch('high-dpi-support', '1'); 9 | app.commandLine.appendSwitch('force-device-scale-factor', '1'); 10 | 11 | // disable hardware acceleration as we're using offscreen rendering 12 | // https://github.com/electron/electron/issues/13368#issuecomment-401188989 13 | app.disableHardwareAcceleration(); 14 | 15 | // This method will be called when Electron has finished 16 | // initialization and is ready to create browser windows. 17 | // Some APIs can only be used after this event occurs. 18 | app.whenReady().then(() => { 19 | // Default open or close DevTools by F12 in development 20 | // and ignore CommandOrControl + R in production. 21 | // see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils 22 | app.on('browser-window-created', (_, window) => { 23 | optimizer.watchWindowShortcuts(window); 24 | }); 25 | 26 | const showNoPipe = !!process.env['SHOW_NO_PIPE']; 27 | 28 | let windowOpts = { 29 | width: 1920, 30 | height: 1080, 31 | show: showNoPipe, 32 | title: 'Grebuloff UI Host', 33 | autoHideMenuBar: true, 34 | transparent: true, 35 | backgroundColor: 'transparent', 36 | frame: false, 37 | webPreferences: { 38 | preload: join(__dirname, '../preload/index.js'), 39 | sandbox: true, 40 | nodeIntegration: false, 41 | offscreen: !showNoPipe, 42 | }, 43 | }; 44 | 45 | if (showNoPipe) { 46 | windowOpts = { 47 | ...windowOpts, 48 | show: true, 49 | title: 'Grebuloff UI Host (show/no-pipe mode)', 50 | transparent: false, 51 | frame: true, 52 | }; 53 | } 54 | 55 | const mainWindow = new BrowserWindow(windowOpts); 56 | 57 | mainWindow.webContents.setWindowOpenHandler((_details) => { 58 | // shell.openExternal(details.url); 59 | return { action: 'deny' }; 60 | }); 61 | 62 | // HMR for renderer base on electron-vite cli. 63 | // Load the remote URL for development or the local html file for production. 64 | if (process.env['ELECTRON_RENDERER_URL']) { 65 | const tryLoad = async (url: string) => { 66 | try { 67 | await mainWindow.loadURL(url); 68 | } catch (e) { 69 | console.error(`failed to load ${url}: ${e}`); 70 | setTimeout(() => tryLoad(url), 1000); 71 | } 72 | }; 73 | 74 | tryLoad(process.env['ELECTRON_RENDERER_URL']); 75 | } else { 76 | mainWindow.loadFile(join(__dirname, '../renderer/index.html')); 77 | } 78 | 79 | if (showNoPipe) { 80 | console.log('not connecting to pipe: SHOW_NO_PIPE is set'); 81 | return; 82 | } 83 | 84 | const pipeId = process.env['LLRT_PIPE_ID']; 85 | if (!pipeId) { 86 | console.error('missing pipe id; set env var LLRT_PIPE_ID appropriately'); 87 | process.exit(1); 88 | } 89 | 90 | console.log(`pipe id: ${pipeId}`); 91 | 92 | // create the pipe manager and connect 93 | const rpcClient = new RpcClient(pipeId, mainWindow); 94 | rpcClient.connect(); 95 | }); 96 | 97 | app.on('window-all-closed', () => { 98 | app.quit(); 99 | }); 100 | -------------------------------------------------------------------------------- /hlrt/src/main/native/index.ts: -------------------------------------------------------------------------------- 1 | import native from './native.node'; 2 | -------------------------------------------------------------------------------- /hlrt/src/main/native/lib.rs: -------------------------------------------------------------------------------- 1 | use neon::prelude::*; 2 | 3 | fn hello(mut cx: FunctionContext) -> JsResult { 4 | Ok(cx.string("hello node")) 5 | } 6 | 7 | #[neon::main] 8 | fn main(mut cx: ModuleContext) -> NeonResult<()> { 9 | cx.export_function("hello", hello)?; 10 | Ok(()) 11 | } 12 | -------------------------------------------------------------------------------- /hlrt/src/main/paint.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow, NativeImage, Rectangle } from 'electron'; 2 | import { RpcClient } from './rpc/client'; 3 | 4 | export class UiPainter { 5 | private paintData?: PaintData; 6 | private shouldRepaint = true; 7 | private sending = false; 8 | 9 | constructor(private rpc: RpcClient, private browser: BrowserWindow) { 10 | browser.webContents.on('paint', this.onPaint.bind(this)); 11 | setInterval(this.tick.bind(this), 1); 12 | } 13 | 14 | handleResize(width: number, height: number) { 15 | console.log(`resize: ${width}x${height}`); 16 | this.paintData = undefined; 17 | this.browser.setContentSize(width, height); 18 | } 19 | 20 | async repaint(): Promise { 21 | if (!this.paintData) return false; 22 | 23 | if (this.rpc.ready && !this.sending && this.shouldRepaint) { 24 | this.sending = true; 25 | 26 | this.shouldRepaint = false; 27 | await this.rpc.sendRaw(this.paintData.prepareBuffer()); 28 | 29 | this.sending = false; 30 | return true; 31 | } 32 | 33 | return false; 34 | } 35 | 36 | private tick() { 37 | this.repaint(); 38 | } 39 | 40 | private onPaint(_event: Event, dirty: Rectangle, image: NativeImage) { 41 | this.paintData = new PaintData(dirty, image); 42 | this.shouldRepaint = true; 43 | } 44 | } 45 | 46 | export enum ImageFormat { 47 | BGRA8 = 0, 48 | } 49 | 50 | export class PaintData { 51 | constructor( 52 | public readonly dirty: Rectangle, 53 | public readonly image: NativeImage, 54 | ) {} 55 | 56 | /** 57 | * Gets the prepared buffer to send to LLRT. 58 | * You must consume this buffer in the same event loop tick as calling this method; 59 | * otherwise, the image data is not guaranteed to be valid. 60 | */ 61 | prepareBuffer(): Buffer { 62 | const buf = Buffer.alloc(5); 63 | 64 | const size = this.image.getSize(); 65 | buf.writeUInt8(ImageFormat.BGRA8, 0); 66 | buf.writeUInt16LE(size.width, 1); 67 | buf.writeUInt16LE(size.height, 3); 68 | 69 | return Buffer.concat([buf, this.image.getBitmap()]); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /hlrt/src/main/rpc/client.ts: -------------------------------------------------------------------------------- 1 | import { default as net, Socket } from 'net'; 2 | import EventEmitter from 'events'; 3 | import { RpcMessageType } from './messages'; 4 | import { 5 | PackedRpcMessage, 6 | RpcMessageDecoderStream, 7 | RpcMessageEncoderStream, 8 | RpcRawEncoderStream, 9 | } from './codec'; 10 | import { UiPainter } from '../paint'; 11 | import { BrowserWindow } from 'electron'; 12 | 13 | export class RpcClient extends EventEmitter { 14 | private pipeName: string; 15 | private client: Socket | null = null; 16 | private encoder: RpcMessageEncoderStream | null = null; 17 | private rawEncoder: RpcRawEncoderStream | null = null; 18 | private decoder: RpcMessageDecoderStream | null = null; 19 | 20 | // downstream services 21 | // todo: tidy this up 22 | private uiPainter: UiPainter; 23 | 24 | constructor(pipeId: string, mainWindow: BrowserWindow) { 25 | super(); 26 | this.pipeName = `\\\\.\\pipe\\grebuloff-llrt-ui-${pipeId}`; 27 | 28 | this.uiPainter = new UiPainter(this, mainWindow); 29 | } 30 | 31 | connect() { 32 | console.log(`connecting to LLRT on ${this.pipeName}`); 33 | 34 | this.client = net.connect( 35 | { path: this.pipeName }, 36 | this.onConnect.bind(this), 37 | ); 38 | } 39 | 40 | get ready() { 41 | return ( 42 | this.client && this.client.writable && !this.client.writableNeedDrain 43 | ); 44 | } 45 | 46 | async send(type: RpcMessageType, data: unknown) { 47 | new Promise((resolve, reject) => { 48 | if (!this.client || !this.encoder) { 49 | return reject(new Error('client is null')); 50 | } 51 | 52 | const packed = new PackedRpcMessage(type, data); 53 | if (this.encoder.write(packed)) { 54 | process.nextTick(resolve); 55 | } else { 56 | this.client.once('drain', () => { 57 | resolve(); 58 | }); 59 | } 60 | }); 61 | } 62 | 63 | async sendRaw(data: Buffer) { 64 | new Promise((resolve, reject) => { 65 | if (!this.client || !this.rawEncoder) { 66 | return reject(new Error('client is null')); 67 | } 68 | 69 | if (this.rawEncoder.write(data)) { 70 | process.nextTick(resolve); 71 | } else { 72 | this.client.once('drain', () => { 73 | resolve(); 74 | }); 75 | } 76 | }); 77 | } 78 | 79 | private onConnect() { 80 | if (!this.client) { 81 | throw new Error('client is null'); 82 | } 83 | 84 | this.encoder = new RpcMessageEncoderStream(); 85 | this.rawEncoder = new RpcRawEncoderStream(); 86 | this.decoder = new RpcMessageDecoderStream(); 87 | 88 | this.client.pipe(this.decoder); 89 | this.encoder.pipe(this.client); 90 | this.rawEncoder.pipe(this.client); 91 | 92 | this.decoder.on('data', this.onData.bind(this)); 93 | this.client.on('end', this.onDisconnect.bind(this)); 94 | this.client.on('drain', this.onDrain.bind(this)); 95 | 96 | console.log('connected to LLRT pipe'); 97 | 98 | this.emit('connect'); 99 | } 100 | 101 | private onDisconnect() { 102 | console.log('disconnected from LLRT pipe'); 103 | this.emit('close'); 104 | } 105 | 106 | private onData(packed: PackedRpcMessage | Buffer) { 107 | console.log('received data from LLRT pipe'); 108 | console.dir(packed); 109 | 110 | if (!(packed instanceof PackedRpcMessage)) { 111 | throw new Error('received unexpected raw data from LLRT pipe'); 112 | } 113 | 114 | const data = packed.data; 115 | switch (packed.type) { 116 | case RpcMessageType.Resize: 117 | this.uiPainter.handleResize(data.width, data.height); 118 | break; 119 | } 120 | } 121 | 122 | private onDrain() { 123 | this.emit('drain'); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /hlrt/src/main/rpc/codec.ts: -------------------------------------------------------------------------------- 1 | import { Packr, Unpackr } from 'msgpackr'; 2 | import { Transform, TransformCallback } from 'stream'; 3 | import { RpcMessageType } from './messages'; 4 | 5 | abstract class LengthDecoderStream extends Transform { 6 | private incompleteChunk: Buffer | null = null; 7 | 8 | constructor() { 9 | super({ 10 | objectMode: true, 11 | }); 12 | } 13 | 14 | readFullChunk(chunk: Buffer): Buffer | null { 15 | if (this.incompleteChunk) { 16 | chunk = Buffer.concat([this.incompleteChunk, chunk]); 17 | this.incompleteChunk = null; 18 | } 19 | 20 | // read a little-endian 32-bit integer from the start of the chunk 21 | const length = chunk.readUInt32LE(0); 22 | if (chunk.length >= length + 4) { 23 | // if there's anything left in the chunk, it's the start of the next message - save it 24 | if (chunk.length > length + 4) { 25 | this.incompleteChunk = chunk.subarray(length + 4); 26 | } 27 | 28 | // we have a complete chunk, trim the chunk to size and return it 29 | return chunk.subarray(4, length + 4); 30 | } 31 | 32 | return null; 33 | } 34 | } 35 | 36 | abstract class LengthEncoderStream extends Transform { 37 | constructor() { 38 | super({ 39 | writableObjectMode: true, 40 | }); 41 | } 42 | 43 | writeFullChunk(chunk: Buffer) { 44 | // prepend the length 45 | const length = Buffer.alloc(4); 46 | length.writeUInt32LE(chunk.length, 0); 47 | 48 | // push the encoded message 49 | this.push(Buffer.concat([length, chunk])); 50 | } 51 | } 52 | 53 | export class RpcMessageDecoderStream extends LengthDecoderStream { 54 | private readonly codec: Unpackr; 55 | 56 | constructor() { 57 | super(); 58 | this.codec = new Unpackr({ useRecords: false }); 59 | } 60 | 61 | _transform( 62 | partialChunk: Buffer, 63 | encoding: string, 64 | callback: TransformCallback, 65 | ) { 66 | const fullChunk = this.readFullChunk(partialChunk); 67 | if (fullChunk) { 68 | // optimization: if the first byte isn't within 0x80-0x8f or 0xde-0xdf, then we know it's not a 69 | // valid msgpack structure for our purposes (since we only use maps), so we can skip the 70 | // deserialization step and treat it as a raw message 71 | // currently, the UI doesn't _receive_ any raw messages, but this is here for completeness 72 | if ( 73 | fullChunk[0] < 0x80 || 74 | (fullChunk[0] > 0x8f && fullChunk[0] < 0xde) || 75 | fullChunk[0] > 0xdf 76 | ) { 77 | this.push(fullChunk); 78 | } else { 79 | // decode the message 80 | const decoded = this.codec.decode(fullChunk); 81 | console.dir(decoded); 82 | 83 | // extract the message type 84 | const type = Object.keys(decoded.Ui)[0] as RpcMessageType; 85 | 86 | // push the decoded message 87 | this.push(new PackedRpcMessage(type, decoded.Ui[type])); 88 | } 89 | } 90 | 91 | callback(); 92 | } 93 | } 94 | 95 | export class RpcMessageEncoderStream extends LengthEncoderStream { 96 | private readonly codec: Packr; 97 | 98 | constructor() { 99 | super(); 100 | this.codec = new Packr({ useRecords: false }); 101 | } 102 | 103 | _transform( 104 | message: PackedRpcMessage, 105 | encoding: string, 106 | callback: () => void, 107 | ) { 108 | const encoded = this.codec.encode(message.into()); 109 | this.writeFullChunk(encoded); 110 | 111 | callback(); 112 | } 113 | } 114 | 115 | export class RpcRawEncoderStream extends LengthEncoderStream { 116 | _transform(message: Buffer, encoding: string, callback: () => void) { 117 | this.writeFullChunk(message); 118 | 119 | callback(); 120 | } 121 | } 122 | 123 | export class PackedRpcMessage { 124 | constructor( 125 | public readonly type: RpcMessageType, 126 | public readonly data: any, // todo 127 | ) {} 128 | 129 | into() { 130 | return { 131 | Ui: { 132 | [this.type.toString()]: this.data, 133 | }, 134 | }; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /hlrt/src/main/rpc/messages.ts: -------------------------------------------------------------------------------- 1 | import { Type } from 'class-transformer'; 2 | 3 | export enum RpcMessageType { 4 | Resize = 'Resize', 5 | } 6 | 7 | export class RpcMessageResize {} 8 | 9 | export class PackedRpcMessage { 10 | public readonly type: RpcMessageType; 11 | 12 | @Type(() => Object, { 13 | discriminator: { 14 | property: 'type', 15 | subTypes: [{ value: RpcMessageResize, name: RpcMessageType.Resize }], 16 | }, 17 | }) 18 | public readonly data: any; 19 | 20 | constructor(type: RpcMessageType, data: any) { 21 | this.type = type; 22 | this.data = data; 23 | } 24 | 25 | into() { 26 | return { 27 | Ui: { 28 | [this.type.toString()]: this.data, 29 | }, 30 | }; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /hlrt/src/preload/index.ts: -------------------------------------------------------------------------------- 1 | import { contextBridge } from 'electron'; 2 | 3 | contextBridge.exposeInMainWorld( 4 | 'grebuloffUiMode', 5 | process.env['SHOW_NO_PIPE'] ? 'no-pipe' : 'pipe', 6 | ); 7 | -------------------------------------------------------------------------------- /hlrt/src/renderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /hlrt/src/renderer/src/App.tsx: -------------------------------------------------------------------------------- 1 | import './assets/App.css'; 2 | 3 | function App(): JSX.Element { 4 | return ( 5 |
6 |

Hello from Grebuloff!

7 |
8 | ); 9 | } 10 | 11 | export default App; 12 | -------------------------------------------------------------------------------- /hlrt/src/renderer/src/assets/App.css: -------------------------------------------------------------------------------- 1 | .container { 2 | /* placeholder for testing */ 3 | display: flex; 4 | justify-content: center; 5 | align-items: center; 6 | margin-top: 20%; 7 | font-size: 1.5em; 8 | } 9 | 10 | .spin { 11 | animation: spin 2s linear infinite; 12 | } 13 | 14 | @keyframes spin { 15 | 50% { 16 | transform: rotate(180deg); 17 | font-size: 3em; 18 | } 19 | 20 | 100% { 21 | transform: rotate(360deg); 22 | font-size: 1.5em; 23 | } 24 | } -------------------------------------------------------------------------------- /hlrt/src/renderer/src/assets/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | overflow: hidden; 9 | background: transparent; 10 | color: red; 11 | } 12 | 13 | body.no-pipe { 14 | background: black !important; 15 | } 16 | 17 | code { 18 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 19 | monospace; 20 | } 21 | -------------------------------------------------------------------------------- /hlrt/src/renderer/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /hlrt/src/renderer/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './assets/index.css'; 4 | import App from './App'; 5 | 6 | declare global { 7 | interface Window { 8 | grebuloffUiMode: 'pipe' | 'no-pipe'; 9 | } 10 | } 11 | 12 | if (window.grebuloffUiMode === 'no-pipe') { 13 | document.body.classList.add('no-pipe'); 14 | } 15 | 16 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 17 | 18 | 19 | , 20 | ); 21 | -------------------------------------------------------------------------------- /hlrt/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022", 4 | "module": "es2022", 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "experimentalDecorators": true, 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "composite": true, 16 | }, 17 | "exclude": [ 18 | "build.cjs", 19 | ], 20 | "references": [ 21 | { 22 | "path": "./tsconfig.node.json" 23 | }, 24 | { 25 | "path": "./tsconfig.web.json" 26 | } 27 | ] 28 | } -------------------------------------------------------------------------------- /hlrt/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": [ 4 | "electron.vite.config.*", 5 | "src/main/**/*.{ts,tsx}", 6 | "src/preload/**/*.{ts,tsx}", 7 | ], 8 | "compilerOptions": { 9 | "composite": true, 10 | "types": [ 11 | "node", 12 | "electron-vite/node" 13 | ], 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /hlrt/tsconfig.web.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": [ 4 | "src/renderer/src/env.d.ts", 5 | "src/renderer/src/**/*.{ts,tsx}", 6 | ], 7 | "compilerOptions": { 8 | "lib": [ 9 | "ESNext", 10 | "DOM", 11 | "DOM.Iterable" 12 | ], 13 | "composite": true, 14 | "jsx": "react-jsx", 15 | "baseUrl": ".", 16 | "paths": { 17 | "@renderer/*": [ 18 | "src/renderer/src/*" 19 | ] 20 | }, 21 | } 22 | } -------------------------------------------------------------------------------- /injector/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "grebuloff-injector" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | windows = { workspace = true } 8 | dll-syringe = { workspace = true, features = ["syringe", "rpc"] } 9 | clap = { version = "4.3.11", features = ["derive"] } 10 | sysinfo = "0.29.0" 11 | -------------------------------------------------------------------------------- /injector/src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(unsafe_code)] 2 | 3 | use clap::{Parser, Subcommand}; 4 | use dll_syringe::{process::OwnedProcess, Syringe}; 5 | use std::os::windows::io::FromRawHandle; 6 | use std::path::PathBuf; 7 | use std::ptr::addr_of_mut; 8 | use sysinfo::{PidExt, ProcessExt, SystemExt}; 9 | use windows::Win32::Foundation::{CloseHandle, LUID}; 10 | use windows::Win32::Security::Authorization::{ 11 | GetSecurityInfo, SetSecurityInfo, GRANT_ACCESS, SE_KERNEL_OBJECT, 12 | }; 13 | use windows::Win32::Security::{ 14 | AdjustTokenPrivileges, LookupPrivilegeValueW, PrivilegeCheck, ACE_FLAGS, ACL, 15 | DACL_SECURITY_INFORMATION, LUID_AND_ATTRIBUTES, PRIVILEGE_SET, SECURITY_DESCRIPTOR, 16 | SE_DEBUG_NAME, SE_PRIVILEGE_ENABLED, SE_PRIVILEGE_REMOVED, TOKEN_ADJUST_PRIVILEGES, 17 | TOKEN_PRIVILEGES, TOKEN_QUERY, 18 | }; 19 | use windows::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken, CREATE_SUSPENDED}; 20 | use windows::{ 21 | core::{HSTRING, PWSTR}, 22 | imp::GetLastError, 23 | Win32::{ 24 | Foundation::{BOOL, HANDLE}, 25 | Security::{ 26 | Authorization::{BuildExplicitAccessWithNameW, SetEntriesInAclW, EXPLICIT_ACCESS_W}, 27 | InitializeSecurityDescriptor, SetSecurityDescriptorDacl, PSECURITY_DESCRIPTOR, 28 | SECURITY_ATTRIBUTES, 29 | }, 30 | System::Threading::{CreateProcessW, ResumeThread, STARTUPINFOW}, 31 | }, 32 | }; 33 | 34 | const DEFAULT_INJECTION_DELAY: u64 = 3000; 35 | 36 | #[derive(Parser)] 37 | struct Args { 38 | #[clap(subcommand)] 39 | command: Commands, 40 | 41 | #[clap(short = 'I', long)] 42 | injection_delay: Option, 43 | } 44 | 45 | #[derive(Subcommand)] 46 | enum Commands { 47 | Launch { 48 | #[clap(short, long)] 49 | game_path: PathBuf, 50 | }, 51 | Inject, 52 | } 53 | 54 | #[derive(Debug)] 55 | struct ProcessInfo { 56 | pid: u32, 57 | process_handle: HANDLE, 58 | thread_handle: HANDLE, 59 | } 60 | 61 | impl Drop for ProcessInfo { 62 | fn drop(&mut self) { 63 | unsafe { 64 | CloseHandle(self.thread_handle); 65 | CloseHandle(self.process_handle); 66 | } 67 | } 68 | } 69 | 70 | // ported from Dalamud launch code 71 | unsafe fn spawn_game_process(game_path: PathBuf) -> ProcessInfo { 72 | let mut explicit_access = std::mem::zeroed::(); 73 | 74 | let username = std::env::var("USERNAME").unwrap(); 75 | let pcwstr = HSTRING::from(username); 76 | 77 | BuildExplicitAccessWithNameW( 78 | &mut explicit_access, 79 | &pcwstr, 80 | // STANDARD_RIGHTS_ALL | SPECIFIC_RIGHTS_ALL & ~PROCESS_VM_WRITE 81 | 0x001F0000 | 0x0000FFFF & !0x20, 82 | GRANT_ACCESS, 83 | ACE_FLAGS(0), 84 | ); 85 | 86 | let mut newacl = std::ptr::null_mut(); 87 | 88 | let result = SetEntriesInAclW(Some(&[explicit_access]), None, addr_of_mut!(newacl)); 89 | if result.is_err() { 90 | panic!("SetEntriesInAclA failed with error code {}", result.0); 91 | } 92 | 93 | let mut sec_desc = std::mem::zeroed::(); 94 | let psec_desc = PSECURITY_DESCRIPTOR(&mut sec_desc as *mut _ as *mut _); 95 | if !InitializeSecurityDescriptor(psec_desc, 1).as_bool() { 96 | panic!("InitializeSecurityDescriptor failed"); 97 | } 98 | 99 | if !SetSecurityDescriptorDacl(psec_desc, true, Some(newacl), false).as_bool() { 100 | panic!("SetSecurityDescriptorDacl failed"); 101 | } 102 | 103 | let mut process_information = 104 | std::mem::zeroed::(); 105 | let process_attributes = SECURITY_ATTRIBUTES { 106 | nLength: std::mem::size_of::() as u32, 107 | lpSecurityDescriptor: psec_desc.0, 108 | bInheritHandle: BOOL(0), 109 | }; 110 | let mut startup_info = std::mem::zeroed::(); 111 | startup_info.cb = std::mem::size_of::() as u32; 112 | 113 | let cmd_line = format!( 114 | "\"{}\" DEV.TestSID=0 language=1 DEV.MaxEntitledExpansionID=4 DEV.GameQuitMessageBox=0\0", 115 | game_path.to_str().unwrap() 116 | ); 117 | 118 | let game_dir = game_path.parent().unwrap(); 119 | 120 | let res = CreateProcessW( 121 | None, 122 | PWSTR(cmd_line.encode_utf16().collect::>().as_mut_ptr()), 123 | Some(&process_attributes), 124 | None, 125 | BOOL(0), 126 | CREATE_SUSPENDED, 127 | None, 128 | &HSTRING::from(game_dir.to_str().unwrap()), 129 | &startup_info, 130 | &mut process_information, 131 | ); 132 | let last_error = GetLastError(); 133 | if res == BOOL(0) { 134 | panic!("CreateProcessW failed with error code {}", last_error); 135 | } 136 | 137 | // strip SeDebugPrivilege/ACL from the process 138 | let mut token_handle = std::mem::zeroed::(); 139 | 140 | if !OpenProcessToken( 141 | process_information.hProcess, 142 | TOKEN_QUERY | TOKEN_ADJUST_PRIVILEGES, 143 | &mut token_handle, 144 | ) 145 | .as_bool() 146 | { 147 | panic!("OpenProcessToken failed"); 148 | } 149 | 150 | let mut luid_debug_privilege = std::mem::zeroed::(); 151 | if !LookupPrivilegeValueW(None, SE_DEBUG_NAME, &mut luid_debug_privilege).as_bool() { 152 | panic!("LookupPrivilegeValueW failed"); 153 | } 154 | 155 | let mut required_privileges = PRIVILEGE_SET { 156 | PrivilegeCount: 1, 157 | Control: 1, 158 | Privilege: [LUID_AND_ATTRIBUTES { 159 | Luid: luid_debug_privilege, 160 | Attributes: SE_PRIVILEGE_ENABLED, 161 | }], 162 | }; 163 | 164 | let mut b_result: i32 = 0; 165 | if !PrivilegeCheck(token_handle, &mut required_privileges, &mut b_result).as_bool() { 166 | panic!("PrivilegeCheck failed"); 167 | } 168 | 169 | // remove SeDebugPrivilege 170 | if b_result != 0 { 171 | println!("removing SeDebugPrivilege"); 172 | let mut token_privileges = TOKEN_PRIVILEGES { 173 | PrivilegeCount: 1, 174 | Privileges: [LUID_AND_ATTRIBUTES { 175 | Luid: luid_debug_privilege, 176 | Attributes: SE_PRIVILEGE_REMOVED, 177 | }], 178 | }; 179 | 180 | if !AdjustTokenPrivileges( 181 | token_handle, 182 | false, 183 | Some(&mut token_privileges), 184 | 0, 185 | None, 186 | None, 187 | ) 188 | .as_bool() 189 | { 190 | panic!("AdjustTokenPrivileges failed"); 191 | } 192 | } 193 | 194 | CloseHandle(token_handle); 195 | 196 | ProcessInfo { 197 | pid: process_information.dwProcessId, 198 | process_handle: process_information.hProcess, 199 | thread_handle: process_information.hThread, 200 | } 201 | } 202 | 203 | unsafe fn copy_acl_from_self_to_target(target_process: HANDLE) { 204 | println!("copying current acl to target process..."); 205 | 206 | let mut acl = std::ptr::null_mut() as *mut ACL; 207 | 208 | if !GetSecurityInfo( 209 | GetCurrentProcess(), 210 | SE_KERNEL_OBJECT, 211 | DACL_SECURITY_INFORMATION.0, 212 | None, 213 | None, 214 | Some(addr_of_mut!(acl)), 215 | None, 216 | None, 217 | ) 218 | .is_ok() 219 | { 220 | panic!("GetSecurityInfo failed"); 221 | } 222 | 223 | if !SetSecurityInfo( 224 | target_process, 225 | SE_KERNEL_OBJECT, 226 | DACL_SECURITY_INFORMATION.0, 227 | None, 228 | None, 229 | Some(acl), 230 | None, 231 | ) 232 | .is_ok() 233 | { 234 | panic!("SetSecurityInfo failed"); 235 | } 236 | } 237 | 238 | fn await_game_process() -> u32 { 239 | let pid; 240 | 241 | 'wait: loop { 242 | std::thread::sleep(std::time::Duration::from_millis(100)); 243 | 244 | let system = sysinfo::System::new_all(); 245 | let processes = system.processes(); 246 | 247 | for (_pid, process) in processes { 248 | if process.name() == "ffxiv_dx11.exe" { 249 | pid = _pid.as_u32(); 250 | break 'wait; 251 | } 252 | } 253 | } 254 | 255 | pid 256 | } 257 | 258 | fn main() { 259 | let args = Args::parse(); 260 | 261 | let process_info; 262 | 263 | match args.command { 264 | Commands::Launch { game_path } => { 265 | process_info = unsafe { spawn_game_process(game_path) }; 266 | } 267 | Commands::Inject => { 268 | process_info = ProcessInfo { 269 | pid: await_game_process(), 270 | process_handle: HANDLE(0), 271 | thread_handle: HANDLE(0), 272 | }; 273 | } 274 | } 275 | 276 | println!( 277 | "pid: {} - tid: {}", 278 | process_info.pid, process_info.thread_handle.0 279 | ); 280 | 281 | let target; 282 | if process_info.process_handle.0 != 0 { 283 | target = unsafe { 284 | OwnedProcess::from_raw_handle(std::mem::transmute(process_info.process_handle)) 285 | }; 286 | } else { 287 | target = OwnedProcess::from_pid(process_info.pid).unwrap(); 288 | } 289 | 290 | let syringe = Syringe::for_process(target); 291 | 292 | let current_exe = std::env::current_exe().unwrap(); 293 | let mut grebuloff_path = current_exe.parent().unwrap().to_path_buf(); 294 | 295 | #[cfg(debug_assertions)] 296 | { 297 | // HACK: if this is a debug build, cargo is probably executing it 298 | // from the target directory. we'd rather execute from the dist 299 | // directory, so we'll try to find the dist directory and use that 300 | // instead. 301 | if grebuloff_path.file_name().unwrap() == "debug" { 302 | grebuloff_path.pop(); 303 | grebuloff_path.pop(); 304 | grebuloff_path.pop(); 305 | grebuloff_path.push("dist"); 306 | 307 | println!("[debug build] using grebuloff_path: {:?}", grebuloff_path); 308 | } 309 | } 310 | 311 | let llrt_path = &grebuloff_path.join("grebuloff.dll"); 312 | 313 | unsafe { 314 | if process_info.thread_handle.0 != 0 { 315 | ResumeThread(process_info.thread_handle); 316 | 317 | if process_info.process_handle.0 != 0 { 318 | // the idea here is to change the process acl once the window is created, 319 | // because at that point, the game has already checked its acls. 320 | // we should actually query to see if the window is open here, 321 | // but this should suffice for now 322 | std::thread::sleep(std::time::Duration::from_millis(1000)); 323 | copy_acl_from_self_to_target(process_info.process_handle); 324 | } 325 | } 326 | } 327 | 328 | let injection_delay = std::time::Duration::from_millis( 329 | args.injection_delay 330 | .unwrap_or(DEFAULT_INJECTION_DELAY) 331 | .clamp(0, 60000), 332 | ); 333 | 334 | if !injection_delay.is_zero() { 335 | println!( 336 | "waiting {}ms before injecting...", 337 | injection_delay.as_millis() 338 | ); 339 | std::thread::sleep(injection_delay); 340 | } 341 | 342 | println!("injecting..."); 343 | let injected_payload = syringe.inject(llrt_path).unwrap(); 344 | 345 | println!("calling entrypoint..."); 346 | let remote_load = 347 | unsafe { syringe.get_payload_procedure::)>(injected_payload, "init_injected") } 348 | .unwrap() 349 | .unwrap(); 350 | let str_as_vec = grebuloff_path.to_str().unwrap().as_bytes().to_vec(); 351 | remote_load.call(&str_as_vec).unwrap(); 352 | 353 | println!("done!"); 354 | } 355 | -------------------------------------------------------------------------------- /loader/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "grebuloff-loader" 3 | version = "0.1.0" 4 | edition = "2021" 5 | build = "build.rs" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | name = "winhttp" 10 | 11 | [dependencies] 12 | plthook = "0.2.2" 13 | windows = { workspace = true } 14 | -------------------------------------------------------------------------------- /loader/build.rs: -------------------------------------------------------------------------------- 1 | // this is pretty nasty, but it works 2 | fn main() { 3 | println!("cargo:rustc-link-lib=winhttp"); 4 | println!("cargo:rustc-cdylib-link-arg=/export:SvchostPushServiceGlobals=C:\\Windows\\system32\\winhttp.SvchostPushServiceGlobals,@5"); 5 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpAddRequestHeaders=C:\\Windows\\system32\\winhttp.WinHttpAddRequestHeaders,@6"); 6 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpAddRequestHeadersEx=C:\\Windows\\system32\\winhttp.WinHttpAddRequestHeadersEx,@7"); 7 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpAutoProxySvcMain=C:\\Windows\\system32\\winhttp.WinHttpAutoProxySvcMain,@8"); 8 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpCheckPlatform=C:\\Windows\\system32\\winhttp.WinHttpCheckPlatform,@9"); 9 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpCloseHandle=C:\\Windows\\system32\\winhttp.WinHttpCloseHandle,@10"); 10 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpConnect=C:\\Windows\\system32\\winhttp.WinHttpConnect,@11"); 11 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpConnectionDeletePolicyEntries=C:\\Windows\\system32\\winhttp.WinHttpConnectionDeletePolicyEntries,@12"); 12 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpConnectionDeleteProxyInfo=C:\\Windows\\system32\\winhttp.WinHttpConnectionDeleteProxyInfo,@13"); 13 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpConnectionFreeNameList=C:\\Windows\\system32\\winhttp.WinHttpConnectionFreeNameList,@14"); 14 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpConnectionFreeProxyInfo=C:\\Windows\\system32\\winhttp.WinHttpConnectionFreeProxyInfo,@15"); 15 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpConnectionFreeProxyList=C:\\Windows\\system32\\winhttp.WinHttpConnectionFreeProxyList,@16"); 16 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpConnectionGetNameList=C:\\Windows\\system32\\winhttp.WinHttpConnectionGetNameList,@17"); 17 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpConnectionGetProxyInfo=C:\\Windows\\system32\\winhttp.WinHttpConnectionGetProxyInfo,@18"); 18 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpConnectionGetProxyList=C:\\Windows\\system32\\winhttp.WinHttpConnectionGetProxyList,@19"); 19 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpConnectionSetPolicyEntries=C:\\Windows\\system32\\winhttp.WinHttpConnectionSetPolicyEntries,@20"); 20 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpConnectionSetProxyInfo=C:\\Windows\\system32\\winhttp.WinHttpConnectionSetProxyInfo,@21"); 21 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpConnectionUpdateIfIndexTable=C:\\Windows\\system32\\winhttp.WinHttpConnectionUpdateIfIndexTable,@22"); 22 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpCrackUrl=C:\\Windows\\system32\\winhttp.WinHttpCrackUrl,@23"); 23 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpCreateProxyResolver=C:\\Windows\\system32\\winhttp.WinHttpCreateProxyResolver,@24"); 24 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpCreateUrl=C:\\Windows\\system32\\winhttp.WinHttpCreateUrl,@25"); 25 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpDetectAutoProxyConfigUrl=C:\\Windows\\system32\\winhttp.WinHttpDetectAutoProxyConfigUrl,@26"); 26 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpFreeProxyResult=C:\\Windows\\system32\\winhttp.WinHttpFreeProxyResult,@27"); 27 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpFreeProxyResultEx=C:\\Windows\\system32\\winhttp.WinHttpFreeProxyResultEx,@28"); 28 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpFreeProxySettings=C:\\Windows\\system32\\winhttp.WinHttpFreeProxySettings,@29"); 29 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpGetDefaultProxyConfiguration=C:\\Windows\\system32\\winhttp.WinHttpGetDefaultProxyConfiguration,@30"); 30 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpGetIEProxyConfigForCurrentUser=C:\\Windows\\system32\\winhttp.WinHttpGetIEProxyConfigForCurrentUser,@31"); 31 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpGetProxyForUrl=C:\\Windows\\system32\\winhttp.WinHttpGetProxyForUrl,@32"); 32 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpGetProxyForUrlEx=C:\\Windows\\system32\\winhttp.WinHttpGetProxyForUrlEx,@33"); 33 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpGetProxyForUrlEx2=C:\\Windows\\system32\\winhttp.WinHttpGetProxyForUrlEx2,@34"); 34 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpGetProxyForUrlHvsi=C:\\Windows\\system32\\winhttp.WinHttpGetProxyForUrlHvsi,@35"); 35 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpGetProxyResult=C:\\Windows\\system32\\winhttp.WinHttpGetProxyResult,@36"); 36 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpGetProxyResultEx=C:\\Windows\\system32\\winhttp.WinHttpGetProxyResultEx,@37"); 37 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpGetProxySettingsVersion=C:\\Windows\\system32\\winhttp.WinHttpGetProxySettingsVersion,@38"); 38 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpGetTunnelSocket=C:\\Windows\\system32\\winhttp.WinHttpGetTunnelSocket,@39"); 39 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpOpen=C:\\Windows\\system32\\winhttp.WinHttpOpen,@40"); 40 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpOpenRequest=C:\\Windows\\system32\\winhttp.WinHttpOpenRequest,@41"); 41 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpProbeConnectivity=C:\\Windows\\system32\\winhttp.WinHttpProbeConnectivity,@43"); 42 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpQueryAuthSchemes=C:\\Windows\\system32\\winhttp.WinHttpQueryAuthSchemes,@44"); 43 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpQueryDataAvailable=C:\\Windows\\system32\\winhttp.WinHttpQueryDataAvailable,@45"); 44 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpQueryHeaders=C:\\Windows\\system32\\winhttp.WinHttpQueryHeaders,@46"); 45 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpQueryHeadersEx=C:\\Windows\\system32\\winhttp.WinHttpQueryHeadersEx,@47"); 46 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpQueryOption=C:\\Windows\\system32\\winhttp.WinHttpQueryOption,@48"); 47 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpReadData=C:\\Windows\\system32\\winhttp.WinHttpReadData,@49"); 48 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpReadDataEx=C:\\Windows\\system32\\winhttp.WinHttpReadDataEx,@50"); 49 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpReadProxySettings=C:\\Windows\\system32\\winhttp.WinHttpReadProxySettings,@51"); 50 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpReadProxySettingsHvsi=C:\\Windows\\system32\\winhttp.WinHttpReadProxySettingsHvsi,@52"); 51 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpReceiveResponse=C:\\Windows\\system32\\winhttp.WinHttpReceiveResponse,@53"); 52 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpResetAutoProxy=C:\\Windows\\system32\\winhttp.WinHttpResetAutoProxy,@54"); 53 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpSaveProxyCredentials=C:\\Windows\\system32\\winhttp.WinHttpSaveProxyCredentials,@55"); 54 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpSendRequest=C:\\Windows\\system32\\winhttp.WinHttpSendRequest,@56"); 55 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpSetCredentials=C:\\Windows\\system32\\winhttp.WinHttpSetCredentials,@57"); 56 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpSetDefaultProxyConfiguration=C:\\Windows\\system32\\winhttp.WinHttpSetDefaultProxyConfiguration,@58"); 57 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpSetOption=C:\\Windows\\system32\\winhttp.WinHttpSetOption,@59"); 58 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpSetProxySettingsPerUser=C:\\Windows\\system32\\winhttp.WinHttpSetProxySettingsPerUser,@60"); 59 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpSetStatusCallback=C:\\Windows\\system32\\winhttp.WinHttpSetStatusCallback,@61"); 60 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpSetTimeouts=C:\\Windows\\system32\\winhttp.WinHttpSetTimeouts,@62"); 61 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpTimeFromSystemTime=C:\\Windows\\system32\\winhttp.WinHttpTimeFromSystemTime,@63"); 62 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpTimeToSystemTime=C:\\Windows\\system32\\winhttp.WinHttpTimeToSystemTime,@64"); 63 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpWebSocketClose=C:\\Windows\\system32\\winhttp.WinHttpWebSocketClose,@65"); 64 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpWebSocketCompleteUpgrade=C:\\Windows\\system32\\winhttp.WinHttpWebSocketCompleteUpgrade,@66"); 65 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpWebSocketQueryCloseStatus=C:\\Windows\\system32\\winhttp.WinHttpWebSocketQueryCloseStatus,@67"); 66 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpWebSocketReceive=C:\\Windows\\system32\\winhttp.WinHttpWebSocketReceive,@68"); 67 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpWebSocketSend=C:\\Windows\\system32\\winhttp.WinHttpWebSocketSend,@69"); 68 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpWebSocketShutdown=C:\\Windows\\system32\\winhttp.WinHttpWebSocketShutdown,@70"); 69 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpWriteData=C:\\Windows\\system32\\winhttp.WinHttpWriteData,@71"); 70 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpWriteProxySettings=C:\\Windows\\system32\\winhttp.WinHttpWriteProxySettings,@72"); 71 | } 72 | -------------------------------------------------------------------------------- /loader/src/lib.rs: -------------------------------------------------------------------------------- 1 | use plthook::{ObjectFile, Replacement}; 2 | use std::{ 3 | ffi::{c_void, CString}, 4 | mem::ManuallyDrop, 5 | os::windows::prelude::OsStringExt, 6 | }; 7 | use windows::{ 8 | core::{ComInterface, PCSTR}, 9 | Win32::{ 10 | Foundation::HANDLE, 11 | Graphics::Dxgi::IDXGIFactory2, 12 | System::LibraryLoader::{GetModuleFileNameW, LoadLibraryA}, 13 | }, 14 | }; 15 | use windows::{ 16 | core::{HRESULT, HSTRING}, 17 | Win32::{ 18 | Foundation::HWND, 19 | System::{ 20 | LibraryLoader::{GetProcAddress, LoadLibraryExA, LOAD_WITH_ALTERED_SEARCH_PATH}, 21 | SystemServices::DLL_PROCESS_ATTACH, 22 | }, 23 | UI::WindowsAndMessaging::{MessageBoxW, MB_ICONERROR, MB_OK}, 24 | }, 25 | }; 26 | 27 | const ROOT_ENV_VAR: &'static str = "GREBULOFF_ROOT"; 28 | const ROOT_FILE: &'static str = "grebuloff_root.txt"; 29 | 30 | const ERROR_NO_ROOT: &'static str = r#"Could not find the Grebuloff root directory! 31 | 32 | We checked the following locations: 33 | 1. "GREBULOFF_ROOT" environment variable passed to the game executable 34 | 2. "grebuloff_root.txt" in the same directory as the game executable 35 | 3. The default installation directory: %AppData%\Grebuloff 36 | 37 | None of the paths searched contained a valid Grebuloff installation, so loading cannot continue. 38 | If you are trying to uninstall Grebuloff, delete "winhttp.dll" from the game directory. 39 | 40 | The game will now exit."#; 41 | 42 | static mut IAT_HOOK: Option> = None; 43 | 44 | #[no_mangle] 45 | #[allow(non_snake_case)] 46 | unsafe extern "system" fn DllMain( 47 | _hinstDLL: HANDLE, 48 | fdwReason: u32, 49 | _lpvReserved: *const std::ffi::c_void, 50 | ) -> bool { 51 | if fdwReason == DLL_PROCESS_ATTACH { 52 | let wakeup_cnt = std::env::var("FFIXV_WAKEUP_CNT"); 53 | if !wakeup_cnt.is_ok() { 54 | // FFXIV sets this env var for the fork where it executes for real. 55 | // If the env var isn't set, the process we just loaded into is about to 56 | // get restarted, so we should just exit. 57 | return true; 58 | } 59 | 60 | // we redirect CreateDXGIFactory here and return, so that we can load Grebuloff 61 | // it's expressly forbidden to load libraries in DllMain, and has a tendency to deadlock 62 | redirect_dxgi(); 63 | } 64 | 65 | true 66 | } 67 | 68 | unsafe fn load_grebuloff() { 69 | let root = get_grebuloff_root(); 70 | let load_result = LoadLibraryExA(root.dll_path, None, LOAD_WITH_ALTERED_SEARCH_PATH); 71 | 72 | match load_result { 73 | Ok(dll) => { 74 | // get the address of init_loader 75 | let init_loader: Option ()> = 76 | GetProcAddress(dll, PCSTR::from_raw(b"init_loader\0".as_ptr())) 77 | .map(|func| std::mem::transmute(func)); 78 | 79 | match init_loader { 80 | Some(init_loader) => { 81 | // call init_loader 82 | let runtime_dir = CString::new(root.runtime_path).unwrap(); 83 | init_loader(&runtime_dir); 84 | } 85 | None => { 86 | display_error(&format!( 87 | r#"Failed to find init_loader in Grebuloff at {}! 88 | 89 | The game will now exit."#, 90 | root.dll_path.to_string().unwrap(), 91 | )); 92 | std::process::exit(3); 93 | } 94 | } 95 | } 96 | Err(e) => { 97 | display_error(&format!( 98 | r#"Failed to load Grebuloff at {}! 99 | 100 | The error was: {:?} 101 | 102 | The game will now exit."#, 103 | root.dll_path.to_string().unwrap(), 104 | e 105 | )); 106 | std::process::exit(2); 107 | } 108 | } 109 | } 110 | 111 | unsafe extern "system" fn create_dxgi_factory_wrapper( 112 | _riid: *const (), 113 | pp_factory: *mut *mut c_void, 114 | ) -> HRESULT { 115 | // remove the IAT hook now that we've been called 116 | if let Some(hook) = IAT_HOOK.take() { 117 | ManuallyDrop::into_inner(hook); 118 | } else { 119 | display_error("...huh? IAT_HOOK was None..."); 120 | std::process::exit(5); 121 | } 122 | 123 | // load Grebuloff 124 | load_grebuloff(); 125 | 126 | // call CreateDXGIFactory1 from dxgi.dll 127 | // we use CreateDXGIFactory1 instead of CreateDXGIFactory, passing in IDXGIFactory2 as the riid, 128 | // to create a DXGI 1.2 factory, as opposed to the DXGI 1.0 factory that the game creates 129 | // this shouldn't break anything, but it does allow us to use surface sharing from Chromium 130 | // (once we implement that), for high-performance UI rendering 131 | let dxgi_dll = LoadLibraryA(PCSTR::from_raw(b"dxgi.dll\0".as_ptr())).unwrap(); 132 | let original_func: Option HRESULT> = 133 | GetProcAddress(dxgi_dll, PCSTR::from_raw(b"CreateDXGIFactory1\0".as_ptr())) 134 | .map(|func| std::mem::transmute(func)); 135 | 136 | // CreateDXGIFactory1() 137 | match original_func { 138 | Some(original_func) => original_func(&IDXGIFactory2::IID, pp_factory), 139 | None => { 140 | display_error("...huh? failed to find CreateDXGIFactory1 in dxgi.dll..."); 141 | std::process::exit(4); 142 | } 143 | } 144 | } 145 | 146 | fn display_error(msg: &str) { 147 | let msg = HSTRING::from(msg); 148 | let title = HSTRING::from("Grebuloff Loader"); 149 | unsafe { 150 | MessageBoxW(HWND::default(), &msg, &title, MB_OK | MB_ICONERROR); 151 | } 152 | } 153 | 154 | struct GrebuloffRoot { 155 | runtime_path: String, 156 | dll_path: PCSTR, 157 | } 158 | 159 | impl TryFrom for GrebuloffRoot { 160 | type Error = (); 161 | 162 | fn try_from(runtime_path: String) -> Result { 163 | let mut dll_path = std::path::PathBuf::from(&runtime_path); 164 | dll_path.push("grebuloff.dll"); 165 | 166 | if !dll_path.exists() { 167 | return Err(()); 168 | } 169 | 170 | let dll_path = dll_path.to_str().unwrap().to_owned(); 171 | 172 | Ok(GrebuloffRoot { 173 | runtime_path, 174 | dll_path: PCSTR::from_raw(dll_path.as_ptr()), 175 | }) 176 | } 177 | } 178 | 179 | fn get_grebuloff_root() -> GrebuloffRoot { 180 | // try in this order: 181 | // 1. `GREBULOFF_ROOT` env var, if set 182 | // 2. `grebuloff_root.txt` in the same directory as the game's EXE 183 | // 3. default to %AppData%\Grebuloff 184 | // if none of these exist, we can't continue - display an error message and exit 185 | std::env::var(ROOT_ENV_VAR) 186 | .or_else(|_| { 187 | // usually we'll be in the game directory, but we might not be 188 | // ensure we search for grebuloff_root.txt in the game directory 189 | let game_dir = unsafe { 190 | let mut exe_path = [0u16; 1024]; 191 | let exe_path_len = GetModuleFileNameW(None, &mut exe_path); 192 | 193 | std::path::PathBuf::from(std::ffi::OsString::from_wide( 194 | &exe_path[..exe_path_len as usize], 195 | )) 196 | }; 197 | 198 | std::fs::read_to_string( 199 | game_dir 200 | .parent() 201 | .map(|p| p.join(ROOT_FILE)) 202 | .unwrap_or(ROOT_FILE.into()), 203 | ) 204 | .map(|s| s.trim().to_owned()) 205 | }) 206 | .or_else(|_| { 207 | if let Ok(appdata) = std::env::var("APPDATA") { 208 | let mut path = std::path::PathBuf::from(appdata); 209 | path.push("Grebuloff"); 210 | if path.exists() { 211 | return Ok(path.to_str().map(|s| s.to_owned()).unwrap()); 212 | } 213 | } 214 | 215 | Err(()) 216 | }) 217 | .map(GrebuloffRoot::try_from) 218 | .unwrap_or_else(|_| { 219 | display_error(ERROR_NO_ROOT); 220 | std::process::exit(1); 221 | }) 222 | .unwrap() 223 | } 224 | 225 | unsafe fn redirect_dxgi() { 226 | let source = CString::new("CreateDXGIFactory").unwrap(); 227 | let obj = ObjectFile::open_main_program().unwrap(); 228 | 229 | for symbol in obj.symbols() { 230 | if symbol.name == source { 231 | // replace the address of CreateDXGIFactory with our own init function 232 | let _ = IAT_HOOK.insert(ManuallyDrop::new( 233 | obj.replace( 234 | source.to_str().unwrap(), 235 | create_dxgi_factory_wrapper as *const _, 236 | ) 237 | .unwrap(), 238 | )); 239 | 240 | break; 241 | } 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "grebuloff-macros" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | proc-macro = true 8 | 9 | [dependencies] 10 | proc-macro2 = "1.0.60" 11 | quote = "1.0.28" 12 | syn = { version = "2", features = ["full"] } 13 | walkdir = "2.3.3" 14 | -------------------------------------------------------------------------------- /macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::Ident; 2 | use quote::{format_ident, quote}; 3 | use syn::{Data, ItemFn, TraitItemFn, Type}; 4 | 5 | fn addr_table_name(name: String) -> Ident { 6 | format_ident!("{}AddressTable", name) 7 | } 8 | 9 | #[proc_macro_derive(VTable, attributes(vtable_base))] 10 | pub fn derive_vtable(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 11 | // find the field marked with vtable_base 12 | let input = syn::parse_macro_input!(input as syn::DeriveInput); 13 | 14 | let vtable_base = match input.data { 15 | Data::Struct(ref s) => { 16 | let vtable_base = s 17 | .fields 18 | .iter() 19 | .find(|f| f.attrs.iter().any(|a| a.path().is_ident("vtable_base"))) 20 | .expect("no field marked with #[vtable_base]"); 21 | 22 | match &vtable_base.ty { 23 | Type::Ptr(_) => vtable_base.ident.clone().unwrap(), 24 | _ => panic!("vtable_base field must be a pointer"), 25 | } 26 | } 27 | _ => panic!("#[derive(VTable)] can only be used on structs"), 28 | }; 29 | 30 | let struct_name = input.ident; 31 | let addr_table_name = addr_table_name(struct_name.to_string()); 32 | 33 | quote! { 34 | impl #struct_name { 35 | fn address_table(&self) -> #addr_table_name { 36 | #addr_table_name { 37 | base: self.#vtable_base as *const (), 38 | } 39 | } 40 | 41 | fn vtable_base(&self) -> *const () { 42 | self.#vtable_base as *const () 43 | } 44 | } 45 | 46 | struct #addr_table_name { 47 | base: *const (), 48 | } 49 | } 50 | .into() 51 | } 52 | 53 | #[proc_macro] 54 | pub fn vtable_functions(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 55 | let input = syn::parse_macro_input!(input as syn::ItemImpl); 56 | 57 | let struct_name = input.self_ty; 58 | let addr_table_name = addr_table_name(match struct_name.as_ref() { 59 | Type::Path(path) => path.path.segments.last().unwrap().ident.to_string(), 60 | _ => panic!("#[vtable_functions] can only be used on structs"), 61 | }); 62 | 63 | let mut output_addrs = quote! {}; 64 | let mut output_impls = quote! {}; 65 | 66 | for item in input.items { 67 | match item { 68 | syn::ImplItem::Verbatim(verbatim) => { 69 | let impl_fn = syn::parse2::(verbatim) 70 | .expect("vtable_functions only supports trait-like functions without bodies"); 71 | 72 | let fn_name = impl_fn.sig.ident; 73 | let vtable_index = impl_fn 74 | .attrs 75 | .iter() 76 | .find(|a| a.path().is_ident("vtable_fn")) 77 | .expect("no #[vtable_fn] attribute") 78 | .parse_args::() 79 | .expect("invalid #[vtable_fn] attribute") 80 | .base10_parse::() 81 | .expect("invalid #[vtable_fn] attribute"); 82 | 83 | // ensure the function is marked as unsafe 84 | if impl_fn.sig.unsafety.is_none() { 85 | panic!("#[vtable_fn] functions must be marked as unsafe"); 86 | } 87 | 88 | // preserve doc comments 89 | let doc = impl_fn 90 | .attrs 91 | .iter() 92 | .filter(|a| a.path().is_ident("doc")) 93 | .cloned() 94 | .collect::>(); 95 | 96 | // return c_void instead of the unit type, since that means we just don't know/don't care about the return type 97 | let return_type = match impl_fn.sig.output { 98 | syn::ReturnType::Default => quote! { *mut std::ffi::c_void }, 99 | syn::ReturnType::Type(_, ty) => quote! { #ty }, 100 | }; 101 | 102 | let mut args_input = vec![]; 103 | let mut args_typed = vec![]; 104 | let mut args_named = vec![]; 105 | 106 | for arg in impl_fn.sig.inputs.iter() { 107 | args_input.push(quote! { #arg }); 108 | 109 | match arg { 110 | syn::FnArg::Receiver(_) => { 111 | panic!("vtable_fn functions cannot take self as an arg (you probably want to use a *const / *mut pointer)"); 112 | } 113 | syn::FnArg::Typed(pat) => { 114 | let ty = &pat.ty; 115 | args_typed.push(quote! { #ty }); 116 | 117 | match &*pat.pat { 118 | syn::Pat::Ident(ident) => { 119 | args_named.push(quote! { #ident }); 120 | } 121 | _ => panic!("vtable_fn arguments must be named"), 122 | } 123 | } 124 | } 125 | } 126 | 127 | output_addrs = quote! { 128 | #output_addrs 129 | 130 | fn #fn_name (&self) -> *const *const () { 131 | unsafe { (self.base as *const usize).add(#vtable_index) as *const *const () } 132 | } 133 | }; 134 | 135 | output_impls = quote! { 136 | #output_impls 137 | 138 | #(#doc)* 139 | #[doc = ""] 140 | #[doc = " # Safety"] 141 | #[doc = ""] 142 | #[doc = " This function is unsafe because it calls a C++ virtual function by address."] 143 | unsafe fn #fn_name (&self, #(#args_input),*) -> #return_type { 144 | let address = self.address_table().#fn_name(); 145 | let func: extern "C" fn(#(#args_typed),*) -> #return_type = std::mem::transmute(address); 146 | func(#(#args_named),*) 147 | } 148 | }; 149 | } 150 | _ => panic!("vtable_functions only supports trait-like functions without bodies"), 151 | } 152 | } 153 | 154 | quote! { 155 | impl #addr_table_name { 156 | #output_addrs 157 | } 158 | 159 | impl #struct_name { 160 | #output_impls 161 | } 162 | } 163 | .into() 164 | } 165 | 166 | #[proc_macro_attribute] 167 | pub fn function_hook( 168 | _attr: proc_macro::TokenStream, 169 | input: proc_macro::TokenStream, 170 | ) -> proc_macro::TokenStream { 171 | let impl_fn = syn::parse_macro_input!(input as ItemFn); 172 | 173 | let fn_name = impl_fn.sig.ident; 174 | let hook_name = format_ident!("__hook__{}", fn_name); 175 | let detour_name = format_ident!("__detour__{}", fn_name); 176 | 177 | // ensure the function is marked as unsafe 178 | if impl_fn.sig.unsafety.is_none() { 179 | panic!("function hooks must be marked as unsafe"); 180 | } 181 | 182 | // preserve doc comments 183 | let doc = impl_fn 184 | .attrs 185 | .iter() 186 | .filter(|a| a.path().is_ident("doc")) 187 | .cloned() 188 | .collect::>(); 189 | 190 | let return_type = match impl_fn.sig.output { 191 | syn::ReturnType::Default => quote! { () }, 192 | syn::ReturnType::Type(_, ty) => quote! { #ty }, 193 | }; 194 | 195 | let mut args_input = vec![]; 196 | let mut args_named = vec![]; 197 | let mut fn_type_args = vec![]; 198 | 199 | for arg in impl_fn.sig.inputs.iter() { 200 | args_input.push(quote! { #arg }); 201 | 202 | match arg { 203 | syn::FnArg::Typed(pat) => { 204 | let ty = &pat.ty; 205 | fn_type_args.push(quote! { #ty }); 206 | 207 | match &*pat.pat { 208 | syn::Pat::Ident(ident) => { 209 | args_named.push(quote! { #ident }); 210 | } 211 | _ => panic!("function_hook arguments must be named"), 212 | } 213 | } 214 | _ => {} 215 | } 216 | } 217 | 218 | let fn_type = quote! { 219 | fn(#(#fn_type_args),*) -> #return_type 220 | }; 221 | let fn_body = impl_fn.block.stmts; 222 | 223 | // preserve calling convention, if specified on the function, otherwise default to C 224 | let abi = impl_fn 225 | .sig 226 | .abi 227 | .as_ref() 228 | .map(|abi| quote! { #abi }) 229 | .unwrap_or_else(|| quote! { extern "C" }); 230 | 231 | quote! { 232 | #[doc = "Auto-generated function hook."] 233 | #(#doc)* 234 | #[allow(non_upper_case_globals)] 235 | static_detour! { 236 | static #hook_name: unsafe #abi #fn_type; 237 | } 238 | 239 | #[doc = "Auto-generated function hook."] 240 | #[doc = ""] 241 | #(#doc)* 242 | #[doc = "# Safety"] 243 | #[doc = "This function is unsafe and should be treated as such, despite its lack of an `unsafe` keyword."] 244 | #[doc = "This function should not be called outside of hooked native game code."] 245 | #[allow(non_snake_case)] 246 | fn #detour_name (#(#args_input),*) -> #return_type { 247 | // wrap everything in an unsafe block here 248 | // we can't easily pass an unsafe function to initialize otherwise, since we would 249 | // have to wrap it in a closure, which would require knowing the closure signature, 250 | // which we don't know at compile time/from a proc macro, at least not easily 251 | let original = &#hook_name; 252 | unsafe { 253 | #(#fn_body)* 254 | } 255 | } 256 | } 257 | .into() 258 | } 259 | 260 | #[proc_macro] 261 | pub fn __fn_hook_symbol(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 262 | let fn_name = syn::parse_macro_input!(input as Ident); 263 | 264 | let sym = format_ident!("__hook__{}", fn_name); 265 | 266 | quote! { #sym }.into() 267 | } 268 | 269 | #[proc_macro] 270 | pub fn __fn_detour_symbol(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 271 | let fn_name = syn::parse_macro_input!(input as Ident); 272 | 273 | let sym = format_ident!("__detour__{}", fn_name); 274 | 275 | quote! { #sym }.into() 276 | } 277 | -------------------------------------------------------------------------------- /rpc/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "grebuloff-rpc" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | anyhow = { workspace = true } 8 | log = { workspace = true } 9 | tokio = { workspace = true } 10 | rmp = { workspace = true } 11 | rmp-serde = { workspace = true } 12 | serde = { workspace = true } 13 | serde_json = { workspace = true } 14 | bytes = { workspace = true } 15 | async-trait = "0.1.71" 16 | -------------------------------------------------------------------------------- /rpc/src/lib.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | pub mod ui; 4 | 5 | #[derive(Debug, PartialEq, Deserialize, Serialize)] 6 | #[serde(untagged)] 7 | pub enum RpcMessageDirection { 8 | /// Serverbound (client-to-server) communication. 9 | #[serde(skip_serializing)] 10 | Serverbound(RpcServerboundMessage), 11 | 12 | /// Clientbound (server-to-client) communication. 13 | #[serde(skip_deserializing)] 14 | Clientbound(RpcClientboundMessage), 15 | } 16 | 17 | #[derive(Debug, PartialEq, Deserialize)] 18 | pub enum RpcServerboundMessage { 19 | Ui(ui::UiRpcServerboundMessage), 20 | } 21 | 22 | #[derive(Debug, PartialEq, Serialize)] 23 | pub enum RpcClientboundMessage { 24 | Ui(ui::UiRpcClientboundMessage), 25 | } 26 | -------------------------------------------------------------------------------- /rpc/src/ui.rs: -------------------------------------------------------------------------------- 1 | use super::{RpcClientboundMessage, RpcServerboundMessage}; 2 | use anyhow::{bail, Result}; 3 | use bytes::{Buf, Bytes, BytesMut}; 4 | use serde::Deserialize; 5 | use serde::Serialize; 6 | 7 | #[derive(Debug, PartialEq, Deserialize)] 8 | pub enum UiRpcServerboundMessage {} 9 | 10 | impl TryFrom for UiRpcServerboundMessage { 11 | type Error = (); 12 | 13 | fn try_from(msg: RpcServerboundMessage) -> Result { 14 | match msg { 15 | RpcServerboundMessage::Ui(msg) => Ok(msg), 16 | _ => Err(()), 17 | } 18 | } 19 | } 20 | 21 | impl From for RpcClientboundMessage { 22 | fn from(msg: UiRpcClientboundMessage) -> Self { 23 | RpcClientboundMessage::Ui(msg) 24 | } 25 | } 26 | 27 | // note to future self: use actual structs instead of enum variant values 28 | // since rmp-serde doesn't properly (how we want it to, anyways) support 29 | // variant values 30 | #[derive(Debug, PartialEq, Serialize)] 31 | pub enum UiRpcClientboundMessage { 32 | /// Sent when the game window is resized. 33 | /// Triggers a resize of the UI. 34 | Resize(UiRpcClientboundResize), 35 | } 36 | 37 | #[derive(Debug, PartialEq)] 38 | pub struct UiRpcServerboundPaint { 39 | pub width: u16, 40 | pub height: u16, 41 | pub format: ImageFormat, 42 | pub data: Bytes, 43 | } 44 | 45 | impl UiRpcServerboundPaint { 46 | pub fn from_raw(mut buf: BytesMut) -> Result { 47 | let data = buf.split_off(5).freeze(); 48 | 49 | // image format is first, so we don't overlap 0x80..=0x8F | 0xDE..=0xDF (msgpack map) 50 | let format = match buf.get_u8() { 51 | 0 => ImageFormat::BGRA8, 52 | _ => bail!("invalid image format"), 53 | }; 54 | let width = buf.get_u16_le(); 55 | let height = buf.get_u16_le(); 56 | 57 | Ok(Self { 58 | width, 59 | height, 60 | format, 61 | data, 62 | }) 63 | } 64 | } 65 | 66 | #[derive(Debug, PartialEq, Serialize)] 67 | pub struct UiRpcClientboundResize { 68 | pub width: u32, 69 | pub height: u32, 70 | } 71 | 72 | /// Represents supported image formats. 73 | #[derive(Debug, PartialEq, Deserialize, Serialize)] 74 | #[repr(u8)] 75 | pub enum ImageFormat { 76 | BGRA8, 77 | } 78 | 79 | impl ImageFormat { 80 | pub fn byte_size_of(&self, width: usize, height: usize) -> usize { 81 | width * height * self.bytes_per_pixel() as usize 82 | } 83 | 84 | pub fn bytes_per_pixel(&self) -> u32 { 85 | match self { 86 | ImageFormat::BGRA8 => 4, 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | # Newer nightlies break V8 build currently - will be fixed in v8@0.74.0 3 | # I'd love to use stable here, but dll-syringe requires nightly features 4 | channel = "nightly-2023-06-02" 5 | -------------------------------------------------------------------------------- /src/dalamud.rs: -------------------------------------------------------------------------------- 1 | use log::info; 2 | use std::sync::Mutex; 3 | use std::time::Duration; 4 | use tokio::net::windows::named_pipe::{ClientOptions, NamedPipeClient}; 5 | use tokio::time; 6 | use windows::Win32::Foundation::ERROR_PIPE_BUSY; 7 | 8 | #[derive(Debug)] 9 | pub struct DalamudPipe { 10 | pipe_name: String, 11 | pipe_client: Mutex>, 12 | } 13 | 14 | impl DalamudPipe { 15 | pub fn new(pipe_name: &str) -> Self { 16 | Self { 17 | pipe_name: pipe_name.to_owned(), 18 | pipe_client: Mutex::new(None), 19 | } 20 | } 21 | 22 | pub async fn connect(&self) { 23 | let pipe_name = self.pipe_name.to_owned(); 24 | 25 | let client = loop { 26 | match ClientOptions::new().open(&pipe_name) { 27 | Ok(client) => break client, 28 | Err(e) if e.raw_os_error() == Some(ERROR_PIPE_BUSY.0 as i32) => (), 29 | Err(e) => panic!("failed to connect to Dalamud pipe: {}", e), 30 | } 31 | 32 | time::sleep(Duration::from_millis(50)).await; 33 | }; 34 | 35 | self.pipe_client.lock().unwrap().replace(client); 36 | 37 | info!("connected to Dalamud pipe at {}", pipe_name); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/hooking/framework.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Once; 2 | 3 | use anyhow::Result; 4 | use ffxiv_client_structs::generated::ffxiv::client::system::framework::{ 5 | Framework, Framework_Fn_Instance, 6 | }; 7 | use grebuloff_macros::{function_hook, vtable_functions, VTable}; 8 | use log::debug; 9 | 10 | use crate::{get_tokio_rt, hooking::create_function_hook}; 11 | 12 | #[derive(VTable)] 13 | struct FrameworkVTable { 14 | #[vtable_base] 15 | base: *mut *mut Framework, 16 | } 17 | 18 | vtable_functions!(impl FrameworkVTable { 19 | #[vtable_fn(1)] 20 | unsafe fn setup(this: *const Framework); 21 | 22 | #[vtable_fn(2)] 23 | unsafe fn destroy(this: *const Framework); 24 | 25 | #[vtable_fn(3)] 26 | unsafe fn free(this: *const Framework); 27 | 28 | #[vtable_fn(4)] 29 | unsafe fn tick(this: *const Framework) -> bool; 30 | }); 31 | 32 | pub unsafe fn hook_framework() -> Result<()> { 33 | let framework = 34 | ffxiv_client_structs::address::get::() as *mut *mut *mut Framework; 35 | assert!(!framework.is_null(), "failed to resolve Framework instance"); 36 | 37 | debug!("framework: {:p}", framework); 38 | let vtable = FrameworkVTable { base: *framework }; 39 | debug!("framework vtable: {:p}", vtable.base); 40 | 41 | create_function_hook!(tick, *vtable.address_table().tick()).enable()?; 42 | 43 | Ok(()) 44 | } 45 | 46 | #[function_hook] 47 | unsafe extern "C" fn tick(this: *const Framework) -> bool { 48 | static LATE_INIT: Once = Once::new(); 49 | LATE_INIT.call_once(|| { 50 | get_tokio_rt().block_on(crate::init_sync_late()); 51 | }); 52 | 53 | original.call(this) 54 | } 55 | -------------------------------------------------------------------------------- /src/hooking/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt, sync::OnceLock}; 2 | 3 | use anyhow::Result; 4 | use log::{debug, info}; 5 | use retour::StaticDetour; 6 | 7 | mod framework; 8 | mod swapchain; 9 | mod wndproc; 10 | 11 | static mut HOOK_MANAGER: OnceLock = OnceLock::new(); 12 | 13 | pub struct HookManager { 14 | pub hooks: Vec, 15 | } 16 | 17 | impl HookManager { 18 | pub fn instance() -> &'static Self { 19 | unsafe { HOOK_MANAGER.get().unwrap() } 20 | } 21 | 22 | pub fn dump_hooks(&self) { 23 | info!("dump_hooks: {} total hooks registered", self.hooks.len()); 24 | for hook in &self.hooks { 25 | info!("dump_hooks: {:?}", hook); 26 | } 27 | } 28 | } 29 | 30 | /// A lightweight pointer to a function hook. 31 | #[derive(Copy, Clone)] 32 | pub struct FunctionHook { 33 | pub name: &'static str, 34 | pub address: usize, 35 | detour: &'static dyn FunctionHookOps, 36 | } 37 | 38 | pub trait FunctionHookOps { 39 | unsafe fn enable(&self) -> Result<()>; 40 | unsafe fn disable(&self) -> Result<()>; 41 | fn is_enabled(&self) -> bool; 42 | } 43 | 44 | impl FunctionHookOps for StaticDetour { 45 | unsafe fn enable(&self) -> Result<()> { 46 | Ok(self.enable()?) 47 | } 48 | 49 | unsafe fn disable(&self) -> Result<()> { 50 | Ok(self.disable()?) 51 | } 52 | 53 | fn is_enabled(&self) -> bool { 54 | self.is_enabled() 55 | } 56 | } 57 | 58 | impl fmt::Debug for FunctionHook { 59 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 60 | write!( 61 | f, 62 | "{} @ 0x{:X} (enabled: {})", 63 | self.name, 64 | self.address, 65 | self.detour.is_enabled() 66 | ) 67 | } 68 | } 69 | 70 | impl FunctionHook { 71 | pub fn new(name: &'static str, address: usize, detour: &'static dyn FunctionHookOps) -> Self { 72 | let hook = Self { 73 | name, 74 | address, 75 | detour, 76 | }; 77 | 78 | debug!("[hook] register: {}", hook.name); 79 | unsafe { HOOK_MANAGER.get_mut().unwrap().hooks.push(hook) }; 80 | 81 | hook 82 | } 83 | 84 | pub unsafe fn enable(&self) -> Result<()> { 85 | debug!("[hook] enable: {}", self.name); 86 | self.detour.enable() 87 | } 88 | 89 | pub unsafe fn disable(&self) -> Result<()> { 90 | debug!("[hook] disable: {}", self.name); 91 | self.detour.disable() 92 | } 93 | 94 | pub fn is_enabled(&self) -> bool { 95 | self.detour.is_enabled() 96 | } 97 | } 98 | 99 | pub unsafe fn init_early_hooks() -> Result<()> { 100 | info!("initializing hook manager"); 101 | HOOK_MANAGER.get_or_init(|| HookManager { hooks: Vec::new() }); 102 | 103 | info!("initializing early hooks"); 104 | framework::hook_framework()?; 105 | 106 | Ok(()) 107 | } 108 | 109 | pub unsafe fn init_hooks() -> Result<()> { 110 | info!("initializing hooks"); 111 | 112 | swapchain::hook_swap_chain()?; 113 | // wndproc::hook_wndproc()?; 114 | 115 | Ok(()) 116 | } 117 | 118 | macro_rules! create_function_hook { 119 | ($detour:ident, $target:expr) => {{ 120 | ::grebuloff_macros::__fn_hook_symbol!($detour).initialize( 121 | ::std::mem::transmute($target), 122 | ::grebuloff_macros::__fn_detour_symbol!($detour), 123 | )?; 124 | crate::hooking::FunctionHook::new( 125 | concat!(module_path!(), "::", stringify!($detour)), 126 | $target as usize, 127 | &::grebuloff_macros::__fn_hook_symbol!($detour), 128 | ) 129 | }}; 130 | } 131 | pub(crate) use create_function_hook; 132 | -------------------------------------------------------------------------------- /src/hooking/shaders/common.hlsli: -------------------------------------------------------------------------------- 1 | SamplerState PointSampler : register(s0); 2 | Texture2D Texture : register(t0); 3 | 4 | struct Interpolators 5 | { 6 | float4 Position : SV_Position; 7 | float2 TexCoord : TEXCOORD0; 8 | }; 9 | -------------------------------------------------------------------------------- /src/hooking/shaders/ps.cso: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avafloww/Grebuloff/345a8c7988b14b501c4c5c7258c056921e00fa32/src/hooking/shaders/ps.cso -------------------------------------------------------------------------------- /src/hooking/shaders/ps.hlsl: -------------------------------------------------------------------------------- 1 | #include "common.hlsli" 2 | 3 | float4 main(Interpolators In) : SV_Target0 4 | { 5 | float4 col = Texture.Sample(PointSampler, In.TexCoord); 6 | 7 | // Electron for Windows uses RGBA, and even though we can specify the texture as BGRA, it still 8 | // seems to perform faster when we swizzle the colour here instead of changing the D3D11_TEXTURE2D_DESC. 9 | return float4(col.b, col.g, col.r, col.a); 10 | } 11 | -------------------------------------------------------------------------------- /src/hooking/shaders/vs.cso: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avafloww/Grebuloff/345a8c7988b14b501c4c5c7258c056921e00fa32/src/hooking/shaders/vs.cso -------------------------------------------------------------------------------- /src/hooking/shaders/vs.hlsl: -------------------------------------------------------------------------------- 1 | #include "common.hlsli" 2 | 3 | Interpolators main(uint vI : SV_VertexId) 4 | { 5 | Interpolators output; 6 | 7 | float2 texcoord = float2((vI << 1) & 2, vI & 2); 8 | output.TexCoord = texcoord; 9 | output.Position = float4(texcoord.x * 2 - 1, -texcoord.y * 2 + 1, 0, 1); 10 | 11 | return output; 12 | } 13 | -------------------------------------------------------------------------------- /src/hooking/swapchain.rs: -------------------------------------------------------------------------------- 1 | use crate::{hooking::create_function_hook, rpc::ui::UiRpcServer, ui}; 2 | use anyhow::Result; 3 | use ffxiv_client_structs::generated::ffxiv::client::graphics::kernel::{ 4 | Device, Device_Fn_Instance, 5 | }; 6 | use grebuloff_macros::{function_hook, vtable_functions, VTable}; 7 | use log::{debug, trace, warn}; 8 | use std::{ 9 | cell::{RefCell, RefMut}, 10 | mem::MaybeUninit, 11 | ptr::addr_of_mut, 12 | }; 13 | use windows::Win32::{ 14 | Foundation::RECT, 15 | Graphics::{ 16 | Direct3D::{ 17 | D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP, D3D11_SRV_DIMENSION_TEXTURE2D, 18 | D3D_PRIMITIVE_TOPOLOGY, 19 | }, 20 | Direct3D11::*, 21 | Dxgi::{ 22 | Common::{DXGI_FORMAT, DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_SAMPLE_DESC}, 23 | IDXGISwapChain, DXGI_SWAP_CHAIN_DESC, 24 | }, 25 | }, 26 | }; 27 | 28 | thread_local! { 29 | static RENDER_DATA: RefCell> = RefCell::new(None); 30 | } 31 | 32 | #[derive(VTable)] 33 | struct ResolvedSwapChain { 34 | #[vtable_base] 35 | base: *mut *mut IDXGISwapChain, 36 | } 37 | 38 | vtable_functions!(impl ResolvedSwapChain { 39 | #[vtable_fn(8)] 40 | unsafe fn present(this: *mut IDXGISwapChain, sync_interval: u32, present_flags: u32); 41 | 42 | #[vtable_fn(13)] 43 | unsafe fn resize_buffers( 44 | this: *mut IDXGISwapChain, 45 | buffer_count: u32, 46 | width: u32, 47 | height: u32, 48 | new_format: u32, 49 | swap_chain_flags: u32, 50 | ); 51 | }); 52 | 53 | unsafe fn resolve_swap_chain() -> ResolvedSwapChain { 54 | debug!("resolving swap chain"); 55 | let device = loop { 56 | let device = ffxiv_client_structs::address::get::() as *mut Device; 57 | 58 | if device.is_null() { 59 | trace!("device is null, waiting"); 60 | std::thread::sleep(std::time::Duration::from_millis(100)); 61 | continue; 62 | } 63 | 64 | break device; 65 | }; 66 | 67 | debug!("device: {:p}", device); 68 | 69 | let swap_chain = (*device).swap_chain; 70 | assert!(!swap_chain.is_null(), "swap chain is null"); 71 | debug!("swap chain: {:p}", swap_chain); 72 | 73 | let dxgi_swap_chain = (*swap_chain).dxgiswap_chain as *mut *mut *mut IDXGISwapChain; 74 | assert!(!dxgi_swap_chain.is_null(), "dxgi swap chain is null"); 75 | debug!("dxgi swap chain: {:p}", *dxgi_swap_chain); 76 | 77 | ResolvedSwapChain { 78 | base: *dxgi_swap_chain, 79 | } 80 | } 81 | 82 | pub unsafe fn hook_swap_chain() -> Result<()> { 83 | let resolved = resolve_swap_chain(); 84 | 85 | create_function_hook!(present, *resolved.address_table().present()).enable()?; 86 | create_function_hook!(resize_buffers, *resolved.address_table().resize_buffers()).enable()?; 87 | 88 | Ok(()) 89 | } 90 | 91 | /// Stores data that is used for rendering our UI overlay. 92 | struct RenderData { 93 | /// Used to sanity-check that we're rendering into the correct context. 94 | sc_addr: *const IDXGISwapChain, 95 | /// The render target view for the swap chain's back buffer. 96 | rtv: ID3D11RenderTargetView, 97 | /// The texture we render into. 98 | texture: ID3D11Texture2D, 99 | srv: ID3D11ShaderResourceView, 100 | pixel_shader: ID3D11PixelShader, 101 | vertex_shader: ID3D11VertexShader, 102 | sampler: ID3D11SamplerState, 103 | blend_state: ID3D11BlendState, 104 | depth_stencil_state: ID3D11DepthStencilState, 105 | rasterizer_state: ID3D11RasterizerState, 106 | viewport: D3D11_VIEWPORT, 107 | scissor_rect: RECT, 108 | buffer_width: u32, 109 | buffer_height: u32, 110 | } 111 | 112 | #[function_hook] 113 | unsafe extern "stdcall" fn resize_buffers( 114 | this: IDXGISwapChain, 115 | buffer_count: u32, 116 | width: u32, 117 | height: u32, 118 | new_format: DXGI_FORMAT, 119 | swapchain_flags: u32, 120 | ) -> i32 { 121 | // using a separate block scope here to make extra sure that we don't 122 | // accidentally hold onto any old resources when calling the original 123 | { 124 | RENDER_DATA.with(|cell| { 125 | let cell = cell.borrow_mut(); 126 | if cell.is_some() { 127 | trace!("calling initialize_render_data from IDXGISwapChain::ResizeBuffers"); 128 | initialize_render_data(&this, cell, Some((width, height))); 129 | } 130 | }); 131 | } 132 | 133 | // inform the UI host of the new size 134 | UiRpcServer::resize(width, height); 135 | 136 | original.call( 137 | this, 138 | buffer_count, 139 | width, 140 | height, 141 | new_format, 142 | swapchain_flags, 143 | ) 144 | } 145 | 146 | #[function_hook] 147 | unsafe extern "stdcall" fn present( 148 | this: IDXGISwapChain, 149 | sync_interval: u32, 150 | present_flags: u32, 151 | ) -> i32 { 152 | let device: ID3D11Device2 = this.GetDevice().unwrap(); 153 | 154 | RENDER_DATA.with(move |cell| { 155 | let mut cell = cell.borrow_mut(); 156 | if cell.is_none() { 157 | // initialize the render data 158 | trace!("calling initialize_render_data from IDXGISwapChain::Present"); 159 | cell = initialize_render_data(&this, cell, None); 160 | } 161 | 162 | // borrow it as mutable now 163 | let data = cell.as_mut().unwrap(); 164 | 165 | let context = device.GetImmediateContext().unwrap(); 166 | 167 | // use a new scope here to ensure the state backup is dropped at the end, 168 | // thus restoring the original render state before we call the original function 169 | { 170 | let _ = RenderStateBackup::new(device.GetImmediateContext().unwrap()); 171 | 172 | // poll to see if we have new data, and if so, update the texture 173 | if let Some(snapshot) = ui::poll_dirty() { 174 | let mut mapped = MaybeUninit::::zeroed(); 175 | context 176 | .Map( 177 | &data.texture, 178 | 0, 179 | D3D11_MAP_WRITE_DISCARD, 180 | 0, 181 | Some(mapped.as_mut_ptr()), 182 | ) 183 | .expect("Map failed"); 184 | let mut mapped = mapped.assume_init(); 185 | 186 | if snapshot.width * 4 != mapped.RowPitch 187 | || mapped.RowPitch * snapshot.height != mapped.DepthPitch 188 | { 189 | warn!("latest UI snapshot does not match our current UI size, skipping update"); 190 | } else { 191 | let src = snapshot.data.as_ptr(); 192 | let dst = mapped.pData as *mut u8; 193 | 194 | mapped.RowPitch = snapshot.width * 4; 195 | mapped.DepthPitch = snapshot.width * snapshot.height * 4; 196 | 197 | let size = (data.buffer_width as usize * data.buffer_height as usize * 4) 198 | .min(snapshot.data.len()); 199 | std::ptr::copy_nonoverlapping(src, dst, size); 200 | 201 | context.Unmap(&data.texture, 0); 202 | } 203 | } 204 | 205 | // render the overlay 206 | context.RSSetViewports(Some(&[data.viewport])); 207 | context.RSSetScissorRects(Some(&[data.scissor_rect])); 208 | context.RSSetState(&data.rasterizer_state); 209 | 210 | context.IASetInputLayout(None); 211 | context.IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP); 212 | context.IASetVertexBuffers(0, 0, None, None, None); 213 | 214 | context.VSSetShader(&data.vertex_shader, None); 215 | context.PSSetShader(&data.pixel_shader, None); 216 | 217 | context.PSSetShaderResources(0, Some(&[Some(data.srv.clone())])); 218 | context.PSSetSamplers(0, Some(&[Some(data.sampler.clone())])); 219 | 220 | context.OMSetBlendState(&data.blend_state, None, 0xffffffff); 221 | context.OMSetDepthStencilState(&data.depth_stencil_state, 0); 222 | 223 | context.OMSetRenderTargets(Some(&[Some(data.rtv.clone())]), None); 224 | 225 | context.Draw(3, 0); 226 | } 227 | 228 | // call the original function 229 | original.call(this, sync_interval, present_flags) 230 | }) 231 | } 232 | 233 | unsafe fn initialize_render_data<'c>( 234 | this: &IDXGISwapChain, 235 | mut cell: RefMut<'c, Option>, 236 | size: Option<(u32, u32)>, 237 | ) -> RefMut<'c, Option> { 238 | let device: ID3D11Device2 = this.GetDevice().unwrap(); 239 | 240 | // initialize our render data for this thread (the render thread) 241 | trace!("initializing RenderData (initialize_render_data)"); 242 | 243 | let (viewport_width, viewport_height) = match size { 244 | Some(_) => size.unwrap(), 245 | None => { 246 | let sc_desc = { 247 | let mut sc_desc = MaybeUninit::::zeroed(); 248 | this.GetDesc(sc_desc.as_mut_ptr()) 249 | .expect("failed to get DXGI_SWAP_CHAIN_DESC"); 250 | sc_desc.assume_init() 251 | }; 252 | 253 | (sc_desc.BufferDesc.Width, sc_desc.BufferDesc.Height) 254 | } 255 | }; 256 | 257 | let texture = { 258 | let texture_desc = D3D11_TEXTURE2D_DESC { 259 | Width: viewport_width, 260 | Height: viewport_height, 261 | MipLevels: 1, 262 | ArraySize: 1, 263 | // despite our input image being BGRA, it seems faster to specify 264 | // RGBA here and then swizzle the channels in the pixel shader, 265 | // likely because the game's native format is RGBA? idk 266 | // it could be placebo, but we'll roll with it 267 | Format: DXGI_FORMAT_R8G8B8A8_UNORM, 268 | SampleDesc: DXGI_SAMPLE_DESC { 269 | Count: 1, 270 | Quality: 0, 271 | }, 272 | Usage: D3D11_USAGE_DYNAMIC, 273 | BindFlags: D3D11_BIND_SHADER_RESOURCE, 274 | CPUAccessFlags: D3D11_CPU_ACCESS_WRITE, 275 | ..Default::default() 276 | }; 277 | 278 | let mut tex = MaybeUninit::>::zeroed(); 279 | device 280 | .CreateTexture2D(&texture_desc, None, Some(tex.as_mut_ptr())) 281 | .expect("CreateTexture2D failed"); 282 | tex.assume_init().expect("CreateTexture2D returned null") 283 | }; 284 | 285 | // create the shader resource view 286 | let srv = { 287 | let srv_desc = D3D11_SHADER_RESOURCE_VIEW_DESC { 288 | Format: DXGI_FORMAT_R8G8B8A8_UNORM, 289 | ViewDimension: D3D11_SRV_DIMENSION_TEXTURE2D, 290 | Anonymous: D3D11_SHADER_RESOURCE_VIEW_DESC_0 { 291 | Texture2D: D3D11_TEX2D_SRV { 292 | MostDetailedMip: 0, 293 | MipLevels: 1, 294 | }, 295 | }, 296 | }; 297 | 298 | let mut srv = MaybeUninit::>::zeroed(); 299 | device 300 | .CreateShaderResourceView(&texture, Some(&srv_desc), Some(srv.as_mut_ptr())) 301 | .expect("CreateShaderResourceView failed"); 302 | srv.assume_init() 303 | .expect("CreateShaderResourceView returned null") 304 | }; 305 | 306 | // create the pixel shader 307 | let pixel_shader = { 308 | let ps_bytecode = include_bytes!("shaders/ps.cso"); 309 | let mut ps = MaybeUninit::>::zeroed(); 310 | device 311 | .CreatePixelShader(ps_bytecode, None, Some(ps.as_mut_ptr())) 312 | .expect("CreatePixelShader failed"); 313 | ps.assume_init().expect("CreatePixelShader returned null") 314 | }; 315 | 316 | // create the vertex shader 317 | let vertex_shader = { 318 | let vs_bytecode = include_bytes!("shaders/vs.cso"); 319 | let mut vs = MaybeUninit::>::zeroed(); 320 | device 321 | .CreateVertexShader(vs_bytecode, None, Some(vs.as_mut_ptr())) 322 | .expect("CreateVertexShader failed"); 323 | vs.assume_init().expect("CreateVertexShader returned null") 324 | }; 325 | 326 | // create the linear clamp sampler 327 | let sampler = { 328 | let sampler_desc = D3D11_SAMPLER_DESC { 329 | Filter: D3D11_FILTER_MIN_MAG_MIP_POINT, 330 | AddressU: D3D11_TEXTURE_ADDRESS_CLAMP, 331 | AddressV: D3D11_TEXTURE_ADDRESS_CLAMP, 332 | AddressW: D3D11_TEXTURE_ADDRESS_CLAMP, 333 | ComparisonFunc: D3D11_COMPARISON_ALWAYS, 334 | MinLOD: 0.0, 335 | MaxLOD: 1.0, 336 | MipLODBias: 0.0, 337 | MaxAnisotropy: 0, 338 | BorderColor: [0.0; 4], 339 | }; 340 | 341 | let mut sampler = MaybeUninit::>::zeroed(); 342 | device 343 | .CreateSamplerState(&sampler_desc, Some(sampler.as_mut_ptr())) 344 | .expect("CreateSamplerState failed"); 345 | sampler 346 | .assume_init() 347 | .expect("CreateSamplerState returned null") 348 | }; 349 | 350 | // create alpha blend state 351 | let blend_state = { 352 | let blend_desc = D3D11_BLEND_DESC { 353 | AlphaToCoverageEnable: false.into(), 354 | RenderTarget: [ 355 | D3D11_RENDER_TARGET_BLEND_DESC { 356 | BlendEnable: true.into(), 357 | SrcBlend: D3D11_BLEND_SRC_ALPHA, 358 | DestBlend: D3D11_BLEND_INV_SRC_ALPHA, 359 | BlendOp: D3D11_BLEND_OP_ADD, 360 | SrcBlendAlpha: D3D11_BLEND_INV_SRC_ALPHA, 361 | DestBlendAlpha: D3D11_BLEND_ZERO, 362 | BlendOpAlpha: D3D11_BLEND_OP_ADD, 363 | RenderTargetWriteMask: D3D11_COLOR_WRITE_ENABLE_ALL.0 as u8, 364 | }, 365 | D3D11_RENDER_TARGET_BLEND_DESC { 366 | ..Default::default() 367 | }, 368 | D3D11_RENDER_TARGET_BLEND_DESC { 369 | ..Default::default() 370 | }, 371 | D3D11_RENDER_TARGET_BLEND_DESC { 372 | ..Default::default() 373 | }, 374 | D3D11_RENDER_TARGET_BLEND_DESC { 375 | ..Default::default() 376 | }, 377 | D3D11_RENDER_TARGET_BLEND_DESC { 378 | ..Default::default() 379 | }, 380 | D3D11_RENDER_TARGET_BLEND_DESC { 381 | ..Default::default() 382 | }, 383 | D3D11_RENDER_TARGET_BLEND_DESC { 384 | ..Default::default() 385 | }, 386 | ], 387 | ..Default::default() 388 | }; 389 | 390 | let mut blend_state = MaybeUninit::>::zeroed(); 391 | device 392 | .CreateBlendState(&blend_desc, Some(blend_state.as_mut_ptr())) 393 | .expect("CreateBlendState failed"); 394 | blend_state 395 | .assume_init() 396 | .expect("CreateBlendState returned null") 397 | }; 398 | 399 | // create cull none rasterizer state 400 | let rasterizer_state = { 401 | let rasterizer_desc = D3D11_RASTERIZER_DESC { 402 | FillMode: D3D11_FILL_SOLID, 403 | CullMode: D3D11_CULL_NONE, 404 | DepthClipEnable: false.into(), 405 | ScissorEnable: true.into(), 406 | ..Default::default() 407 | }; 408 | 409 | let mut rasterizer_state = MaybeUninit::>::zeroed(); 410 | device 411 | .CreateRasterizerState(&rasterizer_desc, Some(rasterizer_state.as_mut_ptr())) 412 | .expect("CreateRasterizerState failed"); 413 | rasterizer_state 414 | .assume_init() 415 | .expect("CreateRasterizerState returned null") 416 | }; 417 | 418 | // create depth stencil state with no depth 419 | let depth_stencil_state = { 420 | let depth_stencil_desc = D3D11_DEPTH_STENCIL_DESC { 421 | DepthEnable: false.into(), 422 | DepthWriteMask: D3D11_DEPTH_WRITE_MASK_ALL, 423 | DepthFunc: D3D11_COMPARISON_ALWAYS, 424 | StencilEnable: false.into(), 425 | FrontFace: D3D11_DEPTH_STENCILOP_DESC { 426 | StencilFailOp: D3D11_STENCIL_OP_KEEP, 427 | StencilDepthFailOp: D3D11_STENCIL_OP_KEEP, 428 | StencilPassOp: D3D11_STENCIL_OP_KEEP, 429 | StencilFunc: D3D11_COMPARISON_ALWAYS, 430 | }, 431 | BackFace: D3D11_DEPTH_STENCILOP_DESC { 432 | StencilFailOp: D3D11_STENCIL_OP_KEEP, 433 | StencilDepthFailOp: D3D11_STENCIL_OP_KEEP, 434 | StencilPassOp: D3D11_STENCIL_OP_KEEP, 435 | StencilFunc: D3D11_COMPARISON_ALWAYS, 436 | }, 437 | ..Default::default() 438 | }; 439 | 440 | let mut depth_stencil_state = MaybeUninit::>::zeroed(); 441 | device 442 | .CreateDepthStencilState(&depth_stencil_desc, Some(depth_stencil_state.as_mut_ptr())) 443 | .expect("CreateDepthStencilState failed"); 444 | depth_stencil_state 445 | .assume_init() 446 | .expect("CreateDepthStencilState returned null") 447 | }; 448 | 449 | // viewport 450 | let viewport = D3D11_VIEWPORT { 451 | TopLeftX: 0.0, 452 | TopLeftY: 0.0, 453 | Width: viewport_width as f32, 454 | Height: viewport_height as f32, 455 | MinDepth: 0.0, 456 | MaxDepth: 1.0, 457 | }; 458 | 459 | // scissor rect 460 | let scissor_rect = RECT { 461 | left: 0, 462 | top: 0, 463 | right: viewport_width as i32, 464 | bottom: viewport_height as i32, 465 | }; 466 | 467 | // init render target view 468 | let rtv = { 469 | let back_buffer: ID3D11Texture2D = this.GetBuffer(0).expect("failed to get back buffer"); 470 | let mut rtv = None; 471 | 472 | device 473 | .CreateRenderTargetView(&back_buffer, None, Some(&mut rtv)) 474 | .expect("failed to create render target view (CreateRenderTargetView not ok)"); 475 | 476 | rtv.expect("failed to create render target view (was null)") 477 | }; 478 | 479 | // set the cell with the initialized data 480 | *cell = Some(RenderData { 481 | sc_addr: this, 482 | rtv, 483 | texture, 484 | srv, 485 | pixel_shader, 486 | vertex_shader, 487 | blend_state, 488 | rasterizer_state, 489 | depth_stencil_state, 490 | sampler, 491 | viewport, 492 | scissor_rect, 493 | buffer_width: viewport_width, 494 | buffer_height: viewport_height, 495 | }); 496 | 497 | cell 498 | } 499 | 500 | // let (mut out, out_ptr, mut out_count) = temp_array!(D3D11_VIEWPORT, D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE); 501 | macro_rules! temp_array { 502 | ($type: ident, $capacity: ident) => {{ 503 | let mut out: MaybeUninit<[$type; $capacity as usize]> = MaybeUninit::zeroed(); 504 | let out_ptr = out.as_mut_ptr(); 505 | let out_count = $capacity as u32; 506 | 507 | (out, out_ptr, out_count) 508 | }}; 509 | 510 | (Option<$opt_type: ident>, $capacity: ident) => {{ 511 | let mut out: MaybeUninit<[Option<$opt_type>; $capacity as usize]> = MaybeUninit::zeroed(); 512 | let out_ptr = out.as_mut_ptr(); 513 | let out_count = $capacity as u32; 514 | 515 | (out, out_ptr, out_count) 516 | }}; 517 | } 518 | 519 | // reconcile_array!(out, out_count); 520 | macro_rules! reconcile_array { 521 | ($out: ident, $out_count: ident) => {{ 522 | let mut vec = Vec::with_capacity($out_count as usize); 523 | vec.extend_from_slice(&$out.assume_init()[0..$out_count as usize]); 524 | vec 525 | }}; 526 | } 527 | 528 | macro_rules! backup_shaders { 529 | ( 530 | $context: ident, $obj_ptr: ident, 531 | ($shader_field: ident, $class_inst_field: ident) => $get_shader: ident, 532 | $constant_buf_field: ident => $get_constant_buf: ident, 533 | $resource_field: ident => $get_shader_resources: ident, 534 | $samplers_field: ident => $get_samplers: ident$(,)? 535 | ) => {{ 536 | // save shader 537 | { 538 | let (out, out_ptr, mut out_count) = 539 | temp_array!(Option, D3D11_SHADER_MAX_INTERFACES); 540 | 541 | $context.$get_shader( 542 | addr_of_mut!((*$obj_ptr).$shader_field), 543 | Some(out_ptr as *mut _), 544 | Some(&mut out_count), 545 | ); 546 | 547 | addr_of_mut!((*$obj_ptr).$class_inst_field).write(reconcile_array!(out, out_count)); 548 | } 549 | 550 | // save constant buffers 551 | { 552 | let (out, out_ptr, out_count) = temp_array!( 553 | Option, 554 | D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT 555 | ); 556 | 557 | $context.$get_constant_buf(0, Some(&mut *out_ptr)); 558 | 559 | addr_of_mut!((*$obj_ptr).$constant_buf_field).write(reconcile_array!(out, out_count)); 560 | } 561 | 562 | // save resources 563 | { 564 | let (out, out_ptr, out_count) = temp_array!( 565 | Option, 566 | D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT 567 | ); 568 | 569 | $context.$get_shader_resources(0, Some(&mut *out_ptr)); 570 | 571 | addr_of_mut!((*$obj_ptr).$resource_field).write(reconcile_array!(out, out_count)); 572 | } 573 | 574 | // save samplers 575 | { 576 | let (out, out_ptr, out_count) = temp_array!( 577 | Option, 578 | D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT 579 | ); 580 | 581 | $context.$get_samplers(0, Some(&mut *out_ptr)); 582 | 583 | addr_of_mut!((*$obj_ptr).$samplers_field).write(reconcile_array!(out, out_count)); 584 | } 585 | }}; 586 | } 587 | 588 | macro_rules! restore_shaders { 589 | ( 590 | $context: expr, 591 | ($shader_field: expr, $class_inst_field: expr) => $set_shader: ident, 592 | $constant_buf_field: expr => $set_constant_buf: ident, 593 | $resource_field: expr => $set_shader_resources: ident, 594 | $samplers_field: expr => $set_samplers: ident$(,)? 595 | ) => {{ 596 | $context.$set_shader($shader_field.as_ref(), Some($class_inst_field.as_slice())); 597 | $context.$set_constant_buf(0, Some($constant_buf_field.as_slice())); 598 | $context.$set_shader_resources(0, Some($resource_field.as_slice())); 599 | $context.$set_samplers(0, Some($samplers_field.as_slice())); 600 | }}; 601 | } 602 | 603 | struct RenderStateBackup { 604 | context: ID3D11DeviceContext, 605 | 606 | // ### IA ### 607 | ia_input_layout: Option, 608 | ia_vertex_buffers: Vec>, 609 | ia_vertex_buffer_strides: Vec, 610 | ia_vertex_buffer_offsets: Vec, 611 | ia_index_buffer: Option, 612 | ia_index_buffer_format: DXGI_FORMAT, 613 | ia_index_buffer_offset: u32, 614 | ia_primitive_topology: D3D_PRIMITIVE_TOPOLOGY, 615 | 616 | // ### RS ### 617 | rs_state: Option, 618 | rs_viewport: Vec, 619 | rs_scissor_rect: Vec, 620 | 621 | // ### OM ### 622 | om_blend_state: Option, 623 | om_blend_factor: f32, 624 | om_sample_mask: u32, 625 | om_depth_stencil_state: Option, 626 | om_depth_stencil_ref: u32, 627 | om_render_targets: Vec>, 628 | om_depth_stencil_view: Option, 629 | 630 | // ### VS ### 631 | vs_shader: Option, 632 | vs_class_instances: Vec>, 633 | vs_constant_buffers: Vec>, 634 | vs_shader_resources: Vec>, 635 | vs_samplers: Vec>, 636 | 637 | // ### HS ### 638 | hs_shader: Option, 639 | hs_class_instances: Vec>, 640 | hs_constant_buffers: Vec>, 641 | hs_shader_resources: Vec>, 642 | hs_samplers: Vec>, 643 | 644 | // ### DS ### 645 | ds_shader: Option, 646 | ds_class_instances: Vec>, 647 | ds_constant_buffers: Vec>, 648 | ds_shader_resources: Vec>, 649 | ds_samplers: Vec>, 650 | 651 | // ### GS ### 652 | gs_shader: Option, 653 | gs_class_instances: Vec>, 654 | gs_constant_buffers: Vec>, 655 | gs_shader_resources: Vec>, 656 | gs_samplers: Vec>, 657 | 658 | // ### PS ### 659 | ps_shader: Option, 660 | ps_class_instances: Vec>, 661 | ps_constant_buffers: Vec>, 662 | ps_shader_resources: Vec>, 663 | ps_samplers: Vec>, 664 | 665 | // ### CS ### 666 | cs_shader: Option, 667 | cs_class_instances: Vec>, 668 | cs_constant_buffers: Vec>, 669 | cs_shader_resources: Vec>, 670 | cs_samplers: Vec>, 671 | } 672 | 673 | impl RenderStateBackup { 674 | #[allow(const_item_mutation)] 675 | pub unsafe fn new(context: ID3D11DeviceContext) -> Self { 676 | // why are some of these using `as *mut _`? 677 | // this is why: https://github.com/microsoft/windows-rs/issues/1567 678 | let mut obj = MaybeUninit::::zeroed(); 679 | let obj_ptr = obj.as_mut_ptr(); 680 | 681 | Self::backup_ia(&context, obj_ptr); 682 | Self::backup_rs(&context, obj_ptr); 683 | Self::backup_om(&context, obj_ptr); 684 | Self::backup_vs(&context, obj_ptr); 685 | Self::backup_hs(&context, obj_ptr); 686 | Self::backup_ds(&context, obj_ptr); 687 | Self::backup_gs(&context, obj_ptr); 688 | Self::backup_ps(&context, obj_ptr); 689 | Self::backup_cs(&context, obj_ptr); 690 | 691 | // save the context 692 | addr_of_mut!((*obj_ptr).context).write(context); 693 | 694 | obj.assume_init() 695 | } 696 | 697 | unsafe fn backup_ia(context: &ID3D11DeviceContext, obj_ptr: *mut Self) { 698 | // save input layout 699 | { 700 | addr_of_mut!((*obj_ptr).ia_input_layout).write(context.IAGetInputLayout().ok()); 701 | } 702 | 703 | // save index buffer 704 | { 705 | context.IAGetIndexBuffer( 706 | Some(addr_of_mut!((*obj_ptr).ia_index_buffer)), 707 | Some(addr_of_mut!((*obj_ptr).ia_index_buffer_format)), 708 | Some(addr_of_mut!((*obj_ptr).ia_index_buffer_offset)), 709 | ); 710 | } 711 | 712 | // save primitive topology 713 | { 714 | addr_of_mut!((*obj_ptr).ia_primitive_topology).write(context.IAGetPrimitiveTopology()); 715 | } 716 | 717 | // save vertex buffers 718 | { 719 | let (buf_out, mut buf_out_ptr, buf_out_count) = temp_array!( 720 | Option, 721 | D3D11_IA_VERTEX_INPUT_RESOURCE_SLOT_COUNT 722 | ); 723 | let (stride_out, mut stride_out_ptr, stride_out_count) = 724 | temp_array!(u32, D3D11_IA_VERTEX_INPUT_RESOURCE_SLOT_COUNT); 725 | let (offset_out, mut offset_out_ptr, offset_out_count) = 726 | temp_array!(u32, D3D11_IA_VERTEX_INPUT_RESOURCE_SLOT_COUNT); 727 | 728 | context.IAGetVertexBuffers( 729 | 0, 730 | D3D11_IA_VERTEX_INPUT_RESOURCE_SLOT_COUNT, 731 | Some(addr_of_mut!(buf_out_ptr) as *mut _), 732 | Some(addr_of_mut!(stride_out_ptr) as *mut _), 733 | Some(addr_of_mut!(offset_out_ptr) as *mut _), 734 | ); 735 | 736 | addr_of_mut!((*obj_ptr).ia_vertex_buffers) 737 | .write(reconcile_array!(buf_out, buf_out_count)); 738 | addr_of_mut!((*obj_ptr).ia_vertex_buffer_strides) 739 | .write(reconcile_array!(stride_out, stride_out_count)); 740 | addr_of_mut!((*obj_ptr).ia_vertex_buffer_offsets) 741 | .write(reconcile_array!(offset_out, offset_out_count)); 742 | } 743 | } 744 | 745 | unsafe fn backup_rs(context: &ID3D11DeviceContext, obj_ptr: *mut Self) { 746 | // save rasterizer state 747 | { 748 | addr_of_mut!((*obj_ptr).rs_state).write(context.RSGetState().ok()); 749 | } 750 | 751 | // save viewport 752 | { 753 | let (out, mut out_ptr, mut out_count) = temp_array!( 754 | D3D11_VIEWPORT, 755 | D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE 756 | ); 757 | 758 | context.RSGetViewports(&mut out_count, Some(addr_of_mut!(out_ptr) as *mut _)); 759 | 760 | addr_of_mut!((*obj_ptr).rs_viewport).write(reconcile_array!(out, out_count)); 761 | } 762 | 763 | // save scissor rects 764 | { 765 | let (out, mut out_ptr, mut out_count) = temp_array!( 766 | RECT, 767 | D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE 768 | ); 769 | 770 | context.RSGetScissorRects(&mut out_count, Some(addr_of_mut!(out_ptr) as *mut _)); 771 | 772 | addr_of_mut!((*obj_ptr).rs_scissor_rect).write(reconcile_array!(out, out_count)); 773 | } 774 | } 775 | 776 | unsafe fn backup_om(context: &ID3D11DeviceContext, obj_ptr: *mut Self) { 777 | // save blend state 778 | { 779 | context.OMGetBlendState( 780 | Some(addr_of_mut!((*obj_ptr).om_blend_state)), 781 | Some(addr_of_mut!((*obj_ptr).om_blend_factor)), 782 | Some(addr_of_mut!((*obj_ptr).om_sample_mask)), 783 | ); 784 | } 785 | 786 | // save depth stencil state 787 | { 788 | context.OMGetDepthStencilState( 789 | Some(addr_of_mut!((*obj_ptr).om_depth_stencil_state)), 790 | Some(addr_of_mut!((*obj_ptr).om_depth_stencil_ref)), 791 | ); 792 | } 793 | 794 | // save render targets 795 | { 796 | context.OMGetRenderTargets( 797 | Some(&mut *addr_of_mut!((*obj_ptr).om_render_targets)), 798 | Some(addr_of_mut!((*obj_ptr).om_depth_stencil_view)), 799 | ); 800 | } 801 | } 802 | 803 | unsafe fn backup_vs(context: &ID3D11DeviceContext, obj_ptr: *mut Self) { 804 | backup_shaders!( 805 | context, obj_ptr, 806 | (vs_shader, vs_class_instances) => VSGetShader, 807 | vs_constant_buffers => VSGetConstantBuffers, 808 | vs_shader_resources => VSGetShaderResources, 809 | vs_samplers => VSGetSamplers, 810 | ); 811 | } 812 | 813 | unsafe fn backup_hs(context: &ID3D11DeviceContext, obj_ptr: *mut Self) { 814 | backup_shaders!( 815 | context, obj_ptr, 816 | (hs_shader, hs_class_instances) => HSGetShader, 817 | hs_constant_buffers => HSGetConstantBuffers, 818 | hs_shader_resources => HSGetShaderResources, 819 | hs_samplers => HSGetSamplers, 820 | ); 821 | } 822 | 823 | unsafe fn backup_ds(context: &ID3D11DeviceContext, obj_ptr: *mut Self) { 824 | backup_shaders!( 825 | context, obj_ptr, 826 | (ds_shader, ds_class_instances) => DSGetShader, 827 | ds_constant_buffers => DSGetConstantBuffers, 828 | ds_shader_resources => DSGetShaderResources, 829 | ds_samplers => DSGetSamplers, 830 | ); 831 | } 832 | 833 | unsafe fn backup_gs(context: &ID3D11DeviceContext, obj_ptr: *mut Self) { 834 | backup_shaders!( 835 | context, obj_ptr, 836 | (gs_shader, gs_class_instances) => GSGetShader, 837 | gs_constant_buffers => GSGetConstantBuffers, 838 | gs_shader_resources => GSGetShaderResources, 839 | gs_samplers => GSGetSamplers, 840 | ); 841 | } 842 | 843 | unsafe fn backup_ps(context: &ID3D11DeviceContext, obj_ptr: *mut Self) { 844 | backup_shaders!( 845 | context, obj_ptr, 846 | (ps_shader, ps_class_instances) => PSGetShader, 847 | ps_constant_buffers => PSGetConstantBuffers, 848 | ps_shader_resources => PSGetShaderResources, 849 | ps_samplers => PSGetSamplers, 850 | ); 851 | } 852 | 853 | unsafe fn backup_cs(context: &ID3D11DeviceContext, obj_ptr: *mut Self) { 854 | backup_shaders!( 855 | context, obj_ptr, 856 | (cs_shader, cs_class_instances) => CSGetShader, 857 | cs_constant_buffers => CSGetConstantBuffers, 858 | cs_shader_resources => CSGetShaderResources, 859 | cs_samplers => CSGetSamplers, 860 | ); 861 | } 862 | 863 | unsafe fn restore_ia(&self) { 864 | self.context.IASetInputLayout(self.ia_input_layout.as_ref()); 865 | self.context 866 | .IASetPrimitiveTopology(self.ia_primitive_topology); 867 | self.context.IASetVertexBuffers( 868 | 0, 869 | self.ia_vertex_buffers.len() as u32, 870 | Some(self.ia_vertex_buffers.as_ptr()), 871 | Some(self.ia_vertex_buffer_strides.as_ptr()), 872 | Some(self.ia_vertex_buffer_offsets.as_ptr()), 873 | ); 874 | } 875 | 876 | unsafe fn restore_rs(&self) { 877 | self.context.RSSetState(self.rs_state.as_ref()); 878 | self.context 879 | .RSSetViewports(Some(self.rs_viewport.as_slice())); 880 | self.context 881 | .RSSetScissorRects(Some(self.rs_scissor_rect.as_slice())); 882 | } 883 | 884 | unsafe fn restore_om(&self) { 885 | self.context.OMSetBlendState( 886 | self.om_blend_state.as_ref(), 887 | Some(&self.om_blend_factor), 888 | self.om_sample_mask, 889 | ); 890 | self.context.OMSetDepthStencilState( 891 | self.om_depth_stencil_state.as_ref(), 892 | self.om_depth_stencil_ref, 893 | ); 894 | self.context.OMSetRenderTargets( 895 | Some(&self.om_render_targets), 896 | self.om_depth_stencil_view.as_ref(), 897 | ); 898 | } 899 | 900 | unsafe fn restore_vs(&self) { 901 | restore_shaders!( 902 | self.context, 903 | (self.vs_shader, self.vs_class_instances) => VSSetShader, 904 | self.vs_constant_buffers => VSSetConstantBuffers, 905 | self.vs_shader_resources => VSSetShaderResources, 906 | self.vs_samplers => VSSetSamplers, 907 | ); 908 | } 909 | 910 | unsafe fn restore_hs(&self) { 911 | restore_shaders!( 912 | self.context, 913 | (self.hs_shader, self.hs_class_instances) => HSSetShader, 914 | self.hs_constant_buffers => HSSetConstantBuffers, 915 | self.hs_shader_resources => HSSetShaderResources, 916 | self.hs_samplers => HSSetSamplers, 917 | ); 918 | } 919 | 920 | unsafe fn restore_ds(&self) { 921 | restore_shaders!( 922 | self.context, 923 | (self.ds_shader, self.ds_class_instances) => DSSetShader, 924 | self.ds_constant_buffers => DSSetConstantBuffers, 925 | self.ds_shader_resources => DSSetShaderResources, 926 | self.ds_samplers => DSSetSamplers, 927 | ); 928 | } 929 | 930 | unsafe fn restore_gs(&self) { 931 | restore_shaders!( 932 | self.context, 933 | (self.gs_shader, self.gs_class_instances) => GSSetShader, 934 | self.gs_constant_buffers => GSSetConstantBuffers, 935 | self.gs_shader_resources => GSSetShaderResources, 936 | self.gs_samplers => GSSetSamplers, 937 | ); 938 | } 939 | 940 | unsafe fn restore_ps(&self) { 941 | restore_shaders!( 942 | self.context, 943 | (self.ps_shader, self.ps_class_instances) => PSSetShader, 944 | self.ps_constant_buffers => PSSetConstantBuffers, 945 | self.ps_shader_resources => PSSetShaderResources, 946 | self.ps_samplers => PSSetSamplers, 947 | ); 948 | } 949 | 950 | unsafe fn restore_cs(&self) { 951 | restore_shaders!( 952 | self.context, 953 | (self.cs_shader, self.cs_class_instances) => CSSetShader, 954 | self.cs_constant_buffers => CSSetConstantBuffers, 955 | self.cs_shader_resources => CSSetShaderResources, 956 | self.cs_samplers => CSSetSamplers, 957 | ); 958 | } 959 | } 960 | 961 | /// Restores the render state that was backed up in the constructor. 962 | impl Drop for RenderStateBackup { 963 | fn drop(&mut self) { 964 | unsafe { 965 | self.restore_ia(); 966 | self.restore_rs(); 967 | self.restore_om(); 968 | self.restore_vs(); 969 | self.restore_hs(); 970 | self.restore_ds(); 971 | self.restore_gs(); 972 | self.restore_ps(); 973 | self.restore_cs(); 974 | } 975 | } 976 | } 977 | -------------------------------------------------------------------------------- /src/hooking/wndproc.rs: -------------------------------------------------------------------------------- 1 | use super::create_function_hook; 2 | use crate::resolvers::resolve_signature; 3 | use anyhow::Result; 4 | use grebuloff_macros::function_hook; 5 | use log::debug; 6 | use windows::Win32::Foundation::{HWND, LPARAM, LRESULT, WPARAM}; 7 | 8 | pub unsafe fn hook_wndproc() -> Result<()> { 9 | let wndproc_ptr = resolve_signature!("E8 ?? ?? ?? ?? 80 7C 24 ?? ?? 74 ?? B8"); 10 | debug!("WndProc: {:p}", wndproc_ptr); 11 | create_function_hook!(wndproc, wndproc_ptr).enable()?; 12 | 13 | Ok(()) 14 | } 15 | 16 | #[function_hook] 17 | unsafe fn wndproc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT { 18 | debug!( 19 | "WndProc invoked with: hwnd = {:?}, msg = {}, wparam = {:?}, lparam = {:?}", 20 | hwnd, msg, wparam, lparam 21 | ); 22 | original.call(hwnd, msg, wparam, lparam) 23 | } 24 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod dalamud; 2 | mod hooking; 3 | mod resolvers; 4 | mod rpc; 5 | mod ui; 6 | 7 | #[macro_use] 8 | extern crate retour; 9 | #[macro_use] 10 | extern crate serde; 11 | 12 | use crate::{ 13 | dalamud::DalamudPipe, 14 | rpc::{ui::UiRpcServer, RpcServer}, 15 | }; 16 | use anyhow::Result; 17 | use log::{debug, error, info}; 18 | use msgbox::IconType; 19 | use std::sync::OnceLock; 20 | use std::thread; 21 | use std::{ffi::CString, path::PathBuf}; 22 | use tokio::task; 23 | 24 | static TOKIO_RT: OnceLock = OnceLock::new(); 25 | static DALAMUD_PIPE: OnceLock = OnceLock::new(); 26 | static EXEC_ID: OnceLock = OnceLock::new(); 27 | static RUNTIME_DIR: OnceLock = OnceLock::new(); 28 | static LOAD_METHOD: OnceLock = OnceLock::new(); 29 | 30 | dll_syringe::payload_procedure! { 31 | fn init_injected(runtime_dir: CString) { 32 | init_sync_rt(&runtime_dir, None) 33 | } 34 | } 35 | 36 | dll_syringe::payload_procedure! { 37 | fn init_dalamud(runtime_dir: CString, dalamud_pipe_name: CString) { 38 | init_sync_rt(&runtime_dir, Some(&dalamud_pipe_name)) 39 | } 40 | } 41 | 42 | #[no_mangle] 43 | pub unsafe extern "system" fn init_loader(runtime_dir: &CString) { 44 | LOAD_METHOD.set(GrebuloffLoadMethod::Loader).unwrap(); 45 | init_sync_rt(runtime_dir, None) 46 | } 47 | 48 | #[derive(Copy, Clone, Debug, Eq, PartialEq)] 49 | pub enum GrebuloffLoadMethod { 50 | Loader, 51 | Injected, 52 | Dalamud, 53 | } 54 | 55 | impl GrebuloffLoadMethod { 56 | pub fn controls_its_own_destiny(self) -> bool { 57 | match self { 58 | GrebuloffLoadMethod::Dalamud => false, 59 | _ => true, 60 | } 61 | } 62 | } 63 | 64 | pub fn get_load_method() -> GrebuloffLoadMethod { 65 | *LOAD_METHOD.get().unwrap() 66 | } 67 | 68 | pub fn get_execution_id() -> String { 69 | EXEC_ID.get().unwrap().clone() 70 | } 71 | 72 | fn setup_logging(dir: &PathBuf) { 73 | // log to grebuloff.log in the specified directory 74 | // log format should have timestamps, level, module, and message 75 | fern::Dispatch::new() 76 | .format(|out, message, record| { 77 | out.finish(format_args!( 78 | "[{}] [{}] {}", 79 | chrono::Local::now().format("%Y-%m-%d %H:%M:%S"), 80 | record.level(), 81 | message 82 | )) 83 | }) 84 | .chain(fern::log_file(dir.join("grebuloff.log")).unwrap()) 85 | .apply() 86 | .unwrap(); 87 | 88 | // log on panic 89 | std::panic::set_hook(Box::new(|info| { 90 | let backtrace = std::backtrace::Backtrace::force_capture(); 91 | 92 | let thread = thread::current(); 93 | let thread = thread.name().unwrap_or(""); 94 | let msg = match info.payload().downcast_ref::<&'static str>() { 95 | Some(s) => *s, 96 | None => match info.payload().downcast_ref::() { 97 | Some(s) => &**s, 98 | None => "Box", 99 | }, 100 | }; 101 | 102 | let formatted = match info.location() { 103 | Some(location) => { 104 | format!( 105 | "thread '{}' panicked at '{}': {}:{}\nbacktrace:\n{:?}", 106 | thread, 107 | msg, 108 | location.file(), 109 | location.line(), 110 | backtrace 111 | ) 112 | } 113 | None => format!( 114 | "thread '{}' panicked at '{}'\nbacktrace:\n{:?}", 115 | thread, msg, backtrace 116 | ), 117 | }; 118 | 119 | error!("{}", formatted); 120 | log::logger().flush(); 121 | 122 | msgbox::create("Grebuloff", &formatted, IconType::Error).unwrap(); 123 | })); 124 | } 125 | 126 | fn init_sync_rt(runtime_dir: &CString, dalamud_pipe_name: Option<&CString>) { 127 | if LOAD_METHOD.get().is_none() { 128 | LOAD_METHOD 129 | .set(if dalamud_pipe_name.is_some() { 130 | GrebuloffLoadMethod::Dalamud 131 | } else { 132 | GrebuloffLoadMethod::Injected 133 | }) 134 | .unwrap(); 135 | } 136 | 137 | // generate an execution ID used for pipe communication 138 | EXEC_ID 139 | .set(uuid::Uuid::new_v4().to_string()) 140 | .expect("failed to set execution ID"); 141 | 142 | let runtime_dir = PathBuf::from(std::str::from_utf8(runtime_dir.as_bytes()).unwrap()); 143 | 144 | // set up logging early 145 | setup_logging(&runtime_dir); 146 | 147 | RUNTIME_DIR.set(runtime_dir).unwrap(); 148 | 149 | // set up the tokio runtime 150 | TOKIO_RT 151 | .set( 152 | tokio::runtime::Builder::new_multi_thread() 153 | .enable_all() 154 | .build() 155 | .unwrap(), 156 | ) 157 | .expect("failed to set tokio runtime"); 158 | 159 | let tokio_rt = TOKIO_RT.get().unwrap(); 160 | 161 | // run the sync init on the tokio runtime 162 | tokio_rt.block_on(init_sync_early( 163 | dalamud_pipe_name 164 | .map(CString::as_bytes) 165 | .map(std::str::from_utf8) 166 | .transpose() 167 | .unwrap(), 168 | )); 169 | } 170 | 171 | async fn init_sync_early(dalamud_pipe_name: Option<&str>) { 172 | if let Some(pipe_name) = dalamud_pipe_name { 173 | DALAMUD_PIPE 174 | .set(DalamudPipe::new(&pipe_name)) 175 | .expect("failed to set Dalamud pipe"); 176 | } 177 | 178 | info!("--------------------------------------------------"); 179 | info!( 180 | "Grebuloff Low-Level Runtime starting (load method: {:?})", 181 | get_load_method(), 182 | ); 183 | info!("Build time: {}", env!("BUILD_TIMESTAMP")); 184 | info!("Git commit: {}", env!("GIT_DESCRIBE")); 185 | info!("Execution ID: {}", get_execution_id()); 186 | 187 | // resolve clientstructs 188 | unsafe { resolvers::init_resolvers(get_load_method()) } 189 | .await 190 | .expect("failed to init resolvers"); 191 | 192 | // initialize early hooks (framework) 193 | unsafe { hooking::init_early_hooks() }.expect("failed to init early hooks"); 194 | 195 | match get_load_method() { 196 | GrebuloffLoadMethod::Loader => { 197 | // if we're loaded by the loader, we're loading very early 198 | // into the game's boot process. we need to wait for 199 | // Framework::Tick to be called by the game, so we just 200 | // return here and wait for the game to call us back 201 | debug!("waiting for framework tick before continuing init"); 202 | } 203 | _ => { 204 | // if we're loaded by anything else, we're loading later 205 | // into the boot process, so we shouldn't wait - call 206 | // init_sync_late now 207 | init_sync_late().await; 208 | } 209 | } 210 | } 211 | 212 | pub async fn init_sync_late() { 213 | info!("late sync init starting"); 214 | 215 | // start attempting connection to the Dalamud pipe, if applicable 216 | if let Some(pipe) = DALAMUD_PIPE.get() { 217 | task::spawn(pipe.connect()); 218 | } 219 | 220 | // handle anything that needs to be loaded sync first 221 | // core hooks 222 | unsafe { hooking::init_hooks() }.expect("failed to init hooks"); 223 | 224 | // call async init now 225 | task::spawn(init_async()); 226 | } 227 | 228 | async fn init_async() -> Result<()> { 229 | info!("async init starting"); 230 | 231 | // start RPC for the UI server 232 | task::spawn(async { UiRpcServer::instance().listen_forever().await }); 233 | 234 | // start the UI server itself 235 | task::spawn(async move { ui::spawn_ui_host(RUNTIME_DIR.get().clone().unwrap()).await }); 236 | 237 | // run the main loop 238 | // this is the last thing that should be called in init_async 239 | // let mut interval = time::interval(time::Duration::from_millis(1000)); 240 | 241 | // loop { 242 | // interval.tick().await; 243 | // trace!("in main loop"); 244 | // hooking::HookManager::instance().dump_hooks(); 245 | // } 246 | 247 | Ok(()) 248 | } 249 | 250 | pub fn get_tokio_rt() -> &'static tokio::runtime::Runtime { 251 | TOKIO_RT.get().unwrap() 252 | } 253 | -------------------------------------------------------------------------------- /src/resolvers/dalamud.rs: -------------------------------------------------------------------------------- 1 | use ffxiv_client_structs::MemberFunctionSignature; 2 | 3 | pub unsafe fn resolve_member_function(_input: &MemberFunctionSignature) -> *const u8 { 4 | // TODO 5 | panic!("not yet implemented"); 6 | } 7 | -------------------------------------------------------------------------------- /src/resolvers/mod.rs: -------------------------------------------------------------------------------- 1 | mod dalamud; 2 | mod native; 3 | 4 | use crate::{get_load_method, GrebuloffLoadMethod}; 5 | use anyhow::Result; 6 | use ffxiv_client_structs::MemberFunctionSignature; 7 | use log::info; 8 | 9 | pub async unsafe fn init_resolvers(load_method: GrebuloffLoadMethod) -> Result<()> { 10 | info!("init resolvers: {:?}", load_method); 11 | native::prepare()?; 12 | 13 | ffxiv_client_structs::resolve_all_async( 14 | native::resolve_vtable, 15 | native::resolve_static_address, 16 | if load_method.controls_its_own_destiny() { 17 | native::resolve_member_function 18 | } else { 19 | dalamud::resolve_member_function 20 | }, 21 | ) 22 | .await; 23 | 24 | Ok(()) 25 | } 26 | 27 | /// Internal helper function used by the `resolve_signature` macro. 28 | pub unsafe fn resolve_member_function(input: &MemberFunctionSignature) -> *const u8 { 29 | if get_load_method().controls_its_own_destiny() { 30 | native::resolve_member_function(input) 31 | } else { 32 | dalamud::resolve_member_function(input) 33 | } 34 | } 35 | 36 | /// Resolves a signature to a pointer. 37 | /// Returns a null pointer if the signature could not be resolved. 38 | macro_rules! resolve_signature { 39 | ($signature: tt) => {{ 40 | let member_func = ::ffxiv_client_structs::MemberFunctionSignature::new( 41 | ::ffxiv_client_structs_macros::signature!($signature), 42 | ); 43 | 44 | crate::resolvers::resolve_member_function(&member_func) 45 | }}; 46 | } 47 | pub(crate) use resolve_signature; 48 | -------------------------------------------------------------------------------- /src/resolvers/native.rs: -------------------------------------------------------------------------------- 1 | use anyhow::bail; 2 | use ffxiv_client_structs::{ 3 | MemberFunctionSignature, Signature, StaticAddressSignature, VTableSignature, 4 | }; 5 | use log::{debug, info, warn}; 6 | use windows::Win32::System::LibraryLoader::GetModuleHandleA; 7 | use windows::Win32::System::ProcessStatus::{GetModuleInformation, MODULEINFO}; 8 | use windows::Win32::System::Threading::GetCurrentProcess; 9 | 10 | static mut MODULE_START: *const u8 = std::ptr::null(); 11 | static mut MODULE_SIZE: usize = 0; 12 | 13 | static mut TEXT_START: *const u8 = std::ptr::null(); 14 | static mut TEXT_SIZE: usize = 0; 15 | 16 | static mut RDATA_START: *const u8 = std::ptr::null(); 17 | static mut RDATA_SIZE: usize = 0; 18 | 19 | static mut DATA_START: *const u8 = std::ptr::null(); 20 | static mut DATA_SIZE: usize = 0; 21 | 22 | pub unsafe fn prepare() -> anyhow::Result<()> { 23 | let handle = GetModuleHandleA(None)?; 24 | let mut info = std::mem::zeroed::(); 25 | let result = GetModuleInformation( 26 | GetCurrentProcess(), 27 | handle, 28 | &mut info, 29 | std::mem::size_of::() as u32, 30 | ); 31 | 32 | if !result.as_bool() { 33 | bail!("GetModuleInformation failed"); 34 | } 35 | 36 | info!( 37 | "found module base: {:X}, size: {:X}", 38 | info.lpBaseOfDll as usize, info.SizeOfImage 39 | ); 40 | 41 | MODULE_START = info.lpBaseOfDll as *const u8; 42 | MODULE_SIZE = info.SizeOfImage as usize; 43 | 44 | // adapted from FFXIVClientStructs.Resolver, which was adapted from Dalamud SigScanner 45 | let base_address = info.lpBaseOfDll as *const u8; 46 | 47 | // We don't want to read all of IMAGE_DOS_HEADER or IMAGE_NT_HEADER stuff so we cheat here. 48 | let nt_new_offset = std::ptr::read_unaligned(base_address.offset(0x3C) as *const i32); 49 | let nt_header = base_address.offset(nt_new_offset as isize); 50 | 51 | // IMAGE_NT_HEADER 52 | let file_header = nt_header.offset(4); 53 | let num_sections = std::ptr::read_unaligned(file_header.offset(6) as *const i16); 54 | 55 | // IMAGE_OPTIONAL_HEADER 56 | let optional_header = file_header.offset(20); 57 | 58 | let section_header = optional_header.offset(240); // IMAGE_OPTIONAL_HEADER64 59 | 60 | // IMAGE_SECTION_HEADER 61 | let mut section_cursor = section_header; 62 | for _ in 0..num_sections { 63 | let section_name = std::ptr::read_unaligned(section_cursor as *const i64); 64 | 65 | // .text 66 | match section_name { 67 | 0x747865742E => { 68 | // .text 69 | TEXT_START = base_address.offset(std::ptr::read_unaligned( 70 | section_cursor.offset(12) as *const i32 71 | ) as isize); 72 | TEXT_SIZE = 73 | std::ptr::read_unaligned(section_cursor.offset(8) as *const i32) as usize; 74 | } 75 | 0x617461642E => { 76 | // .data 77 | DATA_START = base_address.offset(std::ptr::read_unaligned( 78 | section_cursor.offset(12) as *const i32 79 | ) as isize); 80 | DATA_SIZE = 81 | std::ptr::read_unaligned(section_cursor.offset(8) as *const i32) as usize; 82 | } 83 | 0x61746164722E => { 84 | // .rdata 85 | RDATA_START = base_address.offset(std::ptr::read_unaligned( 86 | section_cursor.offset(12) as *const i32, 87 | ) as isize); 88 | RDATA_SIZE = 89 | std::ptr::read_unaligned(section_cursor.offset(8) as *const i32) as usize; 90 | } 91 | _ => {} 92 | } 93 | 94 | section_cursor = section_cursor.offset(40); // advance by 40 95 | } 96 | 97 | info!( 98 | "image sections: .text {:X}(+{:X}), .data {:X}(+{:X}), .rdata {:X}(+{:X})", 99 | TEXT_START as usize, 100 | TEXT_SIZE, 101 | DATA_START as usize, 102 | DATA_SIZE, 103 | RDATA_START as usize, 104 | RDATA_SIZE 105 | ); 106 | 107 | Ok(()) 108 | } 109 | 110 | pub unsafe fn resolve_vtable(input: &VTableSignature) -> *const u8 { 111 | let sig_result = find_sig(TEXT_START, TEXT_SIZE, &input.signature); 112 | 113 | if sig_result == std::ptr::null() { 114 | warn!("resolve_vtable: couldn't resolve {}", input.signature); 115 | 116 | sig_result 117 | } else { 118 | // get the 4 bytes at input.offset bytes past the result 119 | let access_offset = std::ptr::read_unaligned(sig_result.offset(input.offset) as *const i32); 120 | let mut result = 121 | sig_result.offset(input.offset + 4 + access_offset as isize) as *const usize; 122 | 123 | if input.is_pointer { 124 | // dereference the pointer 125 | result = std::ptr::read_unaligned(result as *const *const usize); 126 | } 127 | 128 | debug!( 129 | "resolve_vtable: resolved {} (offset {}, is_pointer {}) - {:p}", 130 | input.signature, input.offset, input.is_pointer, sig_result 131 | ); 132 | 133 | result as *const u8 134 | } 135 | } 136 | 137 | pub unsafe fn resolve_static_address(input: &StaticAddressSignature) -> *const u8 { 138 | let sig_result = find_sig(TEXT_START, TEXT_SIZE, &input.signature); 139 | 140 | if sig_result == std::ptr::null() { 141 | warn!( 142 | "resolve_static_address: couldn't resolve {}", 143 | input.signature 144 | ); 145 | 146 | sig_result 147 | } else { 148 | // get the 4 bytes at input.offset bytes past the result 149 | let access_offset = std::ptr::read_unaligned(sig_result.offset(input.offset) as *const i32); 150 | let mut result = 151 | sig_result.offset(input.offset + 4 + access_offset as isize) as *const usize; 152 | 153 | if input.is_pointer { 154 | // dereference the pointer 155 | result = std::ptr::read_unaligned(result as *const *const usize); 156 | } 157 | 158 | debug!( 159 | "resolve_static_address: resolved {} (offset {}, is_pointer {}, access_offset {:X}, sig_result {:p}) - {:p} p, {:X} x", 160 | input.signature, input.offset, input.is_pointer, access_offset, sig_result, result, result as usize 161 | ); 162 | 163 | result as *const u8 164 | } 165 | } 166 | 167 | pub unsafe fn resolve_member_function(input: &MemberFunctionSignature) -> *const u8 { 168 | let result = find_sig(TEXT_START, TEXT_SIZE, &input.signature); 169 | 170 | if result == std::ptr::null() { 171 | warn!( 172 | "resolve_member_function: couldn't resolve {}", 173 | input.signature 174 | ); 175 | } else { 176 | debug!( 177 | "resolve_member_function: resolved {} - {:p}", 178 | input.signature, result 179 | ); 180 | } 181 | 182 | result 183 | } 184 | 185 | unsafe fn find_sig(start_addr: *const u8, size: usize, sig: &Signature) -> *const u8 { 186 | let sig_len = sig.bytes.len(); 187 | 188 | // we use two cursors here to handle edge cases 189 | // first, we iterate over the entire memory region 190 | 'prog: for pi in 0..size { 191 | let prog_cursor = start_addr.add(pi); 192 | 193 | // next, we attempt to match the entire signature from the program cursor 194 | for si in 0..sig_len { 195 | let sig_cursor = prog_cursor.add(si); 196 | 197 | let valid = sig.mask[si] == 0x00 || sig.bytes[si] == *sig_cursor; 198 | if !valid { 199 | continue 'prog; 200 | } 201 | } 202 | 203 | // if we get here, we found the signature 204 | let b = *prog_cursor; 205 | return if b == 0xE8 || b == 0xE9 { 206 | // relative call 207 | let offset = std::ptr::read_unaligned(prog_cursor.add(1) as *const i32); 208 | prog_cursor.add(5).offset(offset as isize) 209 | } else { 210 | prog_cursor 211 | }; 212 | } 213 | 214 | std::ptr::null() 215 | } 216 | -------------------------------------------------------------------------------- /src/rpc/mod.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, bail, Result}; 2 | use async_trait::async_trait; 3 | use bytes::{Buf, BytesMut}; 4 | use grebuloff_rpc::{RpcClientboundMessage, RpcMessageDirection, RpcServerboundMessage}; 5 | use log::{error, info}; 6 | use rustc_hash::FxHashMap; 7 | use serde::{Deserialize, Serialize}; 8 | use std::{any::Any, borrow::Cow, ffi::OsString, sync::OnceLock}; 9 | use tokio::{ 10 | io::{AsyncReadExt, AsyncWriteExt}, 11 | net::windows::named_pipe::{NamedPipeServer, PipeMode, ServerOptions}, 12 | sync::{mpsc, Mutex}, 13 | }; 14 | 15 | pub mod ui; 16 | 17 | static mut CLIENT_STATE: OnceLock>>> = 18 | OnceLock::new(); 19 | 20 | pub struct RpcServerOptions { 21 | pub pipe_name: Cow<'static, str>, 22 | pub buffer_size: usize, 23 | } 24 | 25 | struct RpcServerClientState 26 | where 27 | C: Into + Send + 'static, 28 | { 29 | pub send: mpsc::UnboundedSender, 30 | } 31 | 32 | async fn with_client_state( 33 | server_name: &'static str, 34 | f: impl FnOnce(&mut RpcServerClientState) -> T, 35 | ) -> Result 36 | where 37 | C: Into + Send + 'static, 38 | { 39 | let mut state = unsafe { &CLIENT_STATE } 40 | .get_or_init(|| Mutex::new(FxHashMap::default())) 41 | .lock() 42 | .await; 43 | let state = state.get_mut(server_name); 44 | 45 | match state { 46 | Some(state) => { 47 | let state = state.downcast_mut::>().unwrap(); 48 | Ok(f(state)) 49 | } 50 | None => bail!("no client state for server {}", server_name), 51 | } 52 | } 53 | 54 | async fn set_client_state(server_name: &'static str, new_state: Option>) 55 | where 56 | C: Into + Send + 'static, 57 | { 58 | let mut state_map = unsafe { &CLIENT_STATE } 59 | .get_or_init(|| Mutex::new(FxHashMap::default())) 60 | .lock() 61 | .await; 62 | 63 | match new_state { 64 | Some(new_state) => { 65 | if let Some(old_state) = state_map.get_mut(server_name) { 66 | *old_state = Box::new(new_state); 67 | } else { 68 | state_map.insert(server_name, Box::new(new_state)); 69 | } 70 | } 71 | None => { 72 | state_map.remove(server_name); 73 | } 74 | } 75 | } 76 | 77 | #[async_trait] 78 | pub trait RpcServer { 79 | const SERVER_NAME: &'static str; 80 | 81 | type Serverbound: TryFrom + Send + 'static; 82 | type Clientbound: Into + Send + 'static; 83 | 84 | fn options(&self) -> &RpcServerOptions; 85 | 86 | /// Starts a task to listen on the named pipe. 87 | async fn listen_forever(&self) { 88 | loop { 89 | match self.await_connection().await { 90 | Ok(_) => info!("[rpc:{}] connection closed", Self::SERVER_NAME), 91 | Err(e) => error!("[rpc:{}] connection failed: {}", Self::SERVER_NAME, e), 92 | } 93 | } 94 | } 95 | 96 | async fn await_connection(&self) -> Result<()> { 97 | loop { 98 | set_client_state::(Self::SERVER_NAME, None).await; 99 | 100 | info!( 101 | "[rpc:{}] awaiting connection on {}", 102 | Self::SERVER_NAME, 103 | self.options().pipe_name 104 | ); 105 | 106 | let mut server = ServerOptions::new() 107 | .pipe_mode(PipeMode::Byte) 108 | .in_buffer_size(self.options().buffer_size as u32) 109 | .out_buffer_size(self.options().buffer_size as u32) 110 | .create(OsString::from(self.options().pipe_name.to_string()))?; 111 | 112 | server.connect().await?; 113 | self.handle_connection(&mut server).await?; 114 | } 115 | } 116 | 117 | async fn handle_connection(&self, server: &mut NamedPipeServer) -> Result<()> { 118 | let (send_tx, mut send_rx) = mpsc::unbounded_channel::(); 119 | let our_send_tx = send_tx.clone(); 120 | set_client_state::( 121 | Self::SERVER_NAME, 122 | Some(RpcServerClientState:: { send: send_tx }), 123 | ) 124 | .await; 125 | 126 | let mut buf = BytesMut::with_capacity(self.options().buffer_size); 127 | 128 | // tracking the length outside the loop to ensure cancel safety 129 | let mut pending_len: Option = None; 130 | loop { 131 | tokio::select! { 132 | send_queue = send_rx.recv() => if let Some(outbound_msg) = send_queue { 133 | // serialize the message 134 | let mut message = Vec::new(); 135 | let mut serializer = rmp_serde::Serializer::new(&mut message).with_struct_map(); 136 | RpcMessageDirection::Clientbound(>::into(outbound_msg)) 137 | .serialize(&mut serializer)?; 138 | 139 | // write it 140 | server.write_u32_le(message.len() as u32).await?; 141 | server.write_all(&message).await?; 142 | }, 143 | read = Self::triage_message(&mut buf, &mut pending_len, server) => match read { 144 | Ok(message) => { 145 | let cloned_tx = our_send_tx.clone(); 146 | tokio::spawn(async move { 147 | match Self::dispatch_message(message, cloned_tx) { 148 | Ok(_) => {}, 149 | Err(e) => error!("[rpc:{}] error dispatching message: {}", Self::SERVER_NAME, e), 150 | } 151 | }); 152 | } 153 | Err(e) => bail!(e), 154 | } 155 | } 156 | } 157 | } 158 | 159 | async fn triage_message( 160 | mut buf: &mut BytesMut, 161 | pending_len: &mut Option, 162 | reader: &mut (impl AsyncReadExt + Send + Unpin), 163 | ) -> Result { 164 | loop { 165 | match reader.read_buf(&mut buf).await { 166 | Ok(0) => bail!("pipe broken"), 167 | Ok(_) => { 168 | // first check to see if this is a new message 169 | if let None = pending_len { 170 | // starting a new message, read the length 171 | let len = buf.split_to(4).get_u32_le() as usize; 172 | if len == 0 { 173 | bail!("message length is zero"); 174 | } 175 | 176 | let _ = pending_len.insert(len); 177 | } 178 | 179 | // if we have a pending message length, check to see if we have enough data 180 | if let Some(required) = pending_len { 181 | if buf.len() >= *required { 182 | // split off the message, process it, and get ready for the next one 183 | let message = buf.split_to(*required); 184 | assert_eq!(message.len(), *required); 185 | pending_len.take(); 186 | 187 | return Ok(message); 188 | } 189 | } 190 | } 191 | Err(e) => bail!(e), 192 | } 193 | } 194 | } 195 | 196 | fn dispatch_message( 197 | mut message: BytesMut, 198 | send_tx: mpsc::UnboundedSender<::Clientbound>, 199 | ) -> Result<()> { 200 | if message.len() < 1 { 201 | bail!("message too short"); 202 | } 203 | 204 | // optimization: if the first byte isn't within 0x80-0x8f or 0xde-0xdf, then we know it's not a 205 | // valid msgpack structure for our purposes (since we only use maps), so we can skip the 206 | // deserialization step and pass it directly to process_incoming_message_raw 207 | // most stuff shouldn't use this, but it's useful for the UI server, where 208 | // performance is more important 209 | match message[0] { 210 | 0x00..=0x7F | 0x90..=0xDD | 0xE0..=0xFF => { 211 | if let Err(e) = ::process_incoming_message_raw(send_tx, message) 212 | { 213 | error!( 214 | "[rpc:{}] error processing message: {}", 215 | Self::SERVER_NAME, 216 | e 217 | ); 218 | } 219 | 220 | return Ok(()); 221 | } 222 | _ => {} 223 | } 224 | 225 | let mut de = rmp_serde::Deserializer::from_read_ref(&mut message[..]); 226 | match RpcMessageDirection::deserialize(&mut de) { 227 | Ok(rpc_message) => match rpc_message { 228 | RpcMessageDirection::Serverbound(msg) => match Self::Serverbound::try_from(msg) { 229 | Ok(msg) => { 230 | if let Err(e) = ::process_incoming_message(send_tx, msg) 231 | { 232 | error!( 233 | "[rpc:{}] error processing message: {}", 234 | Self::SERVER_NAME, 235 | e 236 | ); 237 | } 238 | 239 | Ok(()) 240 | } 241 | Err(_) => { 242 | bail!("inbound message was not of the correct type") 243 | } 244 | }, 245 | RpcMessageDirection::Clientbound(_) => { 246 | bail!("received clientbound message on server pipe"); 247 | } 248 | }, 249 | Err(e) => bail!(e), 250 | } 251 | } 252 | 253 | async fn queue_send(message: Self::Clientbound) -> Result<()> { 254 | with_client_state::>(Self::SERVER_NAME, |state| { 255 | state 256 | .send 257 | .send(message) 258 | .map_err(|e| anyhow!("error sending message: {}", e)) 259 | }) 260 | .await? 261 | } 262 | 263 | fn process_incoming_message_raw( 264 | _send: mpsc::UnboundedSender<::Clientbound>, 265 | _message: BytesMut, 266 | ) -> Result<()> { 267 | Err(anyhow::anyhow!( 268 | "process_incoming_message_raw is not implemented for this server" 269 | )) 270 | } 271 | 272 | fn process_incoming_message( 273 | send: mpsc::UnboundedSender<::Clientbound>, 274 | message: Self::Serverbound, 275 | ) -> Result<()>; 276 | } 277 | 278 | #[cfg(test)] 279 | mod tests { 280 | use rmp_serde::Deserializer; 281 | use serde::Deserialize; 282 | 283 | use super::*; 284 | 285 | const TEST_MESSAGE: &'static [u8] = &[ 286 | 0x40, 0x00, 0x00, 0x00, 0xDE, 0x00, 0x01, 0xA2, 0x55, 0x69, 0xDE, 0x00, 0x01, 0xA5, 0x50, 287 | 0x61, 0x69, 0x6E, 0x74, 0xDE, 0x00, 0x08, 0xA2, 0x76, 0x77, 0x7B, 0xA2, 0x76, 0x68, 0xCD, 288 | 0x01, 0xC8, 0xA1, 0x66, 0xA5, 0x52, 0x47, 0x42, 0x41, 0x38, 0xA2, 0x64, 0x78, 0x45, 0xA2, 289 | 0x64, 0x79, 0x2A, 0xA2, 0x64, 0x77, 0xCD, 0x05, 0x39, 0xA2, 0x64, 0x68, 0xCD, 0x01, 0xA4, 290 | 0xA1, 0x64, 0xC4, 0x04, 0x0C, 0x22, 0x38, 0x4E, 291 | ]; 292 | 293 | async fn do_test_triage() -> BytesMut { 294 | let mut data = TEST_MESSAGE.clone(); 295 | let mut buffer = BytesMut::new(); 296 | let mut pending_length: Option = None; 297 | 298 | let triaged = ::triage_message( 299 | &mut buffer, 300 | &mut pending_length, 301 | &mut data, 302 | ) 303 | .await; 304 | 305 | assert!(triaged.is_ok()); 306 | triaged.unwrap() 307 | } 308 | 309 | #[tokio::test] 310 | async fn test_triage() { 311 | let triaged = do_test_triage().await; 312 | assert_eq!(triaged.len(), TEST_MESSAGE.len() - 4); 313 | } 314 | 315 | #[tokio::test] 316 | async fn test_decode() { 317 | let triaged = do_test_triage().await; 318 | let triaged_vec = triaged.to_vec(); 319 | let mut de = Deserializer::new(triaged_vec.as_slice()); 320 | let decoded = RpcMessageDirection::deserialize(&mut de); //rmp_serde::from_slice::(&triaged); 321 | assert!(decoded.is_ok()); 322 | let decoded = decoded.unwrap(); 323 | 324 | assert!(matches!(decoded, RpcMessageDirection::Serverbound(_))); 325 | } 326 | } 327 | -------------------------------------------------------------------------------- /src/rpc/ui.rs: -------------------------------------------------------------------------------- 1 | use super::{RpcServer, RpcServerOptions}; 2 | use crate::{get_execution_id, get_tokio_rt}; 3 | use anyhow::Result; 4 | use bytes::BytesMut; 5 | use grebuloff_rpc::ui::*; 6 | use log::debug; 7 | use std::sync::OnceLock; 8 | use tokio::sync::mpsc; 9 | 10 | // 32MB buffer allows for 4K 32-bit RGBA images 11 | // TODO: make this configurable, or automatically sized based on the game window size 12 | const PIPE_BUFFER_SIZE: usize = 32 * 1024 * 1024; 13 | 14 | pub struct UiRpcServer { 15 | options: RpcServerOptions, 16 | } 17 | 18 | impl RpcServer for UiRpcServer { 19 | const SERVER_NAME: &'static str = "ui"; 20 | 21 | type Serverbound = UiRpcServerboundMessage; 22 | type Clientbound = UiRpcClientboundMessage; 23 | 24 | fn options(&self) -> &super::RpcServerOptions { 25 | &self.options 26 | } 27 | 28 | fn process_incoming_message( 29 | _send: tokio::sync::mpsc::UnboundedSender<::Clientbound>, 30 | message: Self::Serverbound, 31 | ) -> anyhow::Result<()> { 32 | match message { 33 | _ => unimplemented!(), 34 | } 35 | 36 | Ok(()) 37 | } 38 | 39 | fn process_incoming_message_raw( 40 | _send: mpsc::UnboundedSender<::Clientbound>, 41 | message: BytesMut, 42 | ) -> Result<()> { 43 | // UI only uses raw messages for paint, so process it directly 44 | let paint = UiRpcServerboundPaint::from_raw(message)?; 45 | crate::ui::update_buffer_on_paint(paint); 46 | 47 | Ok(()) 48 | } 49 | } 50 | 51 | static mut UI_RPC_SERVER: OnceLock = OnceLock::new(); 52 | 53 | impl UiRpcServer { 54 | fn new() -> Self { 55 | Self { 56 | options: RpcServerOptions { 57 | pipe_name: format!("\\\\.\\pipe\\grebuloff-llrt-ui-{}", get_execution_id()).into(), 58 | buffer_size: PIPE_BUFFER_SIZE, 59 | }, 60 | } 61 | } 62 | 63 | pub fn instance() -> &'static Self { 64 | unsafe { UI_RPC_SERVER.get_or_init(Self::new) } 65 | } 66 | 67 | pub fn resize(width: u32, height: u32) { 68 | get_tokio_rt().spawn(async move { 69 | debug!("informing UI of resize to {}x{}", width, height); 70 | Self::queue_send(UiRpcClientboundMessage::Resize(UiRpcClientboundResize { 71 | width, 72 | height, 73 | })) 74 | .await 75 | }); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/ui.rs: -------------------------------------------------------------------------------- 1 | use crate::get_execution_id; 2 | use anyhow::{bail, Result}; 3 | use bytes::Bytes; 4 | use grebuloff_rpc::ui::{ImageFormat, UiRpcServerboundPaint}; 5 | use log::{error, info, warn}; 6 | use std::{ 7 | path::{Path, PathBuf}, 8 | process::Stdio, 9 | sync::{ 10 | atomic::{AtomicBool, Ordering}, 11 | Mutex, 12 | }, 13 | }; 14 | use tokio::{ 15 | io::{AsyncBufReadExt, BufReader}, 16 | process::Command, 17 | }; 18 | 19 | static LATEST_BUFFER: Mutex> = Mutex::new(None); 20 | 21 | pub async fn spawn_ui_host(runtime_dir: &PathBuf) -> Result<()> { 22 | loop { 23 | info!( 24 | "spawning HLRT process (runtime dir: {})", 25 | runtime_dir.to_str().unwrap() 26 | ); 27 | 28 | let mut builder = Command::new( 29 | Path::new(runtime_dir) 30 | .join("grebuloff-hlrt-win32-x64") 31 | .join("grebuloff-hlrt.exe"), 32 | ); 33 | 34 | builder.stdout(Stdio::piped()); 35 | builder.stderr(Stdio::piped()); 36 | builder.env("LLRT_PIPE_ID", get_execution_id()); 37 | 38 | #[cfg(debug_assertions)] 39 | { 40 | // in debug builds, we want to use the local dev server 41 | // todo: this should probably be configurable 42 | builder.env("ELECTRON_RENDERER_URL", "http://localhost:5173/"); 43 | } 44 | 45 | if let Ok(mut process) = builder.spawn() { 46 | info!("spawned HLRT process with pid {:?}", process.id()); 47 | 48 | let mut stdout = BufReader::new(process.stdout.take().unwrap()).lines(); 49 | let mut stderr = BufReader::new(process.stderr.take().unwrap()).lines(); 50 | 51 | loop { 52 | tokio::select! { 53 | out = stdout.next_line() => { 54 | if let Ok(Some(line)) = out { 55 | info!("[hlrt:out] {}", line); 56 | } 57 | }, 58 | err = stderr.next_line() => { 59 | if let Ok(Some(line)) = err { 60 | warn!("[hlrt:err] {}", line); 61 | } 62 | }, 63 | status = process.wait() => { 64 | info!("[hlrt:exit] HLRT process exited with status {}", status.unwrap()); 65 | break; 66 | } 67 | } 68 | } 69 | } else { 70 | error!("failed to spawn HLRT process"); 71 | bail!("failed to spawn HLRT process"); 72 | } 73 | } 74 | 75 | Ok(()) 76 | } 77 | 78 | pub fn poll_dirty() -> Option { 79 | let mut lock = LATEST_BUFFER.lock().unwrap(); 80 | lock.as_mut().map(|v| v.poll_dirty()).flatten() 81 | } 82 | 83 | pub fn update_buffer_on_paint(paint: UiRpcServerboundPaint) { 84 | assert_eq!( 85 | paint.format, 86 | ImageFormat::BGRA8, 87 | "only ImageFormat::BGRA8 is supported" 88 | ); 89 | 90 | assert_eq!( 91 | paint.data.len(), 92 | paint 93 | .format 94 | .byte_size_of(paint.width as usize, paint.height as usize) 95 | ); 96 | 97 | let mut lock = LATEST_BUFFER.lock().unwrap(); 98 | let _ = lock.insert(UiBuffer::new_dirty( 99 | paint.width.into(), 100 | paint.height.into(), 101 | paint.data, 102 | )); 103 | } 104 | 105 | pub struct UiBufferSnapshot { 106 | pub width: u32, 107 | pub height: u32, 108 | pub data: Bytes, 109 | } 110 | 111 | struct UiBuffer { 112 | width: u32, 113 | height: u32, 114 | dirty: AtomicBool, 115 | data: Option, 116 | } 117 | 118 | impl UiBuffer { 119 | pub fn poll_dirty(&mut self) -> Option { 120 | if self.dirty.swap(false, Ordering::Relaxed) { 121 | if let Some(data) = self.data.take() { 122 | return Some(UiBufferSnapshot { 123 | width: self.width, 124 | height: self.height, 125 | data, 126 | }); 127 | } 128 | } 129 | 130 | None 131 | } 132 | 133 | fn new_dirty(width: u32, height: u32, data: Bytes) -> Self { 134 | Self { 135 | width, 136 | height, 137 | dirty: AtomicBool::new(true), 138 | data: Some(data), 139 | } 140 | } 141 | } 142 | --------------------------------------------------------------------------------