├── .cargo └── config.toml ├── .github └── workflows │ └── rust-ci.yml ├── .gitignore ├── .gitmodules ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── RustPlugin ├── Resources │ ├── ButtonIcon_40x.png │ └── Icon128.png ├── RustPlugin.uplugin └── Source │ └── RustPlugin │ ├── Private │ ├── Bindings.cpp │ ├── EntityComponent.cpp │ ├── FRustDetailCustomization.cpp │ ├── FUuidGraphPanelPinFactory.cpp │ ├── K2Node_GetComponentRust.cpp │ ├── RustActor.cpp │ ├── RustBindingsActor.cpp │ ├── RustGameModeBase.cpp │ ├── RustPlugin.cpp │ ├── RustPluginCommands.cpp │ ├── RustPluginStyle.cpp │ ├── RustProperty.cpp │ ├── RustUtils.cpp │ ├── SGraphNodeGetComponent.cpp │ ├── SGraphPinUuid.cpp │ ├── SRustDropdownList.cpp │ ├── UEdGraphSchema_Rust.cpp │ └── URustReflectionLibrary.cpp │ ├── Public │ ├── Bindings.h │ ├── EntityComponent.h │ ├── FRustDetailCustomization.h │ ├── FUuidGraphPanelPinFactory.h │ ├── K2Node_GetComponentRust.h │ ├── RustActor.h │ ├── RustBindingsActor.h │ ├── RustGameModeBase.h │ ├── RustPlugin.h │ ├── RustPluginCommands.h │ ├── RustPluginStyle.h │ ├── RustProperty.h │ ├── RustUtils.h │ ├── SGraphNodeGetComponent.h │ ├── SGraphPinUuid.h │ ├── SRustDropdownList.h │ ├── UEdGraphSchema_Rust.h │ └── URustReflectionLibrary.h │ └── RustPlugin.Build.cs ├── Setup.bat ├── deny.toml ├── gameplay-plugins └── unreal-movement │ ├── Cargo.toml │ └── src │ └── lib.rs ├── setup.sh ├── unreal-api-derive ├── Cargo.toml └── src │ ├── lib.rs │ ├── reflect.rs │ └── type_uuid.rs ├── unreal-api ├── .gitignore ├── Cargo.toml ├── src │ ├── api.rs │ ├── core.rs │ ├── editor_component.rs │ ├── input.rs │ ├── lib.rs │ ├── log.rs │ ├── module.rs │ ├── physics.rs │ ├── plugin.rs │ └── sound.rs └── wrapper.h ├── unreal-ffi ├── Cargo.toml ├── build.rs ├── src │ ├── actor.rs │ ├── lib.rs │ ├── physics.rs │ └── sound.rs └── wrapper.h ├── unreal-reflect ├── Cargo.toml └── src │ ├── lib.rs │ └── registry.rs └── unreal-rust-example ├── Cargo.toml └── src └── lib.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.x86_64-pc-windows-msvc] 2 | rustflags = [ 3 | "-C", "link-args=-fuse-ld=lld-link" 4 | ] -------------------------------------------------------------------------------- /.github/workflows/rust-ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | tags: 6 | - "*" 7 | pull_request: 8 | 9 | name: CI 10 | jobs: 11 | lint: 12 | name: Lint 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: actions-rs/toolchain@v1 17 | with: 18 | toolchain: stable 19 | override: true 20 | 21 | # make sure all code has been formatted with rustfmt 22 | - name: check rustfmt 23 | run: | 24 | rustup component add rustfmt 25 | cargo fmt -- --check --color always 26 | 27 | # run clippy to verify we have no warnings 28 | - run: cargo fetch 29 | - name: cargo clippy 30 | run: | 31 | rustup component add clippy 32 | cargo clippy --all-targets --all-features -- -D warnings 33 | 34 | test: 35 | name: Test 36 | strategy: 37 | matrix: 38 | os: [ubuntu-latest, windows-latest, macOS-latest] 39 | runs-on: ${{ matrix.os }} 40 | steps: 41 | - uses: actions/checkout@v2 42 | - uses: actions-rs/toolchain@v1 43 | with: 44 | toolchain: stable 45 | override: true 46 | - run: cargo fetch 47 | - name: cargo test build 48 | # Note the use of release here means longer compile time, but much 49 | # faster test execution time. If you don't have any heavy tests it 50 | # might be faster to take off release and just compile in debug 51 | run: cargo build --tests --release 52 | - name: cargo test 53 | run: cargo test --release 54 | 55 | # TODO: Remove this check if you don't use cargo-deny in the repo 56 | deny-check: 57 | name: cargo-deny 58 | runs-on: ubuntu-latest 59 | steps: 60 | - uses: actions/checkout@v2 61 | - uses: EmbarkStudios/cargo-deny-action@v1 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | .vs 4 | .vscode 5 | Content 6 | DerivedDataCache 7 | Intermediate 8 | Saved 9 | Script 10 | Plugins/RustPlugin/Intermediate 11 | rusttemp 12 | *.exe 13 | *.lib 14 | *.pdb 15 | *.exp 16 | *.dll 17 | *.code-workspace 18 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "example/RustExample"] 2 | path = example/RustExample 3 | url = https://gitlab.com/maikklein/rustexample.git -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "unreal-api", 4 | "unreal-ffi", 5 | "unreal-reflect", 6 | "unreal-api-derive", 7 | "unreal-rust-example", 8 | "gameplay-plugins/unreal-movement", 9 | ] 10 | #[profile.release] 11 | #debug = true 12 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Maik Klein 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🦀 unreal-rust 2 | 3 | Opinionated Rust integration for Unreal Engine 4 | 5 | [![Build Status](https://github.com/MaikKlein/unreal-rust/workflows/CI/badge.svg)](https://github.com/MaikKlein/unreal-rust/actions?workflow=CI) 6 | [![LICENSE](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE-MIT) 7 | [![LICENSE](https://img.shields.io/badge/license-apache-blue.svg)](LICENSE-APACHE) 8 | [![Discord](https://img.shields.io/discord/1015534599654354975.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/jyM6fBBdt6) 9 | 10 | ## ☣️ Warning 11 | 12 | `unreal-rust` is not ready to be used in a real project. `unreal-rust` is in a very early state and nothing more than a proof of concept right now. The API will change quite frequently. Things might crash, or not work as intended. The user experience will not be great. 13 | 14 | I am releasing `unreal-rust` on github to develop it in the open. 15 | 16 | ## 🎯 Features 17 | 18 | - **Simple opinionated bindings**: Easy access to core APIs like playing sounds, spawning actors, pathfinding, physics etc. 19 | - **Developer friendly**: Fast iteration times with hot reloading in the editor and during live play. Panics are caught and do not crash the editor 20 | - **Editor integration**: Add Rust components to actors in the editor, or access Rust components from Blueprint to drive animations. 21 | - **Entity Component System**: unreal-rust is built on top of an ECS. 22 | - **Built on top of `AActor`**: Most gameplay features like `GameMode`, `Characters`, `GameState`, `GAS` are not directly accessible in unreal-rust. Instead unreal-rust will provide optional alternatives. But you can still interact with most parts of the engine as Rust components can be accessed in Blueprint. 23 | - **No engine modifications**: unreal-rust is only a `Plugin`, just drop it in your project. See [Supported versions](#supported-versions) for more information. 24 | - **Samples**: The development of unreal-rust is heavily driven by samples. 25 | - **Free**: Dual licensed under MIT/APACHE 26 | 27 | ## 🖥️ Supported Platforms 28 | 29 | * 🐧 Linux 30 | * 🪟 Windows 31 | 32 | Potential future platforms: iOS, macOS, Android, html5/browser(possibly) 33 | 34 | Consoles are unsupported. I simply can not offer console support as it is a closed off ecosystem. Nor do I have access to any developer kits myself. 35 | 36 | ## 💌 Blog posts / media 37 | 38 | * [Announcing Unreal Rust](https://maikklein.github.io/unreal-rust-1/) 39 | * [unreal-rust devlog](https://www.youtube.com/playlist?list=PLps1NSMUeqzicmTej83z-n1J383u1UVq1) 40 | 41 | 42 | ## 🚩 Known problems 43 | 44 | - This is just a hobby project of mine that I work on outside of my normal work hours. I might be slow to respond to issues, questions, feature requests, or PR reviews. 45 | 46 | ## 🦮 Getting started 47 | 48 | ### Running the example 49 | 50 | _I am aware that these are a lot of steps. I am sorry, I will try to simplify this in the future_ 51 | 52 | * Prerequisites: 53 | * * [Unreal Engine 5](https://www.unrealengine.com/en-US/unreal-engine-5). For Linux users you can get it [here](https://www.unrealengine.com/en-US/linux) 54 | * * Get [git lfs](https://git-lfs.github.com/), and run `git lfs install` 55 | * * [ue4cli](https://docs.adamrehn.com/ue4cli/overview/introduction-to-ue4cli), You can get it with `pip3 install ue4cli`. This step is optional but I will use `ue4cli` in this guide. 56 | * * [Rust](https://www.rust-lang.org/tools/install) 57 | 58 | We start by cloning this repository 59 | 60 | ``` 61 | git clone https://github.com/MaikKlein/unreal-rust 62 | ``` 63 | 64 | Next we clone the submodule. This will download the actual example with all the assets. 65 | 66 | ``` 67 | cd unreal-rust 68 | git submodule update --init 69 | ``` 70 | 71 | Next we need to setup the example 72 | 73 | - - Linux `sh setup.sh` 74 | - - Windows `setup.bat` 75 | 76 | This will symlink the `RustPlugin` into the unreal `example/RustExample/Plugin` folder. 77 | 78 | Now we need to build the actual Rust code: 79 | 80 | Simply run 81 | 82 | ``` 83 | cargo build --release 84 | ``` 85 | 86 | This will build the whole project. This also produces our dll that we are going to load into Unreal. 87 | 88 | Copy the dll/so file into the project 89 | 90 | * Linux: `cp target/release/libunreal_rust_example.so example/RustExample/Binaries/rustplugin.so` 91 | * Windows: `copy .\target\release\unreal_rust_example.dll .\example\RustExample\Binaries\rustplugin.dll` 92 | 93 | Now we need to build the unreal example 94 | 95 | ``` 96 | cd example/RustExample 97 | ue4 build Development Editor 98 | ``` 99 | 100 | Now you can run the example with `ue4 run` 101 | 102 | ## 🚀 Supported versions 103 | 104 | - `5.0` 105 | 106 | This project will always try to support the latest version. 107 | 108 | - Latest version of Unreal 109 | - Latest version of Rust 110 | - Latest version of all dependencies 111 | 112 | ## 🤝 Alternatives 113 | 114 | * [Unreal Angelscript](https://angelscript.hazelight.se/) 115 | * [UnrealCLR](https://github.com/nxrighthere/UnrealCLR) 116 | 117 | ## 🥰 Thank you 118 | 119 | * [kenney](https://kenney.nl/), [quaternius](https://www.patreon.com/quaternius) 120 | * [bevy](https://bevyengine.org/) 121 | -------------------------------------------------------------------------------- /RustPlugin/Resources/ButtonIcon_40x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaikKlein/unreal-rust/21f00062189d9cf31e92fb171a1b5d49dd67733d/RustPlugin/Resources/ButtonIcon_40x.png -------------------------------------------------------------------------------- /RustPlugin/Resources/Icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaikKlein/unreal-rust/21f00062189d9cf31e92fb171a1b5d49dd67733d/RustPlugin/Resources/Icon128.png -------------------------------------------------------------------------------- /RustPlugin/RustPlugin.uplugin: -------------------------------------------------------------------------------- 1 | { 2 | "FileVersion": 3, 3 | "Version": 1, 4 | "VersionName": "1.0", 5 | "FriendlyName": "RustPlugin", 6 | "Description": "", 7 | "Category": "Other", 8 | "CreatedBy": "", 9 | "CreatedByURL": "", 10 | "DocsURL": "", 11 | "MarketplaceURL": "", 12 | "SupportURL": "", 13 | "CanContainContent": false, 14 | "IsBetaVersion": false, 15 | "IsExperimentalVersion": false, 16 | "Installed": false, 17 | "Modules": [ 18 | { 19 | "Name": "RustPlugin", 20 | "Type": "Runtime", 21 | "LoadingPhase": "Default" 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /RustPlugin/Source/RustPlugin/Private/EntityComponent.cpp: -------------------------------------------------------------------------------- 1 | // Fill out your copyright notice in the Description page of Project Settings. 2 | 3 | 4 | #include "EntityComponent.h" 5 | 6 | #include "RustProperty.h" 7 | 8 | 9 | 10 | // Sets default values for this component's properties 11 | UEntityComponent::UEntityComponent() 12 | { 13 | PrimaryComponentTick.bCanEverTick = false; 14 | } 15 | 16 | FEntity UEntityComponent::GetEntity() 17 | { 18 | return Id; 19 | } 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /RustPlugin/Source/RustPlugin/Private/FRustDetailCustomization.cpp: -------------------------------------------------------------------------------- 1 | #include "FRustDetailCustomization.h" 2 | 3 | // MyCustomization.cpp 4 | #include "DetailLayoutBuilder.h" 5 | #include "DetailWidgetRow.h" 6 | #include "Widgets/Input/SVectorInputBox.h" 7 | #include "RustActor.h" 8 | #include "RustProperty.h" 9 | #include "SRustDropdownList.h" 10 | 11 | #define LOCTEXT_NAMESPACE "RustDetailCustomization" 12 | 13 | TSharedRef FRustDetailCustomization::MakeInstance() 14 | { 15 | return MakeShareable(new FRustDetailCustomization); 16 | } 17 | 18 | void FRustDetailCustomization::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) 19 | { 20 | TArray> Objects; 21 | DetailBuilder.GetObjectsBeingCustomized(Objects); 22 | 23 | if (Objects.IsEmpty()) 24 | return; 25 | 26 | TWeakObjectPtr Component = Cast(Objects[0]); 27 | 28 | TSharedRef ComponentsHandle = DetailBuilder.GetProperty( 29 | GET_MEMBER_NAME_CHECKED(UEntityComponent, Components)); 30 | // Don't show the components map in the editor. This should only be edited through the custom ui below. 31 | DetailBuilder.HideProperty(ComponentsHandle); 32 | { 33 | uint32 NumChildren = 0; 34 | ComponentsHandle->GetNumChildren(NumChildren); 35 | 36 | for (uint32 ComponentIdx = 0; ComponentIdx < NumChildren; ++ComponentIdx) 37 | { 38 | auto ChildProp = ComponentsHandle->GetChildHandle(ComponentIdx); 39 | auto KeyProp = ChildProp->GetKeyHandle(); 40 | 41 | FString GuidName; 42 | KeyProp->GetValue(GuidName); 43 | 44 | FGuid Guid; 45 | FGuid::Parse(GuidName, Guid); 46 | 47 | const auto RustComponent = Component->Components.Find(GuidName); 48 | if (RustComponent != nullptr) 49 | { 50 | RustComponent->Reload(ChildProp, Guid); 51 | } 52 | } 53 | } 54 | 55 | 56 | IDetailCategoryBuilder& RustCategory = DetailBuilder.EditCategory(TEXT("Rust")); 57 | FDynamicRustComponent::Render(ComponentsHandle, RustCategory, DetailBuilder); 58 | 59 | auto OnPicked = [Component, &DetailBuilder, ComponentsHandle](FUuidViewNode* Node) 60 | { 61 | if (Node == nullptr || Component == nullptr) 62 | return; 63 | 64 | if (Component.Get()->Components.Contains(Node->Id.ToString())) 65 | { 66 | return; 67 | } 68 | 69 | { 70 | uint32 Index = 0; 71 | ComponentsHandle->GetNumChildren(Index); 72 | ComponentsHandle->AsMap()->AddItem(); 73 | auto ChildProp = ComponentsHandle->GetChildHandle(Index); 74 | auto KeyProp = ChildProp->GetKeyHandle(); 75 | 76 | KeyProp->SetValue(Node->Id.ToString()); 77 | FDynamicRustComponent::Initialize(ChildProp, Node->Id); 78 | } 79 | DetailBuilder.ForceRefreshDetails(); 80 | }; 81 | 82 | RustCategory.AddCustomRow(LOCTEXT("Picker", "Picker")).WholeRowContent()[ 83 | SNew(SRustDropdownList).OnlyShowEditorComponents(true).OnUuidPickedDelegate( 84 | FOnUuidPicked::CreateLambda(OnPicked)) 85 | ]; 86 | } 87 | 88 | #undef LOCTEXT_NAMESPACE 89 | -------------------------------------------------------------------------------- /RustPlugin/Source/RustPlugin/Private/FUuidGraphPanelPinFactory.cpp: -------------------------------------------------------------------------------- 1 | #include "FUuidGraphPanelPinFactory.h" 2 | #include "SGraphPin.h" 3 | 4 | TSharedPtr FUuidGraphPanelPinFactory::CreatePin(class UEdGraphPin* InPin) const 5 | { 6 | //return SNew(SGraphPin, InPin); 7 | return TSharedPtr(); 8 | } 9 | -------------------------------------------------------------------------------- /RustPlugin/Source/RustPlugin/Private/K2Node_GetComponentRust.cpp: -------------------------------------------------------------------------------- 1 | #include "K2Node_GetComponentRust.h" 2 | 3 | #include "Utils.h" 4 | #include "BlueprintActionDatabaseRegistrar.h" 5 | #include "BlueprintNodeSpawner.h" 6 | #include "EntityComponent.h" 7 | #include "K2Node_CallFunction.h" 8 | #include "K2Node_IfThenElse.h" 9 | #include "KismetCompiler.h" 10 | #include "RustPlugin.h" 11 | #include "RustUtils.h" 12 | #include "SGraphNodeGetComponent.h" 13 | #include "URustReflectionLibrary.h" 14 | 15 | #define LOCTEXT_NAMESPACE "K2Node_GetComponentRust" 16 | 17 | static const FName EntityParamName(TEXT("EntityId")); 18 | static const FName UuidParamName(TEXT("Uuid")); 19 | 20 | static const FName ReflectUuidParamName(TEXT("Id")); 21 | static const FName ReflectOutputParamName(TEXT("Out")); 22 | static const FName ReflectEntityParamName(TEXT("EntityId")); 23 | static const FName ReflectIndexParamName(TEXT("Index")); 24 | 25 | void UK2Node_GetComponentRust::AllocateDefaultPins() 26 | { 27 | Super::AllocateDefaultPins(); 28 | IndexPins.Empty(); 29 | CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Exec, UEdGraphSchema_K2::PN_Execute); 30 | CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Exec, UEdGraphSchema_K2::PN_Then); 31 | CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Exec, UEdGraphSchema_K2::PN_Else); 32 | const auto ElementPin = CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Struct, FEntity::StaticStruct(), 33 | EntityParamName); 34 | 35 | const auto& Module = GetRustModule(); 36 | 37 | FGuid Id = SelectedNode.Id; 38 | auto Reflection = Module.Plugin.ReflectionData.Types.Find(Id); 39 | if (Reflection == nullptr) 40 | return; 41 | 42 | uint32 NumberOfFields = Reflection->IndexToFieldName.Num(); 43 | for (uint32 Idx = 0; Idx < NumberOfFields; ++Idx) 44 | { 45 | FString IdxName = FString::FromInt(Idx); 46 | auto IdxPin = CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Int, *IdxName); 47 | IdxPin->bHidden = true; 48 | IdxPin->DefaultValue = IdxName; 49 | auto FieldName = Reflection->IndexToFieldName.Find(Idx); 50 | if (!FieldName) 51 | continue; 52 | 53 | auto FieldTypePtr = Reflection->FieldNameToType.Find(*FieldName); 54 | if (!FieldTypePtr) 55 | continue; 56 | 57 | ReflectionType Type = *FieldTypePtr; 58 | 59 | if (Type == ReflectionType::Composite) 60 | // TODO: Implement composite types 61 | continue; 62 | 63 | const FString& VarName = *FieldName; 64 | 65 | if (Type == ReflectionType::Vector3) 66 | { 67 | CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Struct, TBaseStructure::Get(), 68 | *VarName); 69 | } 70 | 71 | if (Type == ReflectionType::Bool) 72 | { 73 | CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Boolean, 74 | *VarName); 75 | } 76 | 77 | if (Type == ReflectionType::Float) 78 | { 79 | CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Real, UEdGraphSchema_K2::PC_Float, 80 | *VarName); 81 | } 82 | 83 | if (Type == ReflectionType::Quaternion) 84 | { 85 | CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Struct, TBaseStructure::Get(), 86 | *VarName); 87 | } 88 | } 89 | } 90 | 91 | void UK2Node_GetComponentRust::ExpandNode(class FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph) 92 | { 93 | Super::ExpandNode(CompilerContext, SourceGraph); 94 | 95 | Uuid Id = ToUuid(SelectedNode.Id); 96 | const auto& Module = GetRustModule(); 97 | 98 | auto Reflection = Module.Plugin.ReflectionData.Types.Find(SelectedNode.Id); 99 | if (Reflection == nullptr) 100 | return; 101 | 102 | auto UuidPin = CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Object, UUuid::StaticClass(), UuidParamName); 103 | UuidPin->bHidden = true; 104 | auto UuidObject = NewObject(); 105 | UuidObject->Id = Id; 106 | UuidPin->DefaultObject = UuidObject; 107 | 108 | UEdGraphPin* EntityPin = FindPinChecked(EntityParamName, EGPD_Input); 109 | UEdGraphPin* ThenPin = FindPinChecked(UEdGraphSchema_K2::PN_Then, EGPD_Output); 110 | 111 | UFunction* HasComponent = GetDefault()->FindFunctionChecked( 112 | GET_FUNCTION_NAME_CHECKED(URustReflectionLibrary, K2_HasComponent)); 113 | UK2Node_CallFunction* HasComponentFunction = CompilerContext.SpawnIntermediateNode< 114 | UK2Node_CallFunction>(this, SourceGraph); 115 | HasComponentFunction->SetFromFunction(HasComponent); 116 | HasComponentFunction->AllocateDefaultPins(); 117 | 118 | UEdGraphPin* CallUUid = HasComponentFunction->FindPinChecked(ReflectUuidParamName, EGPD_Input); 119 | UEdGraphPin* CallEntity = HasComponentFunction->FindPinChecked(ReflectEntityParamName, EGPD_Input); 120 | 121 | CompilerContext.MovePinLinksToIntermediate(*GetExecPin(), *HasComponentFunction->GetExecPin()); 122 | CompilerContext.CopyPinLinksToIntermediate(*UuidPin, *CallUUid); 123 | CompilerContext.CopyPinLinksToIntermediate(*EntityPin, *CallEntity); 124 | 125 | UK2Node_IfThenElse* HasComponentBranch = CompilerContext.SpawnIntermediateNode( 126 | this, SourceGraph); 127 | HasComponentBranch->AllocateDefaultPins(); 128 | 129 | 130 | HasComponentFunction->GetThenPin()->MakeLinkTo(HasComponentBranch->GetExecPin()); 131 | HasComponentFunction->GetReturnValuePin()->MakeLinkTo(HasComponentBranch->GetConditionPin()); 132 | 133 | CompilerContext.MovePinLinksToIntermediate(*FindPinChecked(UEdGraphSchema_K2::PN_Else, EGPD_Output), 134 | *HasComponentBranch->GetElsePin()); 135 | 136 | UEdGraphPin* PrevExecPin = HasComponentBranch->GetThenPin(); 137 | 138 | uint32_t NumberOfFields = Reflection->IndexToFieldName.Num(); 139 | for (uint32_t Idx = 0; Idx < NumberOfFields; Idx++) 140 | { 141 | FString IdxName = FString::FromInt(Idx); 142 | auto InputIdxPin = FindPinChecked(*IdxName); 143 | auto FieldName = Reflection->IndexToFieldName.Find(Idx); 144 | if (!FieldName) 145 | continue; 146 | 147 | auto FieldTypePtr = Reflection->FieldNameToType.Find(*FieldName); 148 | if (!FieldTypePtr) 149 | continue; 150 | 151 | ReflectionType Type = *FieldTypePtr; 152 | const FString& VarName = *FieldName; 153 | 154 | if (Type == ReflectionType::Composite) 155 | // TODO: Implement composite types 156 | continue; 157 | UEdGraphPin* OutputPin = FindPinChecked(*VarName, EGPD_Output); 158 | UFunction* Function = nullptr; 159 | 160 | if (Type == ReflectionType::Vector3) 161 | { 162 | Function = GetDefault()->FindFunctionChecked( 163 | GET_FUNCTION_NAME_CHECKED(URustReflectionLibrary, K2_GetReflectionVector3)); 164 | } 165 | if (Type == ReflectionType::Bool) 166 | { 167 | Function = GetDefault()->FindFunctionChecked( 168 | GET_FUNCTION_NAME_CHECKED(URustReflectionLibrary, K2_GetReflectionBool)); 169 | } 170 | if (Type == ReflectionType::Float) 171 | { 172 | Function = GetDefault()->FindFunctionChecked( 173 | GET_FUNCTION_NAME_CHECKED(URustReflectionLibrary, K2_GetReflectionFloat)); 174 | } 175 | if (Type == ReflectionType::Quaternion) 176 | { 177 | Function = GetDefault()->FindFunctionChecked( 178 | GET_FUNCTION_NAME_CHECKED(URustReflectionLibrary, K2_GetReflectionQuat)); 179 | } 180 | if (Function != nullptr) 181 | { 182 | UEdGraphPin* CurrentExecPin = CallReflection(CompilerContext, SourceGraph, Function, UuidPin, 183 | EntityPin, 184 | InputIdxPin, 185 | OutputPin, PrevExecPin); 186 | PrevExecPin = CurrentExecPin; 187 | } 188 | } 189 | CompilerContext.MovePinLinksToIntermediate(*ThenPin, *PrevExecPin); 190 | BreakAllNodeLinks(); 191 | } 192 | 193 | void UK2Node_GetComponentRust::GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const 194 | { 195 | const auto ActionKey = GetClass(); 196 | if (ActionRegistrar.IsOpenForRegistration(ActionKey)) 197 | { 198 | const auto NodeSpawner = UBlueprintNodeSpawner::Create(ActionKey); 199 | check(NodeSpawner != nullptr); 200 | 201 | //auto Self = NewObject(); 202 | 203 | ActionRegistrar.AddBlueprintAction(ActionKey, NodeSpawner); 204 | } 205 | } 206 | 207 | FText UK2Node_GetComponentRust::GetNodeTitle(ENodeTitleType::Type TitleType) const 208 | { 209 | return LOCTEXT("NodeTitle_NONE", "Get Component (Rust)"); 210 | } 211 | 212 | TSharedPtr UK2Node_GetComponentRust::CreateVisualWidget() 213 | { 214 | FText NewText = FText::FromString(SelectedNode.Name); 215 | return SNew(SGraphNodeGetComponent, this) 216 | // TODO: This doesn't seem to work. `NewText` here is always empty. 217 | // Maybe `CreateVisualWidget` is called before `SelectedNode` is ready. 218 | .SelectedComponentText(NewText) 219 | .OnUuidPickedDelegate( 220 | FOnUuidPicked::CreateUObject( 221 | this, &UK2Node_GetComponentRust::OnUuidPicked)); 222 | } 223 | 224 | void UK2Node_GetComponentRust::OnUuidPicked(FUuidViewNode* Node) 225 | { 226 | SelectedNode = *Node; 227 | BreakAllOutputPins(); 228 | ReconstructNode(); 229 | } 230 | 231 | void UK2Node_GetComponentRust::BreakAllOutputPins() 232 | { 233 | TSet NodeList; 234 | 235 | NodeList.Add(this); 236 | 237 | // Iterate over each pin and break all links 238 | for (int32 PinIdx = 0; PinIdx < Pins.Num(); ++PinIdx) 239 | { 240 | UEdGraphPin* Pin = Pins[PinIdx]; 241 | if (Pin->Direction != EEdGraphPinDirection::EGPD_Output) 242 | continue; 243 | 244 | // Save all the connected nodes to be notified below 245 | for (UEdGraphPin* Connection : Pin->LinkedTo) 246 | { 247 | NodeList.Add(Connection->GetOwningNode()); 248 | } 249 | 250 | Pin->BreakAllPinLinks(); 251 | } 252 | 253 | // Send a notification to all nodes that lost a connection 254 | for (UEdGraphNode* Node : NodeList) 255 | { 256 | Node->NodeConnectionListChanged(); 257 | } 258 | } 259 | 260 | UEdGraphPin* UK2Node_GetComponentRust::CallReflection(class FKismetCompilerContext& CompilerContext, 261 | UEdGraph* SourceGraph, 262 | UFunction* ReflectionFn, 263 | UEdGraphPin* UuidPin, 264 | UEdGraphPin* EntityIdPin, 265 | UEdGraphPin* InputIdxPin, 266 | UEdGraphPin* VariableOutputPin, 267 | UEdGraphPin* PrevExecPin) 268 | { 269 | UK2Node_CallFunction* CallFunctionNode = CompilerContext.SpawnIntermediateNode< 270 | UK2Node_CallFunction>(this, SourceGraph); 271 | CallFunctionNode->SetFromFunction(ReflectionFn); 272 | CallFunctionNode->AllocateDefaultPins(); 273 | 274 | UEdGraphPin* CallExecPin = CallFunctionNode->GetExecPin(); 275 | UEdGraphPin* CallThen = CallFunctionNode->GetThenPin(); 276 | UEdGraphPin* CallUUid = CallFunctionNode->FindPinChecked(ReflectUuidParamName, EGPD_Input); 277 | UEdGraphPin* CallEntity = CallFunctionNode->FindPinChecked(ReflectEntityParamName, EGPD_Input); 278 | UEdGraphPin* CallIndex = CallFunctionNode->FindPinChecked(ReflectIndexParamName, EGPD_Input); 279 | UEdGraphPin* CallOut = CallFunctionNode->FindPinChecked(ReflectOutputParamName, EGPD_Output); 280 | 281 | PrevExecPin->MakeLinkTo(CallExecPin); 282 | 283 | CompilerContext.CopyPinLinksToIntermediate(*UuidPin, *CallUUid); 284 | CompilerContext.CopyPinLinksToIntermediate(*EntityIdPin, *CallEntity); 285 | CompilerContext.MovePinLinksToIntermediate(*InputIdxPin, *CallIndex); 286 | CompilerContext.MovePinLinksToIntermediate(*VariableOutputPin, *CallOut); 287 | 288 | return CallThen; 289 | } 290 | 291 | #undef LOCTEXT_NAMESPACE 292 | -------------------------------------------------------------------------------- /RustPlugin/Source/RustPlugin/Private/RustActor.cpp: -------------------------------------------------------------------------------- 1 | // Fill out your copyright notice in the Description page of Project Settings. 2 | 3 | 4 | #include "RustActor.h" 5 | #include "RustPlugin.h" 6 | #include "Utils.h" 7 | 8 | // Sets default values 9 | ARustActor::ARustActor() 10 | { 11 | // Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it. 12 | PrimaryActorTick.bCanEverTick = true; 13 | EntityComponent = CreateDefaultSubobject(TEXT("EntityComponent")); 14 | } 15 | 16 | // Called when the game starts or when spawned 17 | void ARustActor::BeginPlay() 18 | { 19 | Super::BeginPlay(); 20 | } 21 | 22 | // Called every frame 23 | void ARustActor::Tick(float DeltaTime) 24 | { 25 | Super::Tick(DeltaTime); 26 | } -------------------------------------------------------------------------------- /RustPlugin/Source/RustPlugin/Private/RustBindingsActor.cpp: -------------------------------------------------------------------------------- 1 | // Fill out your copyright notice in the Description page of Project Settings. 2 | 3 | 4 | #include "RustBindingsActor.h" 5 | 6 | // Sets default values 7 | ARustBindingsActor::ARustBindingsActor() 8 | { 9 | // Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it. 10 | PrimaryActorTick.bCanEverTick = true; 11 | 12 | } 13 | 14 | // Called when the game starts or when spawned 15 | void ARustBindingsActor::BeginPlay() 16 | { 17 | Super::BeginPlay(); 18 | 19 | } 20 | 21 | // Called every frame 22 | void ARustBindingsActor::Tick(float DeltaTime) 23 | { 24 | Super::Tick(DeltaTime); 25 | 26 | } 27 | 28 | -------------------------------------------------------------------------------- /RustPlugin/Source/RustPlugin/Private/RustGameModeBase.cpp: -------------------------------------------------------------------------------- 1 | // Fill out your copyright notice in the Description page of Project Settings. 2 | 3 | #include "RustGameModeBase.h" 4 | #include "Modules/ModuleManager.h" 5 | #include "RustPlugin.h" 6 | #include "EngineUtils.h" 7 | #include "Utils.h" 8 | #include "Components/InputComponent.h" 9 | #include "GameFramework/InputSettings.h" 10 | #include "Engine/InputDelegateBinding.h" 11 | #include "GameFramework/PlayerInput.h" 12 | #include "Kismet/GameplayStatics.h" 13 | #include "Widgets/Notifications/SNotificationList.h" 14 | #include "Framework/Notifications/NotificationManager.h" 15 | 16 | #include "Editor.h" 17 | #include "RustUtils.h" 18 | 19 | #define LOCTEXT_NAMESPACE "FRustPluginModule" 20 | 21 | ARustGameModeBase::~ARustGameModeBase() 22 | { 23 | } 24 | 25 | ARustGameModeBase::ARustGameModeBase() 26 | { 27 | FRustPluginModule& LocalModule = FModuleManager::LoadModuleChecked(TEXT("RustPlugin")); 28 | LocalModule.GameMode = this; 29 | 30 | PrimaryActorTick.bStartWithTickEnabled = true; 31 | PrimaryActorTick.bCanEverTick = true; 32 | 33 | bBlockInput = false; 34 | AutoReceiveInput = EAutoReceiveInput::Player0; 35 | InputComponent = CreateDefaultSubobject(TEXT("InputComponent")); 36 | InputComponent->bBlockInput = bBlockInput; 37 | InputComponent->Priority = InputPriority; 38 | UInputDelegateBinding::BindInputDelegates(GetClass(), InputComponent); 39 | 40 | // PlayerInput = CreateDefaultSubobject(TEXT("Input")); 41 | } 42 | 43 | void ARustGameModeBase::OnActorSpawnedHandler(AActor* actor) 44 | { 45 | EventType Type = EventType::ActorSpawned; 46 | ActorSpawnedEvent Event; 47 | Event.actor = (AActorOpaque*)actor; 48 | GetRustModule().Plugin.Rust.unreal_event(&Type, (void*)&Event); 49 | } 50 | 51 | void ARustGameModeBase::OnActorBeginOverlap(AActor* OverlappedActor, AActor* OtherActor) 52 | { 53 | EventType Type = EventType::ActorBeginOverlap; 54 | ActorBeginOverlap Event; 55 | Event.overlapped_actor = (AActorOpaque*)OverlappedActor; 56 | Event.other = (AActorOpaque*)OtherActor; 57 | GetRustModule().Plugin.Rust.unreal_event(&Type, (void*)&Event); 58 | } 59 | 60 | void ARustGameModeBase::OnActorEndOverlap(AActor* OverlappedActor, AActor* OtherActor) 61 | { 62 | EventType Type = EventType::ActorEndOverlap; 63 | ActorEndOverlap Event; 64 | Event.overlapped_actor = (AActorOpaque*)OverlappedActor; 65 | Event.other = (AActorOpaque*)OtherActor; 66 | GetRustModule().Plugin.Rust.unreal_event(&Type, (void*)&Event); 67 | } 68 | 69 | void ARustGameModeBase::OnActorHit(AActor* SelfActor, AActor* OtherActor, FVector NormalImpulse, const FHitResult& Hit) 70 | { 71 | EventType Type = EventType::ActorOnHit; 72 | ActorHitEvent Event; 73 | Event.self_actor = (AActorOpaque*)SelfActor; 74 | Event.other = (AActorOpaque*)OtherActor; 75 | Event.normal_impulse = ToVector3(NormalImpulse); 76 | GetRustModule().Plugin.Rust.unreal_event(&Type, (void*)&Event); 77 | } 78 | 79 | void ARustGameModeBase::OnActorDestroyed(AActor* Actor) 80 | { 81 | EventType Type = EventType::ActorDestroy; 82 | ActorDestroyEvent Event; 83 | Event.actor = (AActorOpaque*)Actor; 84 | GetRustModule().Plugin.Rust.unreal_event(&Type, (void*)&Event); 85 | } 86 | 87 | void ARustGameModeBase::PostLogin(APlayerController* NewPlayer) 88 | { 89 | Super::PostLogin(NewPlayer); 90 | } 91 | 92 | void ARustGameModeBase::Tick(float Dt) 93 | { 94 | Super::Tick(Dt); 95 | FRustPluginModule& Module = GetRustModule(); 96 | 97 | if (Module.Plugin.NeedsInit) 98 | { 99 | StartPlay(); 100 | } 101 | if (Module.Plugin.IsLoaded() && Module.Plugin.Rust.tick(Dt) == ResultCode::Panic) 102 | { 103 | Module.Exit(); 104 | } 105 | } 106 | 107 | void ARustGameModeBase::StartPlay() 108 | { 109 | Super::StartPlay(); 110 | GetWorld()->AddOnActorSpawnedHandler( 111 | FOnActorSpawned::FDelegate::CreateUObject(this, &ARustGameModeBase::OnActorSpawnedHandler)); 112 | 113 | APlayerController* PC = UGameplayStatics::GetPlayerController(this, 0); 114 | InputComponent->AxisBindings.Empty(); 115 | for (auto Mapping : PC->PlayerInput->AxisMappings) 116 | { 117 | InputComponent->BindAxis(Mapping.AxisName); 118 | } 119 | 120 | FRustPluginModule& Module = GetRustModule(); 121 | if (Module.Plugin.IsLoaded() && Module.Plugin.Rust.begin_play() == ResultCode::Panic) 122 | { 123 | Module.Exit(); 124 | } 125 | else 126 | { 127 | Module.Plugin.NeedsInit = false; 128 | } 129 | for (TActorIterator ActorItr(GetWorld()); ActorItr; ++ActorItr) 130 | { 131 | AActor* Actor = *ActorItr; 132 | Actor->OnDestroyed.AddUniqueDynamic(this, &ARustGameModeBase::OnActorDestroyed); 133 | OnActorSpawnedHandler(Actor); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /RustPlugin/Source/RustPlugin/Private/RustPlugin.cpp: -------------------------------------------------------------------------------- 1 | // Copyright Epic Games, Inc. All Rights Reserved. 2 | 3 | #include "RustPlugin.h" 4 | #include "RustPluginStyle.h" 5 | #include "RustPluginCommands.h" 6 | #include "FRustDetailCustomization.h" 7 | #include "LevelEditor.h" 8 | #include "Widgets/Docking/SDockTab.h" 9 | #include "Widgets/Layout/SBox.h" 10 | #include "Widgets/Text/STextBlock.h" 11 | #include "ToolMenus.h" 12 | #include "DirectoryWatcherModule.h" 13 | #include "IDirectoryWatcher.h" 14 | #include "Misc/Paths.h" 15 | #include "FUuidGraphPanelPinFactory.h" 16 | #include "Modules/ModuleManager.h" 17 | #include "RustUtils.h" 18 | #include "Bindings.h" 19 | #include "Framework/Notifications/NotificationManager.h" 20 | #include "HAL/PlatformFileManager.h" 21 | #include "Widgets/Notifications/SNotificationList.h" 22 | 23 | #include 24 | static const FName RustPluginTabName("RustPlugin"); 25 | 26 | #define LOCTEXT_NAMESPACE "FRustPluginModule" 27 | 28 | FString PlatformExtensionName() 29 | { 30 | #if PLATFORM_LINUX || PLATFORM_MAC 31 | return FString(TEXT("so")); 32 | #elif PLATFORM_WINDOWS 33 | return FString(TEXT("dll")); 34 | #endif 35 | } 36 | 37 | FString FPlugin::PluginFolderPath() 38 | { 39 | return FPaths::Combine(*FPaths::ConvertRelativePathToFull(FPaths::ProjectDir()), TEXT("Binaries")); 40 | } 41 | 42 | FString FPlugin::PluginPath() 43 | { 44 | return FPaths::Combine(*PluginFolderPath(), *PluginFileName()); 45 | } 46 | 47 | FString FPlugin::PluginFileName() 48 | { 49 | return FString::Printf(TEXT("%s.%s"), TEXT("rustplugin"), *PlatformExtensionName()); 50 | } 51 | 52 | FPlugin::FPlugin() 53 | { 54 | } 55 | 56 | bool FPlugin::TryLoad() 57 | { 58 | // Loading ddls is a bit tricky, see https://fasterthanli.me/articles/so-you-want-to-live-reload-rust 59 | // The gist is we can't easily hot reload a dll if the dll uses the thread local storage (TLS). 60 | // The TLS will prevent the dll from being unloaded even when we call `dlclose`. And `dlopen` will return 61 | // the pointer to the previously loaded dll. 62 | // Essentially this means hot reloading will do nothing as we can't unload the currently loaded dll. 63 | // The workaround for this is give each dll a unique name. Here we append the unix timestamp at 64 | // the end of the file. That way we can force `dlopen` to load the dll. 65 | // Please note this is a hack, and this _should_ leak and increase the memory every time you hot reload. 66 | // This behavior is the same on Linux, Windows and most likely all the other platforms. 67 | 68 | FString Path = PluginPath(); 69 | FString LocalTargetPath = FPaths::Combine(*PluginFolderPath(), 70 | *FString::Printf( 71 | TEXT("%s-%i"), *PluginFileName(), 72 | FDateTime::Now().ToUnixTimestamp())); 73 | if (this->IsLoaded()) 74 | { 75 | FPlatformProcess::FreeDllHandle(this->Handle); 76 | this->Handle = nullptr; 77 | // This is leaky. If we close the editor this will not delete the file 78 | // TODO: Cleanup unused dlls here 79 | if (!FPlatformFileManager::Get().GetPlatformFile().DeleteFile(*this->TargetPath)) 80 | { 81 | UE_LOG(LogTemp, Warning, TEXT("Unable to delete File %s"), *this->TargetPath); 82 | } 83 | } 84 | 85 | if (!FPlatformFileManager::Get().GetPlatformFile().CopyFile(*LocalTargetPath, *Path)) 86 | { 87 | UE_LOG(LogTemp, Warning, TEXT("Unable to copy File from %s to %s"), *Path, *LocalTargetPath); 88 | return false; 89 | } 90 | void* LocalHandle = FPlatformProcess::GetDllHandle(*LocalTargetPath); 91 | 92 | if (LocalHandle == nullptr) 93 | { 94 | UE_LOG(LogTemp, Warning, TEXT("Dll open failed")); 95 | return false; 96 | } 97 | 98 | this->Handle = LocalHandle; 99 | 100 | void* LocalBindings = FPlatformProcess::GetDllExport(LocalHandle, TEXT("register_unreal_bindings\0")); 101 | ensure(LocalBindings); 102 | 103 | this->Bindings = (EntryUnrealBindingsFn)LocalBindings; 104 | 105 | this->TargetPath = LocalTargetPath; 106 | NeedsInit = true; 107 | CallEntryPoints(); 108 | return true; 109 | } 110 | 111 | void FRustPluginModule::Exit() 112 | { 113 | if (GEditor) 114 | { 115 | GEditor->RequestEndPlayMap(); 116 | } 117 | } 118 | 119 | bool FPlugin::IsLoaded() 120 | { 121 | return Handle != nullptr; 122 | } 123 | 124 | void FPlugin::CallEntryPoints() 125 | { 126 | if (!IsLoaded()) 127 | return; 128 | 129 | // Pass unreal function pointers to Rust, and also retrieve function pointers from Rust so we can call into Rust 130 | if (Bindings(CreateBindings(), &Rust)) 131 | { 132 | RetrieveReflectionData(); 133 | } 134 | else 135 | { 136 | // TODO: We had a panic when calling the entry point. We need to handle that better, otherwise unreal will segfault because the rust bindings are nullptrs 137 | } 138 | } 139 | 140 | void FPlugin::RetrieveReflectionData() 141 | { 142 | uintptr_t len = 0; 143 | Rust.retrieve_uuids(nullptr, &len); 144 | TArray LocalUuids; 145 | LocalUuids.Reserve(len); 146 | Rust.retrieve_uuids(LocalUuids.GetData(), &len); 147 | LocalUuids.SetNum(len); 148 | 149 | ReflectionData.Types.Reset(); 150 | 151 | for (Uuid Id : LocalUuids) 152 | { 153 | uint32_t NumberOfFields = 0; 154 | Rust.reflection_fns.number_of_fields(Id, &NumberOfFields); 155 | Utf8Str TypeNameStr; 156 | // TODO: Better error handling here. None of this should fail though 157 | check(Rust.reflection_fns.get_type_name(Id, &TypeNameStr)); 158 | 159 | FRustReflection Reflection; 160 | Reflection.Name = ToFString(TypeNameStr); 161 | Reflection.IsEditorComponent = Rust.reflection_fns.is_editor_component(Id) == 1; 162 | 163 | for (uint32_t Idx = 0; Idx < NumberOfFields; Idx++) 164 | { 165 | Utf8Str FieldNamePtr; 166 | check(Rust.reflection_fns.get_field_name(Id, Idx, &FieldNamePtr)); 167 | ReflectionType Type = ReflectionType::Bool; 168 | check(Rust.reflection_fns.get_field_type(Id, Idx, &Type)); 169 | 170 | FString FieldName = ToFString(FieldNamePtr); 171 | Reflection.IndexToFieldName.Add(Idx, FieldName); 172 | Reflection.FieldNameToType.Add(FieldName, Type); 173 | } 174 | 175 | ReflectionData.Types.Add(ToFGuid(Id), Reflection); 176 | } 177 | } 178 | 179 | void FRustPluginModule::StartupModule() 180 | { 181 | // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module 182 | 183 | FRustPluginStyle::Initialize(); 184 | FRustPluginStyle::ReloadTextures(); 185 | 186 | FRustPluginCommands::Register(); 187 | 188 | //PluginCommands = MakeShareable(new FUICommandList); 189 | 190 | //PluginCommands->MapAction( 191 | // FRustPluginCommands::Get().OpenPluginWindow, 192 | // FExecuteAction::CreateRaw(this, &FRustPluginModule::PluginButtonClicked), 193 | // FCanExecuteAction()); 194 | 195 | //UToolMenus::RegisterStartupCallback(FSimpleMulticastDelegate::FDelegate::CreateRaw(this, &FRustPluginModule::RegisterMenus)); 196 | 197 | //FGlobalTabmanager::Get()->RegisterNomadTabSpawner(RustPluginTabName, FOnSpawnTab::CreateRaw(this, &FRustPluginModule::OnSpawnPluginTab)).SetDisplayName(LOCTEXT("FRustPluginTabTitle", "RustPlugin")).SetMenuType(ETabSpawnerMenuType::Hidden); 198 | 199 | IDirectoryWatcher* watcher = FModuleManager::LoadModuleChecked( 200 | TEXT("DirectoryWatcher")). 201 | Get(); 202 | watcher->RegisterDirectoryChangedCallback_Handle( 203 | *Plugin.PluginFolderPath(), 204 | IDirectoryWatcher::FDirectoryChanged::CreateRaw(this, &FRustPluginModule::OnProjectDirectoryChanged), 205 | WatcherHandle, IDirectoryWatcher::WatchOptions::IgnoreChangesInSubtree); 206 | Plugin.TryLoad(); 207 | 208 | TSharedPtr UuidFactory = MakeShareable(new FUuidGraphPanelPinFactory()); 209 | FEdGraphUtilities::RegisterVisualPinFactory(UuidFactory); 210 | 211 | // Register detail customizations 212 | { 213 | auto& PropertyModule = FModuleManager::LoadModuleChecked("PropertyEditor"); 214 | 215 | PropertyModule.RegisterCustomClassLayout( 216 | "EntityComponent", 217 | FOnGetDetailCustomizationInstance::CreateStatic(&FRustDetailCustomization::MakeInstance) 218 | ); 219 | 220 | PropertyModule.NotifyCustomizationModuleChanged(); 221 | } 222 | } 223 | 224 | void FRustPluginModule::OnProjectDirectoryChanged(const TArray& Data) 225 | { 226 | for (FFileChangeData Changed : Data) 227 | { 228 | FString Name = FPaths::GetBaseFilename(Changed.Filename); 229 | FString Ext = FPaths::GetExtension(Changed.Filename, false); 230 | 231 | FString Leaf = FPaths::GetPathLeaf(FPaths::GetPath(Changed.Filename)); 232 | const bool ChangedOrAdded = Changed.Action == FFileChangeData::FCA_Added || Changed.Action == 233 | FFileChangeData::FCA_Modified; 234 | if (Name == TEXT("rustplugin") && Ext == *PlatformExtensionName() && ChangedOrAdded) 235 | { 236 | Plugin.TryLoad(); 237 | // Only show notifications when we are in playmode otherwise notifications are a bit too spamy 238 | if (GEditor != nullptr && GEditor->IsPlaySessionInProgress()) 239 | { 240 | // Still too spamy 241 | 242 | //FNotificationInfo Info(LOCTEXT("SpawnNotification_Notification", "Hotreload: Rust")); 243 | //Info.ExpireDuration = 1.0f; 244 | //FSlateNotificationManager::Get().AddNotification(Info); 245 | } 246 | 247 | UE_LOG(LogTemp, Display, TEXT("Hotreload: Rust")); 248 | 249 | return; 250 | } 251 | } 252 | } 253 | 254 | void FRustPluginModule::ShutdownModule() 255 | { 256 | // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, 257 | // we call this function before unloading the module. 258 | 259 | UToolMenus::UnRegisterStartupCallback(this); 260 | 261 | UToolMenus::UnregisterOwner(this); 262 | 263 | FRustPluginStyle::Shutdown(); 264 | 265 | FRustPluginCommands::Unregister(); 266 | 267 | FGlobalTabmanager::Get()->UnregisterNomadTabSpawner(RustPluginTabName); 268 | } 269 | 270 | TSharedRef FRustPluginModule::OnSpawnPluginTab(const FSpawnTabArgs& SpawnTabArgs) 271 | { 272 | FText WidgetText = FText::Format( 273 | LOCTEXT("WindowWidgetText", "BAR Add code to {0} in {1} to override this window's contents"), 274 | FText::FromString(TEXT("FRustPluginModule::OnSpawnPluginTab")), 275 | FText::FromString(TEXT("RustPlugin.cpp"))); 276 | 277 | return SNew(SDockTab) 278 | .TabRole(ETabRole::NomadTab) 279 | [ 280 | // Put your tab content here! 281 | SNew(SBox) 282 | .HAlign(HAlign_Center) 283 | .VAlign(VAlign_Center) 284 | [SNew(STextBlock) 285 | .Text(WidgetText)]]; 286 | } 287 | 288 | void FRustPluginModule::PluginButtonClicked() 289 | { 290 | FGlobalTabmanager::Get()->TryInvokeTab(RustPluginTabName); 291 | } 292 | 293 | void FRustPluginModule::RegisterMenus() 294 | { 295 | // Owner will be used for cleanup in call to UToolMenus::UnregisterOwner 296 | FToolMenuOwnerScoped OwnerScoped(this); 297 | 298 | { 299 | UToolMenu* Menu = UToolMenus::Get()->ExtendMenu("LevelEditor.MainMenu.Window"); 300 | { 301 | FToolMenuSection& Section = Menu->FindOrAddSection("WindowLayout"); 302 | Section.AddMenuEntryWithCommandList(FRustPluginCommands::Get().OpenPluginWindow, PluginCommands); 303 | } 304 | } 305 | 306 | { 307 | UToolMenu* ToolbarMenu = UToolMenus::Get()->ExtendMenu("LevelEditor.LevelEditorToolBar"); 308 | { 309 | FToolMenuSection& Section = ToolbarMenu->FindOrAddSection("Settings"); 310 | { 311 | FToolMenuEntry& Entry = Section.AddEntry( 312 | FToolMenuEntry::InitToolBarButton(FRustPluginCommands::Get().OpenPluginWindow)); 313 | Entry.SetCommandList(PluginCommands); 314 | } 315 | } 316 | } 317 | } 318 | 319 | #undef LOCTEXT_NAMESPACE 320 | 321 | // IMPLEMENT_MODULE(FRustPluginModule, RustPlugin) 322 | IMPLEMENT_PRIMARY_GAME_MODULE(FRustPluginModule, RustPlugin, "RustPlugin"); 323 | -------------------------------------------------------------------------------- /RustPlugin/Source/RustPlugin/Private/RustPluginCommands.cpp: -------------------------------------------------------------------------------- 1 | // Copyright Epic Games, Inc. All Rights Reserved. 2 | 3 | #include "RustPluginCommands.h" 4 | 5 | #define LOCTEXT_NAMESPACE "FRustPluginModule" 6 | 7 | void FRustPluginCommands::RegisterCommands() 8 | { 9 | //UI_COMMAND(OpenPluginWindow, "RustPlugin", "Bring up RustPlugin window", EUserInterfaceActionType::Button, FInputGesture()); 10 | } 11 | 12 | #undef LOCTEXT_NAMESPACE 13 | -------------------------------------------------------------------------------- /RustPlugin/Source/RustPlugin/Private/RustPluginStyle.cpp: -------------------------------------------------------------------------------- 1 | // Copyright Epic Games, Inc. All Rights Reserved. 2 | 3 | #include "RustPluginStyle.h" 4 | #include "Styling/SlateStyleRegistry.h" 5 | #include "Framework/Application/SlateApplication.h" 6 | #include "Slate/SlateGameResources.h" 7 | #include "Interfaces/IPluginManager.h" 8 | 9 | TSharedPtr< FSlateStyleSet > FRustPluginStyle::StyleInstance = NULL; 10 | 11 | void FRustPluginStyle::Initialize() 12 | { 13 | if (!StyleInstance.IsValid()) 14 | { 15 | StyleInstance = Create(); 16 | FSlateStyleRegistry::RegisterSlateStyle(*StyleInstance); 17 | } 18 | } 19 | 20 | void FRustPluginStyle::Shutdown() 21 | { 22 | FSlateStyleRegistry::UnRegisterSlateStyle(*StyleInstance); 23 | ensure(StyleInstance.IsUnique()); 24 | StyleInstance.Reset(); 25 | } 26 | 27 | FName FRustPluginStyle::GetStyleSetName() 28 | { 29 | static FName StyleSetName(TEXT("RustPluginStyle")); 30 | return StyleSetName; 31 | } 32 | 33 | #define IMAGE_BRUSH( RelativePath, ... ) FSlateImageBrush( Style->RootToContentDir( RelativePath, TEXT(".png") ), __VA_ARGS__ ) 34 | #define BOX_BRUSH( RelativePath, ... ) FSlateBoxBrush( Style->RootToContentDir( RelativePath, TEXT(".png") ), __VA_ARGS__ ) 35 | #define BORDER_BRUSH( RelativePath, ... ) FSlateBorderBrush( Style->RootToContentDir( RelativePath, TEXT(".png") ), __VA_ARGS__ ) 36 | #define TTF_FONT( RelativePath, ... ) FSlateFontInfo( Style->RootToContentDir( RelativePath, TEXT(".ttf") ), __VA_ARGS__ ) 37 | #define OTF_FONT( RelativePath, ... ) FSlateFontInfo( Style->RootToContentDir( RelativePath, TEXT(".otf") ), __VA_ARGS__ ) 38 | 39 | const FVector2D Icon16x16(16.0f, 16.0f); 40 | const FVector2D Icon20x20(20.0f, 20.0f); 41 | const FVector2D Icon40x40(40.0f, 40.0f); 42 | 43 | TSharedRef< FSlateStyleSet > FRustPluginStyle::Create() 44 | { 45 | TSharedRef< FSlateStyleSet > Style = MakeShareable(new FSlateStyleSet("RustPluginStyle")); 46 | Style->SetContentRoot(IPluginManager::Get().FindPlugin("RustPlugin")->GetBaseDir() / TEXT("Resources")); 47 | 48 | Style->Set("RustPlugin.OpenPluginWindow", new IMAGE_BRUSH(TEXT("ButtonIcon_40x"), Icon40x40)); 49 | 50 | return Style; 51 | } 52 | 53 | #undef IMAGE_BRUSH 54 | #undef BOX_BRUSH 55 | #undef BORDER_BRUSH 56 | #undef TTF_FONT 57 | #undef OTF_FONT 58 | 59 | void FRustPluginStyle::ReloadTextures() 60 | { 61 | if (FSlateApplication::IsInitialized()) 62 | { 63 | FSlateApplication::Get().GetRenderer()->ReloadTextureResources(); 64 | } 65 | } 66 | 67 | const ISlateStyle& FRustPluginStyle::Get() 68 | { 69 | return *StyleInstance; 70 | } 71 | -------------------------------------------------------------------------------- /RustPlugin/Source/RustPlugin/Private/RustProperty.cpp: -------------------------------------------------------------------------------- 1 | // Fill out your copyright notice in the Description page of Project Settings. 2 | 3 | #include "RustProperty.h" 4 | 5 | #include "DetailLayoutBuilder.h" 6 | #include "RustPlugin.h" 7 | #include "RustUtils.h" 8 | #include "DetailWidgetRow.h" 9 | #include "IContentBrowserSingleton.h" 10 | #include "IDetailGroup.h" 11 | #include "EditorWidgets/Public/SAssetDropTarget.h" 12 | #include "GameFramework/GameModeBase.h" 13 | #include "Widgets/Input/SVectorInputBox.h" 14 | #include "Widgets/Layout/SBox.h" 15 | #include "Widgets/Input//SButton.h" 16 | 17 | #define LOCTEXT_NAMESPACE "RustProperty" 18 | 19 | void FRustProperty::Initialize(TSharedPtr Handle, ReflectionType Type) 20 | { 21 | auto HandleTag = Handle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FRustProperty, Tag)); 22 | // How does that not work? Falling back to an int32 instead 23 | // HandleTag->SetValue(TEnumAsByte(ERustPropertyTag::Float)); 24 | if (Type == ReflectionType::Float) 25 | { 26 | HandleTag->SetValue(ERustPropertyTag::Float); 27 | } 28 | if (Type == ReflectionType::Vector3) 29 | { 30 | HandleTag->SetValue(ERustPropertyTag::Vector); 31 | } 32 | if (Type == ReflectionType::Bool) 33 | { 34 | HandleTag->SetValue(ERustPropertyTag::Bool); 35 | } 36 | if (Type == ReflectionType::Quaternion) 37 | { 38 | HandleTag->SetValue(ERustPropertyTag::Quat); 39 | } 40 | if (Type == ReflectionType::UClass) 41 | { 42 | HandleTag->SetValue(ERustPropertyTag::Class); 43 | } 44 | if (Type == ReflectionType::USound) 45 | { 46 | HandleTag->SetValue(ERustPropertyTag::Sound); 47 | } 48 | } 49 | 50 | void FDynamicRustComponent::Reload(TSharedPtr Handle, FGuid Guid) 51 | { 52 | TSharedPtr FieldsProperty = Handle->GetChildHandle( 53 | GET_MEMBER_NAME_CHECKED(FDynamicRustComponent, Fields)); 54 | 55 | Uuid Id = ToUuid(Guid); 56 | 57 | auto Reflection = GetRustModule().Plugin.ReflectionData.Types.Find(Guid); 58 | check(Reflection); 59 | for (auto& Elem : Reflection->FieldNameToType) 60 | { 61 | if (Fields.Find(Elem.Key) == nullptr) 62 | { 63 | uint32 IndexAdded = 0; 64 | FieldsProperty->GetNumChildren(IndexAdded); 65 | FieldsProperty->AsMap()->AddItem(); 66 | 67 | auto RustPropertyEntry = FieldsProperty->GetChildHandle(IndexAdded); 68 | FRustProperty::Initialize(RustPropertyEntry, Elem.Value); 69 | auto KeyHandle = RustPropertyEntry->GetKeyHandle(); 70 | KeyHandle->SetValue(Elem.Key); 71 | } 72 | } 73 | } 74 | 75 | void FDynamicRustComponent::Initialize(TSharedPtr Handle, FGuid InitGuid) 76 | { 77 | TSharedPtr NameProperty = Handle->GetChildHandle( 78 | GET_MEMBER_NAME_CHECKED(FDynamicRustComponent, Name)); 79 | TSharedPtr RustPropertyMap = Handle->GetChildHandle( 80 | GET_MEMBER_NAME_CHECKED(FDynamicRustComponent, Fields)); 81 | 82 | Uuid Id = ToUuid(InitGuid); 83 | 84 | auto Reflection = GetRustModule().Plugin.ReflectionData.Types.Find(InitGuid); 85 | NameProperty->SetValue(Reflection->Name); 86 | 87 | uint32_t NumberOfFields = Reflection->IndexToFieldName.Num(); 88 | 89 | for (uint32_t Idx = 0; Idx < NumberOfFields; ++Idx) 90 | { 91 | auto& FieldName = *Reflection->IndexToFieldName.Find(Idx); 92 | auto Type = *Reflection->FieldNameToType.Find(FieldName); 93 | 94 | uint32 FieldPropertyIndex = 0; 95 | RustPropertyMap->GetNumChildren(FieldPropertyIndex); 96 | RustPropertyMap->AsMap()->AddItem(); 97 | 98 | auto RustPropertyEntry = RustPropertyMap->GetChildHandle(FieldPropertyIndex); 99 | RustPropertyEntry->GetKeyHandle()->SetValue(FieldName); 100 | FRustProperty::Initialize(RustPropertyEntry, Type); 101 | } 102 | } 103 | 104 | void FDynamicRustComponent::Render(TSharedRef MapHandle, IDetailCategoryBuilder& DetailBuilder, 105 | IDetailLayoutBuilder& LayoutBuilder) 106 | { 107 | uint32 NumberOfComponents; 108 | MapHandle->GetNumChildren(NumberOfComponents); 109 | for (uint32 ComponentIdx = 0; ComponentIdx < NumberOfComponents; ++ComponentIdx) 110 | { 111 | auto ComponentEntry = MapHandle->GetChildHandle(ComponentIdx); 112 | TSharedPtr NameProperty = ComponentEntry->GetChildHandle( 113 | GET_MEMBER_NAME_CHECKED(FDynamicRustComponent, Name)); 114 | 115 | if (!NameProperty.IsValid()) 116 | { 117 | continue; 118 | } 119 | 120 | FString Name; 121 | NameProperty->GetValue(Name); 122 | 123 | TSharedPtr FieldsProperty = ComponentEntry->GetChildHandle( 124 | GET_MEMBER_NAME_CHECKED(FDynamicRustComponent, Fields)); 125 | 126 | uint32 NumberOfFields = 0; 127 | FieldsProperty->GetNumChildren(NumberOfFields); 128 | 129 | auto& ComponentGroup = DetailBuilder.AddGroup(FName("Component"), FText::FromString(Name), false, true); 130 | ComponentGroup.HeaderRow().NameContent()[ 131 | FieldsProperty->CreatePropertyNameWidget(FText::FromString(Name)) 132 | ] 133 | .ExtensionContent()[ 134 | SNew(SBox) 135 | .HAlign(HAlign_Center) 136 | .VAlign(VAlign_Center) 137 | .WidthOverride(22) 138 | .HeightOverride(22)[ 139 | SNew(SButton) 140 | .ButtonStyle(FAppStyle::Get(), "SimpleButton") 141 | .OnClicked(FOnComponentRemoved::CreateLambda([MapHandle, ComponentIdx, &LayoutBuilder]() 142 | { 143 | MapHandle->AsMap()->DeleteItem(ComponentIdx); 144 | LayoutBuilder.ForceRefreshDetails(); 145 | return FReply::Handled(); 146 | })) 147 | .ContentPadding(0) 148 | [ 149 | SNew(SImage) 150 | .Image(FEditorStyle::GetBrush("Icons.Delete")) 151 | .ColorAndOpacity(FSlateColor::UseForeground()) 152 | ] 153 | ] 154 | ]; 155 | for (uint32 FieldIdx = 0; FieldIdx < NumberOfFields; ++FieldIdx) 156 | { 157 | auto RustPropertyEntry = FieldsProperty->GetChildHandle(FieldIdx); 158 | TSharedPtr TagProperty = RustPropertyEntry->GetChildHandle( 159 | GET_MEMBER_NAME_CHECKED(FRustProperty, Tag)); 160 | 161 | int32 Tag = 0; 162 | TagProperty->GetValue(Tag); 163 | 164 | TSharedPtr FieldNameProperty = RustPropertyEntry->GetKeyHandle(); 165 | 166 | FString FieldPropertyName; 167 | FieldNameProperty->GetValue(FieldPropertyName); 168 | if (Tag == ERustPropertyTag::Float) 169 | { 170 | auto FloatProperty = RustPropertyEntry->GetChildHandle( 171 | GET_MEMBER_NAME_CHECKED(FRustProperty, Float)); 172 | ComponentGroup.AddPropertyRow(FloatProperty.ToSharedRef()).DisplayName( 173 | FText::FromString(FieldPropertyName)); 174 | } 175 | if (Tag == ERustPropertyTag::Vector) 176 | { 177 | auto VectorProperty = RustPropertyEntry->GetChildHandle( 178 | GET_MEMBER_NAME_CHECKED(FRustProperty, Vector)); 179 | ComponentGroup.AddPropertyRow(VectorProperty.ToSharedRef()).DisplayName( 180 | FText::FromString(FieldPropertyName)); 181 | } 182 | if (Tag == ERustPropertyTag::Bool) 183 | { 184 | auto BoolProperty = RustPropertyEntry->GetChildHandle( 185 | GET_MEMBER_NAME_CHECKED(FRustProperty, Bool)); 186 | ComponentGroup.AddPropertyRow(BoolProperty.ToSharedRef()).DisplayName( 187 | FText::FromString(FieldPropertyName)); 188 | } 189 | if (Tag == ERustPropertyTag::Quat) 190 | { 191 | auto QuatProperty = RustPropertyEntry->GetChildHandle( 192 | GET_MEMBER_NAME_CHECKED(FRustProperty, Rotation)); 193 | ComponentGroup.AddPropertyRow(QuatProperty.ToSharedRef()).DisplayName( 194 | FText::FromString(FieldPropertyName)); 195 | } 196 | if (Tag == ERustPropertyTag::Class) 197 | { 198 | auto ClassProperty = RustPropertyEntry->GetChildHandle( 199 | GET_MEMBER_NAME_CHECKED(FRustProperty, Class)); 200 | ComponentGroup.AddPropertyRow(ClassProperty.ToSharedRef()).DisplayName( 201 | FText::FromString(FieldPropertyName)); 202 | } 203 | if (Tag == ERustPropertyTag::Sound) 204 | { 205 | auto SoundProperty = RustPropertyEntry->GetChildHandle( 206 | GET_MEMBER_NAME_CHECKED(FRustProperty, Sound)); 207 | ComponentGroup.AddPropertyRow(SoundProperty.ToSharedRef()).DisplayName( 208 | FText::FromString(FieldPropertyName)); 209 | } 210 | } 211 | } 212 | } 213 | 214 | #undef LOCTEXT_NAMESPACE 215 | -------------------------------------------------------------------------------- /RustPlugin/Source/RustPlugin/Private/RustUtils.cpp: -------------------------------------------------------------------------------- 1 | #include "RustUtils.h" 2 | #include "Modules/ModuleManager.h" 3 | #include "RustPlugin.h" 4 | #include "EngineUtils.h" 5 | #include "RustActor.h" 6 | 7 | UnrealBindings CreateBindings() 8 | { 9 | SoundFns sound_fns; 10 | sound_fns.play_sound_at_location = PlaySoundAtLocation; 11 | 12 | EditorComponentFns editor_component_fns; 13 | editor_component_fns.get_editor_component_bool = &GetEditorComponentBool; 14 | editor_component_fns.get_editor_component_float = &GetEditorComponentFloat; 15 | editor_component_fns.get_editor_component_quat = &GetEditorComponentQuat; 16 | editor_component_fns.get_editor_component_vector = &GetEditorComponentVector; 17 | editor_component_fns.get_editor_component_uobject = &GetEditorComponentUObject; 18 | editor_component_fns.get_editor_components = &GetEditorComponentUuids; 19 | 20 | PhysicsFns physics_fns = {}; 21 | physics_fns.add_force = &AddForce; 22 | physics_fns.add_impulse = &AddImpulse; 23 | physics_fns.set_velocity = &SetVelocity; 24 | physics_fns.get_velocity = &GetVelocity; 25 | physics_fns.is_simulating = &IsSimulating; 26 | physics_fns.line_trace = &LineTrace; 27 | physics_fns.get_bounding_box_extent = &GetBoundingBoxExtent; 28 | physics_fns.sweep = &Sweep; 29 | physics_fns.sweep_multi = &SweepMulti; 30 | physics_fns.get_collision_shape = &GetCollisionShape; 31 | 32 | ActorFns actor_fns = {}; 33 | actor_fns.get_spatial_data = &GetSpatialData; 34 | actor_fns.set_spatial_data = &SetSpatialData; 35 | actor_fns.set_entity_for_actor = &SetEntityForActor; 36 | actor_fns.set_view_target = &SetViewTarget; 37 | actor_fns.get_actor_components = &GetActorComponents; 38 | actor_fns.get_registered_classes = &GetRegisteredClasses; 39 | actor_fns.get_class = &GetClass; 40 | actor_fns.set_owner = &SetOwner; 41 | actor_fns.get_actor_name = &GetActorName; 42 | actor_fns.is_moveable = &IsMoveable; 43 | actor_fns.register_actor_on_overlap = &RegisterActorOnOverlap; 44 | actor_fns.register_actor_on_hit = &RegisterActorOnHit; 45 | actor_fns.get_root_component = &GetRootComponent; 46 | actor_fns.destroy_actor = &DestroyActor; 47 | 48 | UnrealBindings b = {}; 49 | b.actor_fns = actor_fns; 50 | b.sound_fns = sound_fns; 51 | b.physics_fns = physics_fns; 52 | b.editor_component_fns = editor_component_fns; 53 | b.log = &Log; 54 | b.iterate_actors = &IterateActors; 55 | b.get_action_state = &GetActionState; 56 | b.get_axis_value = &GetAxisValue; 57 | b.spawn_actor = &SpawnActor; 58 | b.get_mouse_delta = &GetMouseDelta; 59 | b.visual_log_segment = &VisualLogSegment; 60 | b.visual_log_capsule = &VisualLogCapsule; 61 | b.visual_log_location = &VisualLogLocation; 62 | return b; 63 | } 64 | 65 | Quaternion ToQuaternion(FQuat q) 66 | { 67 | Quaternion r; 68 | r.x = q.X; 69 | r.y = q.Y; 70 | r.z = q.Z; 71 | r.w = q.W; 72 | return r; 73 | } 74 | 75 | Vector3 ToVector3(FVector v) 76 | { 77 | Vector3 r; 78 | r.x = v.X; 79 | r.y = v.Y; 80 | r.z = v.Z; 81 | return r; 82 | } 83 | 84 | FVector ToFVector(Vector3 v) 85 | { 86 | return FVector(v.x, v.y, v.z); 87 | } 88 | 89 | FQuat ToFQuat(Quaternion q) 90 | { 91 | return FQuat(q.x, q.y, q.z, q.w); 92 | } 93 | 94 | AActor* ToAActor(const AActorOpaque* actor) 95 | { 96 | return (AActor*)actor; 97 | } 98 | 99 | AActor* ToAActor(AActorOpaque* actor) 100 | { 101 | return (AActor*)actor; 102 | } 103 | 104 | FGuid ToFGuid(Uuid uuid) 105 | { 106 | return FGuid(uuid.a, uuid.b, uuid.c, uuid.d); 107 | } 108 | 109 | Uuid ToUuid(FGuid guid) 110 | { 111 | Uuid uuid; 112 | uuid.a = guid.A; 113 | uuid.b = guid.B; 114 | uuid.c = guid.C; 115 | uuid.d = guid.D; 116 | return uuid; 117 | } 118 | 119 | FRustPluginModule& GetRustModule() 120 | { 121 | return FModuleManager::LoadModuleChecked(TEXT("RustPlugin")); 122 | } 123 | 124 | FColor ToFColor(Color c) 125 | { 126 | return FColor(c.r, c.g, c.b, c.a); 127 | } 128 | 129 | FCollisionShape ToFCollisionShape(CollisionShape Shape) 130 | { 131 | if (Shape.ty == CollisionShapeType::Box) 132 | { 133 | return FCollisionShape::MakeBox(FVector3f( 134 | Shape.data.collision_box.half_extent_x, 135 | Shape.data.collision_box.half_extent_y, 136 | Shape.data.collision_box.half_extent_z 137 | )); 138 | } 139 | if (Shape.ty == CollisionShapeType::Sphere) 140 | { 141 | return FCollisionShape::MakeSphere(Shape.data.sphere.radius); 142 | } 143 | 144 | if (Shape.ty == CollisionShapeType::Capsule) 145 | { 146 | return FCollisionShape::MakeCapsule( 147 | Shape.data.capsule.radius, 148 | Shape.data.capsule.half_height 149 | ); 150 | } 151 | 152 | // TODO: Unreal way? 153 | abort(); 154 | } 155 | 156 | FString ToFString(Utf8Str Str) 157 | { 158 | if(Str.len == 0) 159 | return FString(); 160 | 161 | return FString(Str.len, UTF8_TO_TCHAR(Str.ptr)); 162 | } 163 | 164 | FRustProperty* GetRustProperty(const AActorOpaque* actor, Uuid uuid, Utf8Str field) 165 | { 166 | AActor* Actor = ToAActor(actor); 167 | if (Actor == nullptr) 168 | return nullptr; 169 | 170 | UEntityComponent* EntityComponent = Actor->FindComponentByClass(); 171 | if (EntityComponent == nullptr) 172 | return nullptr; 173 | 174 | FString FieldName = ToFString(field); 175 | 176 | FDynamicRustComponent* Comp = EntityComponent->Components.Find(ToFGuid(uuid).ToString()); 177 | if (Comp == nullptr) 178 | return nullptr; 179 | return Comp->Fields.Find(FieldName); 180 | } 181 | -------------------------------------------------------------------------------- /RustPlugin/Source/RustPlugin/Private/SGraphNodeGetComponent.cpp: -------------------------------------------------------------------------------- 1 | #include "SGraphNodeGetComponent.h" 2 | 3 | #include "K2Node.h" 4 | #include "SListViewSelectorDropdownMenu.h" 5 | #include "Widgets/Layout/SScrollBox.h" 6 | 7 | #define LOCTEXT_NAMESPACE "GetComponentRust" 8 | 9 | void SGraphNodeGetComponent::Construct(const FArguments& InArgs, UK2Node* Node) 10 | { 11 | OnUuidPicked = InArgs._OnUuidPickedDelegate; 12 | this->GraphNode = Node; 13 | this->SetCursor(EMouseCursor::CardinalCross); 14 | this->UpdateGraphNode(); 15 | SelectedComponentText = InArgs._SelectedComponentText; 16 | } 17 | 18 | void SGraphNodeGetComponent::CreateBelowWidgetControls(TSharedPtr MainBox) 19 | { 20 | MainBox->AddSlot() 21 | [ 22 | SNew(SRustDropdownList).OnUuidPickedDelegate(OnUuidPicked) 23 | ]; 24 | } 25 | #undef LOCTEXT_NAMESPACE 26 | -------------------------------------------------------------------------------- /RustPlugin/Source/RustPlugin/Private/SGraphPinUuid.cpp: -------------------------------------------------------------------------------- 1 | #include "SGraphPinUuid.h" 2 | -------------------------------------------------------------------------------- /RustPlugin/Source/RustPlugin/Private/SRustDropdownList.cpp: -------------------------------------------------------------------------------- 1 | // Fill out your copyright notice in the Description page of Project Settings. 2 | 3 | #include "SRustDropdownList.h" 4 | 5 | #include "RustPlugin.h" 6 | #include "RustUtils.h" 7 | #include "SlateOptMacros.h" 8 | #include "Widgets/Input/SSearchBox.h" 9 | #include "Widgets/Layout/SScrollBox.h" 10 | #include "SListViewSelectorDropdownMenu.h" 11 | #include "Widgets/Views/SListView.h" 12 | #include "Widgets/Input/SComboButton.h" 13 | 14 | BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION 15 | #define LOCTEXT_NAMESPACE "RustDropdownList" 16 | 17 | void SRustDropdownList::Construct(const FArguments& InArgs) 18 | { 19 | OnUuidPicked = InArgs._OnUuidPickedDelegate; 20 | OnlyShowEditorComponents = InArgs._OnlyShowEditorComponents; 21 | 22 | for (auto& Elem : GetRustModule().Plugin.ReflectionData.Types) 23 | { 24 | if (InArgs._OnlyShowEditorComponents) 25 | { 26 | if (!Elem.Value.IsEditorComponent) 27 | { 28 | // We only add editor components here 29 | continue; 30 | } 31 | } 32 | 33 | FUuidViewNode* Node = new FUuidViewNode(); 34 | Node->Name = Elem.Value.Name; 35 | Node->Id = Elem.Key; 36 | AllItems.Add(MakeShareable(Node)); 37 | } 38 | 39 | Items = AllItems; 40 | SAssignNew(ListViewWidget, SListView>) 41 | .ItemHeight(24) 42 | .ListItemsSource(&Items) //The Items array is the source of this listview 43 | .SelectionMode(ESelectionMode::Single) 44 | .OnGenerateRow(this, &SRustDropdownList::OnGenerateRowForList) 45 | .OnSelectionChanged(this, &SRustDropdownList::OnClassViewerSelectionChanged); 46 | SAssignNew(SearchBox, SSearchBox) 47 | .OnTextChanged(this, &SRustDropdownList::OnFilterTextChanged) 48 | .HintText(LOCTEXT("ArrayAddElementSearchBoxHint", "Search Elements")); 49 | ChildSlot[ 50 | SNew(SComboButton) 51 | //.ButtonContent()[ 52 | // SNew(STextBlock) 53 | // .Text(LOCTEXT("ASD", "Foo")) 54 | //] 55 | .MenuContent()[ 56 | SNew(SListViewSelectorDropdownMenu>, SearchBox, ListViewWidget) 57 | [ 58 | SNew(SVerticalBox) 59 | + SVerticalBox::Slot() 60 | .AutoHeight() 61 | .Padding(4.f, 4.f, 4.f, 4.f) 62 | [ 63 | SearchBox.ToSharedRef() 64 | ] 65 | + SVerticalBox::Slot() 66 | .AutoHeight() 67 | .Padding(4.f, 4.f, 4.f, 4.f) 68 | [ 69 | ListViewWidget.ToSharedRef() 70 | ] 71 | ] 72 | ] 73 | ]; 74 | } 75 | 76 | TSharedRef SRustDropdownList::OnGenerateRowForList(TSharedPtr Item, 77 | const TSharedRef& OwnerTable) const 78 | { 79 | //Create the row 80 | return 81 | SNew(STableRow< TSharedPtr >, OwnerTable) 82 | .Padding(2.0f)[ 83 | SNew(STextBlock).Text(FText::FromString(*Item.Get()->Name)) 84 | ]; 85 | } 86 | 87 | void SRustDropdownList::OnClassViewerSelectionChanged(TSharedPtr Item, 88 | ESelectInfo::Type SelectInfo) const 89 | { 90 | if (Item.Get() != nullptr) 91 | { 92 | FText NewText = FText::FromString(Item.Get()->Name); 93 | //Selected->SetText(NewText); 94 | 95 | if (SelectInfo != ESelectInfo::OnNavigation) 96 | { 97 | OnUuidPicked.ExecuteIfBound(Item.Get()); 98 | } 99 | } 100 | } 101 | 102 | void SRustDropdownList::OnFilterTextChanged(const FText& InNewText) 103 | { 104 | Items = FilterItems(AllItems, InNewText); 105 | ListViewWidget->RequestListRefresh(); 106 | 107 | if (!Items.IsEmpty()) 108 | { 109 | ListViewWidget->SetSelection(Items[0], ESelectInfo::OnNavigation); 110 | } 111 | } 112 | 113 | TArray> SRustDropdownList::FilterItems(TArray>& AllItems, 114 | FText Text) 115 | { 116 | FString TrimmedFilterString = FText::TrimPrecedingAndTrailing(Text).ToString(); 117 | if (TrimmedFilterString.IsEmpty()) 118 | { 119 | return AllItems; 120 | } 121 | TArray> Result; 122 | 123 | for (auto& Item : AllItems) 124 | { 125 | // TODO: We can do a better search filter than that. Maybe check how the asset browser filters things? 126 | if (Item->Name.Contains(TrimmedFilterString)) 127 | { 128 | Result.Add(Item); 129 | } 130 | } 131 | return Result; 132 | } 133 | 134 | #undef LOCTEXT_NAMESPACE 135 | END_SLATE_FUNCTION_BUILD_OPTIMIZATION 136 | -------------------------------------------------------------------------------- /RustPlugin/Source/RustPlugin/Private/UEdGraphSchema_Rust.cpp: -------------------------------------------------------------------------------- 1 | // Fill out your copyright notice in the Description page of Project Settings. 2 | 3 | 4 | #include "UEdGraphSchema_Rust.h" 5 | const FName UUEdGraphSchema_Rust::PC_Uuid(TEXT("uuid-rust")); 6 | -------------------------------------------------------------------------------- /RustPlugin/Source/RustPlugin/Private/URustReflectionLibrary.cpp: -------------------------------------------------------------------------------- 1 | #include "URustReflectionLibrary.h" 2 | 3 | #include "RustUtils.h" 4 | #include "RustPlugin.h" 5 | 6 | void URustReflectionLibrary::K2_GetReflectionVector3(UUuid* Id, FEntity EntityId, int32 Index, FVector& Out) 7 | { 8 | if (Id == nullptr) 9 | return; 10 | 11 | auto Guid = ToFGuid(Id->Id); 12 | Vector3 V; 13 | Entity E; 14 | E.id = EntityId.Id; 15 | 16 | auto Module = GetRustModule(); 17 | if (Module.Plugin.IsLoaded()) 18 | { 19 | Module.Plugin.Rust.reflection_fns.get_field_vector3_value(Id->Id, E, Index, &V); 20 | Out = ToFVector(V); 21 | } 22 | } 23 | 24 | void URustReflectionLibrary::K2_GetReflectionBool(UUuid* Id, FEntity EntityId, int32 Index, bool& Out) 25 | { 26 | if (Id == nullptr) 27 | return; 28 | 29 | uint32_t Result; 30 | Entity E; 31 | E.id = EntityId.Id; 32 | 33 | auto Module = GetRustModule(); 34 | if (Module.Plugin.IsLoaded()) 35 | { 36 | Module.Plugin.Rust.reflection_fns.get_field_bool_value(Id->Id, E, Index, &Result); 37 | Out = Result == 1; 38 | } 39 | } 40 | 41 | void URustReflectionLibrary::K2_GetReflectionQuat(UUuid* Id, FEntity EntityId, int32 Index, FQuat& Out) 42 | { 43 | if (Id == nullptr) 44 | return; 45 | 46 | Quaternion Result; 47 | Entity E; 48 | E.id = EntityId.Id; 49 | 50 | auto Module = GetRustModule(); 51 | if (Module.Plugin.IsLoaded()) 52 | { 53 | Module.Plugin.Rust.reflection_fns.get_field_quat_value(Id->Id, E, Index, &Result); 54 | Out = ToFQuat(Result); 55 | } 56 | } 57 | 58 | void URustReflectionLibrary::K2_GetReflectionFloat(UUuid* Id, FEntity EntityId, int32 Index, float& Out) 59 | { 60 | if (Id == nullptr) 61 | return; 62 | 63 | float Result; 64 | Entity E; 65 | E.id = EntityId.Id; 66 | 67 | auto Module = GetRustModule(); 68 | if (Module.Plugin.IsLoaded()) 69 | { 70 | Module.Plugin.Rust.reflection_fns.get_field_float_value(Id->Id, E, Index, &Result); 71 | Out = Result; 72 | } 73 | } 74 | 75 | bool URustReflectionLibrary::K2_HasComponent(UUuid* Id, FEntity EntityId) 76 | { 77 | if (Id == nullptr) 78 | return false; 79 | 80 | Entity E; 81 | E.id = EntityId.Id; 82 | 83 | auto Module = GetRustModule(); 84 | if (Module.Plugin.IsLoaded()) 85 | { 86 | return Module.Plugin.Rust.reflection_fns.has_component(E, Id->Id) > 0; 87 | } 88 | 89 | return false; 90 | } 91 | -------------------------------------------------------------------------------- /RustPlugin/Source/RustPlugin/Public/EntityComponent.h: -------------------------------------------------------------------------------- 1 | // Fill out your copyright notice in the Description page of Project Settings. 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "Components/ActorComponent.h" 7 | #include "Bindings.h" 8 | #include "RustProperty.h" 9 | #include "EntityComponent.generated.h" 10 | 11 | 12 | class UDynamicRustComponent; 13 | USTRUCT(BlueprintType) 14 | struct FEntity 15 | { 16 | GENERATED_BODY() 17 | UPROPERTY(EditDefaultsOnly, Category=Rust) 18 | uint64 Id; 19 | 20 | Entity ToRustEntity() 21 | { 22 | Entity E; 23 | E.id = Id; 24 | return E; 25 | } 26 | }; 27 | 28 | UCLASS(BlueprintType) 29 | class UUuid : public UObject 30 | { 31 | GENERATED_BODY() 32 | public: 33 | Uuid Id; 34 | }; 35 | 36 | 37 | UCLASS(Blueprintable, meta=(BlueprintSpawnableComponent)) 38 | class RUSTPLUGIN_API UEntityComponent : public UActorComponent 39 | { 40 | GENERATED_BODY() 41 | 42 | public: 43 | UEntityComponent(); 44 | FEntity Id; 45 | // This apprently needs to be "EditAnywhere" so that we can read and write the properties in the details panel 46 | // We also hide this property manually. It should only be edited through the custom Rust components panel. 47 | UPROPERTY(EditAnywhere, Category="Rust") 48 | TMap Components; 49 | 50 | public: 51 | UFUNCTION(BlueprintCallable, Category="Rust|Utilities", meta=(Keywords = "entity")) 52 | virtual FEntity GetEntity(); 53 | }; 54 | -------------------------------------------------------------------------------- /RustPlugin/Source/RustPlugin/Public/FRustDetailCustomization.h: -------------------------------------------------------------------------------- 1 | // MyCustomization.h 2 | #pragma once 3 | 4 | #include "IDetailCustomization.h" 5 | 6 | class FRustDetailCustomization: public IDetailCustomization 7 | { 8 | public: 9 | // IDetailCustomization interface 10 | virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override; 11 | // 12 | 13 | static TSharedRef< IDetailCustomization > MakeInstance(); 14 | }; 15 | -------------------------------------------------------------------------------- /RustPlugin/Source/RustPlugin/Public/FUuidGraphPanelPinFactory.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "CoreMinimal.h" 4 | #include "EdGraphUtilities.h" 5 | 6 | class SGraphPin; 7 | 8 | class FUuidGraphPanelPinFactory: public FGraphPanelPinFactory 9 | { 10 | virtual TSharedPtr CreatePin(class UEdGraphPin* InPin) const override; 11 | }; 12 | -------------------------------------------------------------------------------- /RustPlugin/Source/RustPlugin/Public/K2Node_GetComponentRust.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "K2Node.h" 4 | #include "K2Node_CallFunction.h" 5 | #include "SGraphNodeGetComponent.h" 6 | 7 | #include "K2Node_GetComponentRust.generated.h" 8 | 9 | UCLASS() 10 | class UK2Node_GetComponentRust : public UK2Node 11 | { 12 | GENERATED_BODY() 13 | 14 | virtual void AllocateDefaultPins() override; 15 | virtual void ExpandNode(class FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph) override; 16 | virtual void GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const override; 17 | virtual FText GetNodeTitle(ENodeTitleType::Type TitleType) const override; 18 | virtual TSharedPtr CreateVisualWidget() override; 19 | void OnUuidPicked(FUuidViewNode* Name); 20 | UPROPERTY() 21 | FUuidViewNode SelectedNode; 22 | void BreakAllOutputPins(); 23 | UPROPERTY() 24 | TArray IndexPins; 25 | 26 | UEdGraphPin* CallReflection(class FKismetCompilerContext& CompilerContext, 27 | UEdGraph* SourceGraph, 28 | UFunction* ReflectionFn, 29 | UEdGraphPin* UuidPin, 30 | UEdGraphPin* EntityIdPin, 31 | UEdGraphPin* InputIdxPin, 32 | UEdGraphPin* VariableOutputPin, 33 | UEdGraphPin* PrevExecPin); 34 | }; 35 | -------------------------------------------------------------------------------- /RustPlugin/Source/RustPlugin/Public/RustActor.h: -------------------------------------------------------------------------------- 1 | // Fill out your copyright notice in the Description page of Project Settings. 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "EntityComponent.h" 7 | #include "GameFramework/Actor.h" 8 | #include "RustActor.generated.h" 9 | 10 | UCLASS() 11 | class ARustActor : public AActor 12 | { 13 | GENERATED_BODY() 14 | 15 | public: 16 | uint64 Entity; 17 | // Sets default values for this actor's properties 18 | ARustActor(); 19 | UPROPERTY(Category=Rust, EditAnywhere) 20 | TObjectPtr EntityComponent; 21 | 22 | protected: 23 | // Called when the game starts or when spawned 24 | virtual void BeginPlay() override; 25 | 26 | public: 27 | // Called every frame 28 | virtual void Tick(float DeltaTime) override; 29 | UFUNCTION(BlueprintCallable, Category="Rust", meta=(DisplayName="Get Entity Component")) 30 | UEntityComponent* GetEntityComponent() { return EntityComponent; } 31 | UFUNCTION(BlueprintCallable, Category="Rust", meta=(DisplayName="Get Entity")) 32 | FEntity GetEntity() 33 | { 34 | if(EntityComponent != nullptr) 35 | { 36 | return EntityComponent->GetEntity(); 37 | } 38 | 39 | return FEntity(); 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /RustPlugin/Source/RustPlugin/Public/RustBindingsActor.h: -------------------------------------------------------------------------------- 1 | // Fill out your copyright notice in the Description page of Project Settings. 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "GameFramework/Actor.h" 7 | #include "RustBindingsActor.generated.h" 8 | 9 | UCLASS() 10 | class RUSTPLUGIN_API ARustBindingsActor : public AActor 11 | { 12 | GENERATED_BODY() 13 | 14 | public: 15 | // Sets default values for this actor's properties 16 | ARustBindingsActor(); 17 | 18 | protected: 19 | // Called when the game starts or when spawned 20 | virtual void BeginPlay() override; 21 | 22 | public: 23 | // Called every frame 24 | virtual void Tick(float DeltaTime) override; 25 | 26 | }; 27 | -------------------------------------------------------------------------------- /RustPlugin/Source/RustPlugin/Public/RustGameModeBase.h: -------------------------------------------------------------------------------- 1 | // Fill out your copyright notice in the Description page of Project Settings. 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "GameFramework/GameModeBase.h" 7 | #include "Containers/Map.h" 8 | #include "RustGameModeBase.generated.h" 9 | 10 | class FRustPluginModule; 11 | class UPlayerInput; 12 | 13 | struct FInputMap 14 | { 15 | TMap AxisMapping; 16 | TMap ActionMapping; 17 | }; 18 | /** 19 | * 20 | */ 21 | UCLASS() 22 | class RUSTPLUGIN_API ARustGameModeBase : public AGameModeBase 23 | { 24 | GENERATED_BODY() 25 | ARustGameModeBase(); 26 | ~ARustGameModeBase(); 27 | virtual void StartPlay(); 28 | virtual void Tick(float Dt); 29 | UPlayerInput *PlayerInput; 30 | int32 Handle; 31 | virtual void PostLogin(APlayerController *NewPlayer); 32 | void OnActorSpawnedHandler(AActor *actor); 33 | 34 | public: 35 | UPROPERTY(EditAnywhere, Category = Game) 36 | TArray> RegisteredClasses; 37 | 38 | UFUNCTION() 39 | void OnActorBeginOverlap(AActor* OverlappedActor, AActor* OtherActor); 40 | 41 | UFUNCTION() 42 | void OnActorEndOverlap(AActor* OverlappedActor, AActor* OtherActor); 43 | 44 | UFUNCTION() 45 | void OnActorHit(AActor* SelfActor, AActor* OtherActor, FVector NormalImpulse, const FHitResult& Hit); 46 | 47 | UFUNCTION() 48 | void OnActorDestroyed(AActor* Actor); 49 | }; 50 | -------------------------------------------------------------------------------- /RustPlugin/Source/RustPlugin/Public/RustPlugin.h: -------------------------------------------------------------------------------- 1 | // Copyright Epic Games, Inc. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "Bindings.h" 7 | #include "Containers/Array.h" 8 | #include "Modules/ModuleInterface.h" 9 | 10 | 11 | class FToolBarBuilder; 12 | class FMenuBuilder; 13 | class FDelegateHandle; 14 | struct FFileChangeData; 15 | class FString; 16 | class ARustGameModeBase; 17 | 18 | struct FRustReflection 19 | { 20 | FString Name; 21 | uint32 NumberOfFields; 22 | bool IsEditorComponent; 23 | TMap IndexToFieldName; 24 | TMap FieldNameToType; 25 | }; 26 | 27 | struct FReflectionData 28 | { 29 | TMap Types; 30 | }; 31 | 32 | struct FPlugin { 33 | FString TargetPath; 34 | void* Handle; 35 | EntryUnrealBindingsFn Bindings; 36 | RustBindings Rust; 37 | 38 | bool NeedsInit; 39 | bool IsLoaded(); 40 | bool TryLoad(); 41 | void CallEntryPoints(); 42 | void RetrieveReflectionData(); 43 | FString PluginFolderPath(); 44 | FString PluginPath(); 45 | FString PluginFileName(); 46 | 47 | FReflectionData ReflectionData; 48 | FPlugin(); 49 | }; 50 | 51 | FString PlatformExtensionName(); 52 | 53 | 54 | class FRustPluginModule : public IModuleInterface 55 | { 56 | public: 57 | 58 | /** IModuleInterface implementation */ 59 | virtual void StartupModule() override; 60 | virtual void ShutdownModule() override; 61 | 62 | /** This function will be bound to Command (by default it will bring up plugin window) */ 63 | void PluginButtonClicked(); 64 | 65 | FPlugin Plugin; 66 | ARustGameModeBase* GameMode; 67 | void Exit(); 68 | private: 69 | 70 | void RegisterMenus(); 71 | 72 | TSharedRef OnSpawnPluginTab(const class FSpawnTabArgs& SpawnTabArgs); 73 | void OnProjectDirectoryChanged(const TArray & Data); 74 | 75 | private: 76 | TSharedPtr PluginCommands; 77 | FDelegateHandle WatcherHandle; 78 | }; 79 | -------------------------------------------------------------------------------- /RustPlugin/Source/RustPlugin/Public/RustPluginCommands.h: -------------------------------------------------------------------------------- 1 | // Copyright Epic Games, Inc. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "Framework/Commands/Commands.h" 7 | #include "RustPluginStyle.h" 8 | 9 | class FRustPluginCommands : public TCommands 10 | { 11 | public: 12 | 13 | FRustPluginCommands() 14 | : TCommands(TEXT("RustPlugin"), NSLOCTEXT("Contexts", "RustPlugin", "RustPlugin Plugin"), NAME_None, FRustPluginStyle::GetStyleSetName()) 15 | { 16 | } 17 | 18 | // TCommands<> interface 19 | virtual void RegisterCommands() override; 20 | 21 | public: 22 | TSharedPtr< FUICommandInfo > OpenPluginWindow; 23 | }; -------------------------------------------------------------------------------- /RustPlugin/Source/RustPlugin/Public/RustPluginStyle.h: -------------------------------------------------------------------------------- 1 | // Copyright Epic Games, Inc. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "Styling/SlateStyle.h" 7 | 8 | /** */ 9 | class FRustPluginStyle 10 | { 11 | public: 12 | 13 | static void Initialize(); 14 | 15 | static void Shutdown(); 16 | 17 | /** reloads textures used by slate renderer */ 18 | static void ReloadTextures(); 19 | 20 | /** @return The Slate style set for the Shooter game */ 21 | static const ISlateStyle& Get(); 22 | 23 | static FName GetStyleSetName(); 24 | 25 | private: 26 | 27 | static TSharedRef< class FSlateStyleSet > Create(); 28 | 29 | private: 30 | 31 | static TSharedPtr< class FSlateStyleSet > StyleInstance; 32 | }; -------------------------------------------------------------------------------- /RustPlugin/Source/RustPlugin/Public/RustProperty.h: -------------------------------------------------------------------------------- 1 | // Fill out your copyright notice in the Description page of Project Settings. 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "Bindings.h" 7 | #include "DetailCategoryBuilder.h" 8 | #include "Templates/SubclassOf.h" 9 | #include "UObject/Object.h" 10 | #include "RustProperty.generated.h" 11 | 12 | class USoundBase; 13 | DECLARE_DELEGATE_RetVal(FReply, FOnComponentRemoved); 14 | 15 | UENUM() 16 | enum ERustPropertyTag 17 | { 18 | Bool, 19 | Float, 20 | Vector, 21 | Quat, 22 | Class, 23 | Sound 24 | }; 25 | 26 | // TODO: This is a disgusting hack. We store all the possible variants in this struct so that we can access them 27 | // via the property system. The reason for that is `IPropertyHandle::SetValue` only implements a few overrides 28 | // like for FVector, FRotator etc. It would have been nice if we could set values for non default types. 29 | // There is `IPropertyHandle::AccessRawData` which we can use to write any data we want. But using this doesn't 30 | // update all instances of this property. Eg if we edit the blueprint base class, but we already placed this class in 31 | // a level, none of the properties would update for the blueprint classes that were already placed in the level. 32 | // But `IPropertyHandle::SetValue` seems to support that. 33 | USTRUCT() 34 | struct FRustProperty 35 | { 36 | GENERATED_BODY() 37 | 38 | //UPROPERTY(EditAnywhere) 39 | //TEnumAsByte Tag; 40 | 41 | UPROPERTY(EditAnywhere, Category=Rust) 42 | int32 Tag; 43 | 44 | UPROPERTY(EditAnywhere, Category=Rust) 45 | float Float; 46 | 47 | UPROPERTY(EditAnywhere, Category=Rust) 48 | bool Bool; 49 | 50 | UPROPERTY(EditAnywhere, Category=Rust) 51 | FVector Vector; 52 | 53 | UPROPERTY(EditAnywhere, Category=Rust) 54 | FRotator Rotation; 55 | 56 | UPROPERTY(EditAnywhere, Category=Rust) 57 | TSubclassOf Class; 58 | 59 | UPROPERTY(EditAnywhere, Category=Rust) 60 | TObjectPtr Sound; 61 | static void Initialize(TSharedPtr Handle, ReflectionType Type); 62 | }; 63 | 64 | 65 | USTRUCT() 66 | struct FDynamicRustComponent 67 | { 68 | GENERATED_BODY() 69 | 70 | UPROPERTY(EditAnywhere, Category=Rust) 71 | TMap Fields; 72 | 73 | UPROPERTY(EditAnywhere, Category=Rust) 74 | FString Name; 75 | 76 | void Reload(TSharedPtr Handle, FGuid Guid); 77 | // Initializes the property handle. It sets the same, and adds all the fields to the hashmap. 78 | static void Initialize(TSharedPtr Handle, FGuid InitGuid); 79 | static void Render(TSharedRef MapHandle, IDetailCategoryBuilder& DetailBuilder, 80 | IDetailLayoutBuilder& LayoutBuilder); 81 | }; 82 | -------------------------------------------------------------------------------- /RustPlugin/Source/RustPlugin/Public/RustUtils.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "CoreMinimal.h" 4 | #include "Bindings.h" 5 | #include "CollisionShape.h" 6 | 7 | class AActor; 8 | class FRustPluginModule; 9 | 10 | extern struct FLogCategoryRustVisualLog : public FLogCategory 11 | { 12 | FORCEINLINE FLogCategoryRustVisualLog() : FLogCategory(TEXT("RustVisualLog")) 13 | { 14 | } 15 | } RustVisualLog;; 16 | 17 | Quaternion ToQuaternion(FQuat q); 18 | 19 | Vector3 ToVector3(FVector v); 20 | 21 | FVector ToFVector(Vector3 v); 22 | FColor ToFColor(Color c); 23 | 24 | // W, X, Y, Z 25 | FQuat ToFQuat(Quaternion q); 26 | 27 | AActor* ToAActor(const AActorOpaque* actor); 28 | AActor* ToAActor(AActorOpaque* actor); 29 | 30 | FGuid ToFGuid(Uuid uuid); 31 | Uuid ToUuid(FGuid guid); 32 | 33 | UnrealBindings CreateBindings(); 34 | FRustPluginModule& GetRustModule(); 35 | 36 | FCollisionShape ToFCollisionShape(CollisionShape Shape); 37 | 38 | 39 | FString ToFString(Utf8Str Str); 40 | struct FRustProperty* GetRustProperty(const AActorOpaque* actor, Uuid uuid, Utf8Str field); 41 | -------------------------------------------------------------------------------- /RustPlugin/Source/RustPlugin/Public/SGraphNodeGetComponent.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "CoreMinimal.h" 3 | #include "KismetNodes/SGraphNodeK2Default.h" 4 | #include "Widgets/Input/SSearchBox.h" 5 | #include "SRustDropdownList.h" 6 | 7 | 8 | class SGraphNodeGetComponent : public SGraphNode 9 | { 10 | public: 11 | SLATE_BEGIN_ARGS(SGraphNodeGetComponent) 12 | { 13 | } 14 | 15 | SLATE_ARGUMENT(FOnUuidPicked, OnUuidPickedDelegate) 16 | SLATE_ATTRIBUTE( FText, SelectedComponentText ); 17 | 18 | SLATE_END_ARGS() 19 | 20 | void Construct(const FArguments& InArgs, UK2Node* Node); 21 | virtual void CreateBelowWidgetControls(TSharedPtr MainBox) override; 22 | private: 23 | FOnUuidPicked OnUuidPicked; 24 | TAttribute SelectedComponentText; 25 | }; 26 | -------------------------------------------------------------------------------- /RustPlugin/Source/RustPlugin/Public/SGraphPinUuid.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | class SGraphPinUuid 4 | { 5 | public: 6 | 7 | }; 8 | -------------------------------------------------------------------------------- /RustPlugin/Source/RustPlugin/Public/SRustDropdownList.h: -------------------------------------------------------------------------------- 1 | // Fill out your copyright notice in the Description page of Project Settings. 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "Widgets/SCompoundWidget.h" 7 | #include "Widgets/Views/SListView.h" 8 | #include "SRustDropdownList.generated.h" 9 | 10 | class SSearchBox; 11 | class STableViewBase; 12 | class ITableRow; 13 | USTRUCT() 14 | struct FUuidViewNode 15 | { 16 | GENERATED_BODY() 17 | UPROPERTY() 18 | FString Name; 19 | UPROPERTY() 20 | FGuid Id; 21 | }; 22 | 23 | DECLARE_DELEGATE_OneParam(FOnUuidPicked, FUuidViewNode*); 24 | 25 | class RUSTPLUGIN_API SRustDropdownList : public SCompoundWidget 26 | { 27 | TArray> Items; 28 | TArray> AllItems; 29 | 30 | TSharedPtr>> ListViewWidget; 31 | TSharedPtr SearchBox; 32 | FOnUuidPicked OnUuidPicked; 33 | bool OnlyShowEditorComponents; 34 | public: 35 | SLATE_BEGIN_ARGS(SRustDropdownList) 36 | { 37 | } 38 | 39 | SLATE_ARGUMENT(FOnUuidPicked, OnUuidPickedDelegate) 40 | SLATE_ARGUMENT(bool, OnlyShowEditorComponents) 41 | 42 | SLATE_END_ARGS() 43 | 44 | void Construct(const FArguments& InArgs); 45 | static TArray> FilterItems(TArray>& AllItems, FText Text); 46 | 47 | TSharedRef OnGenerateRowForList(TSharedPtr Item, 48 | const TSharedRef& OwnerTable) const; 49 | void OnClassViewerSelectionChanged(TSharedPtr Item, ESelectInfo::Type SelectInfo) const; 50 | void OnFilterTextChanged(const FText& InNewText); 51 | }; 52 | -------------------------------------------------------------------------------- /RustPlugin/Source/RustPlugin/Public/UEdGraphSchema_Rust.h: -------------------------------------------------------------------------------- 1 | // Fill out your copyright notice in the Description page of Project Settings. 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "UObject/Object.h" 7 | #include "UEdGraphSchema_Rust.generated.h" 8 | 9 | /** 10 | * 11 | */ 12 | UCLASS() 13 | class RUSTPLUGIN_API UUEdGraphSchema_Rust : public UObject 14 | { 15 | GENERATED_BODY() 16 | static const FName PC_Uuid;; 17 | }; 18 | -------------------------------------------------------------------------------- /RustPlugin/Source/RustPlugin/Public/URustReflectionLibrary.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "EntityComponent.h" 4 | #include "Kismet/BlueprintFunctionLibrary.h" 5 | #include "URustReflectionLibrary.generated.h" 6 | 7 | UCLASS() 8 | class URustReflectionLibrary: public UBlueprintFunctionLibrary 9 | { 10 | GENERATED_BODY() 11 | public: 12 | 13 | UFUNCTION(BlueprintCallable, Category=Rust) 14 | static void K2_GetReflectionVector3(UUuid* Id, FEntity EntityId, int32 Index, FVector &Out); 15 | UFUNCTION(BlueprintCallable, Category=Rust) 16 | static void K2_GetReflectionBool(UUuid* Id, FEntity EntityId, int32 Index, bool &Out); 17 | UFUNCTION(BlueprintCallable, Category=Rust) 18 | static void K2_GetReflectionQuat(UUuid* Id, FEntity EntityId, int32 Index, FQuat &Out); 19 | UFUNCTION(BlueprintCallable, Category=Rust) 20 | static void K2_GetReflectionFloat(UUuid* Id, FEntity EntityId, int32 Index, float &Out); 21 | UFUNCTION(BlueprintCallable, Category=Rust) 22 | static bool K2_HasComponent(UUuid* Id, FEntity EntityId); 23 | }; 24 | 25 | -------------------------------------------------------------------------------- /RustPlugin/Source/RustPlugin/RustPlugin.Build.cs: -------------------------------------------------------------------------------- 1 | // Copyright Epic Games, Inc. All Rights Reserved. 2 | 3 | using UnrealBuildTool; 4 | 5 | public class RustPlugin : ModuleRules 6 | { 7 | public RustPlugin(ReadOnlyTargetRules Target) : base(Target) 8 | { 9 | PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; 10 | 11 | PublicIncludePaths.AddRange( 12 | new string[] { 13 | // ... add public include paths required here ... 14 | } 15 | ); 16 | 17 | 18 | PrivateIncludePaths.AddRange( 19 | new string[] { 20 | // ... add other private include paths required here ... 21 | } 22 | ); 23 | 24 | 25 | PublicDependencyModuleNames.AddRange( 26 | new string[] 27 | { 28 | "Core", "GraphEditor", 29 | // ... add other public dependencies that you statically link with here ... 30 | } 31 | ); 32 | 33 | 34 | PrivateDependencyModuleNames.AddRange( 35 | new string[] 36 | { 37 | "Projects", 38 | "InputCore", 39 | "EditorFramework", 40 | "UnrealEd", 41 | "ToolMenus", 42 | "CoreUObject", 43 | "Engine", 44 | "Slate", 45 | "SlateCore", 46 | "BlueprintGraph", 47 | "GraphEditor", 48 | "KismetWidgets", 49 | "KismetCompiler", 50 | "PropertyEditor", 51 | "EditorWidgets", 52 | "ClassViewer", 53 | "EditorStyle", 54 | } 55 | ); 56 | 57 | 58 | DynamicallyLoadedModuleNames.AddRange( 59 | new string[] 60 | { 61 | // ... add any modules that your module loads dynamically here ... 62 | } 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Setup.bat: -------------------------------------------------------------------------------- 1 | mkdir "example\RustExample\Plugins" 2 | mklink /D "example\RustExample\Plugins\RustPlugin" "..\..\..\RustPlugin" 3 | mkdir "example\RustExample\Binaries" -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | # This template contains all of the possible sections and their default values 2 | 3 | # Note that all fields that take a lint level have these possible values: 4 | # * deny - An error will be produced and the check will fail 5 | # * warn - A warning will be produced, but the check will not fail 6 | # * allow - No warning or error will be produced, though in some cases a note 7 | # will be 8 | 9 | # The values provided in this template are the default values that will be used 10 | # when any section or field is not specified in your own configuration 11 | 12 | # If 1 or more target triples (and optionally, target_features) are specified, 13 | # only the specified targets will be checked when running `cargo deny check`. 14 | # This means, if a particular package is only ever used as a target specific 15 | # dependency, such as, for example, the `nix` crate only being used via the 16 | # `target_family = "unix"` configuration, that only having windows targets in 17 | # this list would mean the nix crate, as well as any of its exclusive 18 | # dependencies not shared by any other crates, would be ignored, as the target 19 | # list here is effectively saying which targets you are building for. 20 | targets = [ 21 | # The triple can be any string, but only the target triples built in to 22 | # rustc (as of 1.40) can be checked against actual config expressions 23 | #{ triple = "x86_64-unknown-linux-musl" }, 24 | # You can also specify which target_features you promise are enabled for a 25 | # particular target. target_features are currently not validated against 26 | # the actual valid features supported by the target architecture. 27 | #{ triple = "wasm32-unknown-unknown", features = ["atomics"] }, 28 | ] 29 | 30 | # This section is considered when running `cargo deny check advisories` 31 | # More documentation for the advisories section can be found here: 32 | # https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html 33 | [advisories] 34 | # The path where the advisory database is cloned/fetched into 35 | db-path = "~/.cargo/advisory-db" 36 | # The url(s) of the advisory databases to use 37 | db-urls = ["https://github.com/rustsec/advisory-db"] 38 | # The lint level for security vulnerabilities 39 | vulnerability = "deny" 40 | # The lint level for unmaintained crates 41 | unmaintained = "warn" 42 | # The lint level for crates that have been yanked from their source registry 43 | yanked = "warn" 44 | # The lint level for crates with security notices. Note that as of 45 | # 2019-12-17 there are no security notice advisories in 46 | # https://github.com/rustsec/advisory-db 47 | notice = "warn" 48 | # A list of advisory IDs to ignore. Note that ignored advisories will still 49 | # output a note when they are encountered. 50 | ignore = [ 51 | #"RUSTSEC-0000-0000", 52 | ] 53 | # Threshold for security vulnerabilities, any vulnerability with a CVSS score 54 | # lower than the range specified will be ignored. Note that ignored advisories 55 | # will still output a note when they are encountered. 56 | # * None - CVSS Score 0.0 57 | # * Low - CVSS Score 0.1 - 3.9 58 | # * Medium - CVSS Score 4.0 - 6.9 59 | # * High - CVSS Score 7.0 - 8.9 60 | # * Critical - CVSS Score 9.0 - 10.0 61 | #severity-threshold = 62 | 63 | # This section is considered when running `cargo deny check licenses` 64 | # More documentation for the licenses section can be found here: 65 | # https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html 66 | [licenses] 67 | # The lint level for crates which do not have a detectable license 68 | unlicensed = "deny" 69 | # List of explicitly allowed licenses 70 | # See https://spdx.org/licenses/ for list of possible licenses 71 | # [possible values: any SPDX 3.11 short identifier (+ optional exception)]. 72 | allow = [ 73 | "MIT", 74 | "Apache-2.0", 75 | "Apache-2.0 WITH LLVM-exception", 76 | "MPL-2.0", 77 | "BSD-3-Clause", 78 | "Unicode-DFS-2016" 79 | ] 80 | # List of explicitly disallowed licenses 81 | # See https://spdx.org/licenses/ for list of possible licenses 82 | # [possible values: any SPDX 3.11 short identifier (+ optional exception)]. 83 | deny = [ 84 | #"Nokia", 85 | ] 86 | # Lint level for licenses considered copyleft 87 | copyleft = "warn" 88 | # Blanket approval or denial for OSI-approved or FSF Free/Libre licenses 89 | # * both - The license will be approved if it is both OSI-approved *AND* FSF 90 | # * either - The license will be approved if it is either OSI-approved *OR* FSF 91 | # * osi-only - The license will be approved if is OSI-approved *AND NOT* FSF 92 | # * fsf-only - The license will be approved if is FSF *AND NOT* OSI-approved 93 | # * neither - This predicate is ignored and the default lint level is used 94 | allow-osi-fsf-free = "neither" 95 | # Lint level used when no other predicates are matched 96 | # 1. License isn't in the allow or deny lists 97 | # 2. License isn't copyleft 98 | # 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither" 99 | default = "deny" 100 | # The confidence threshold for detecting a license from license text. 101 | # The higher the value, the more closely the license text must be to the 102 | # canonical license text of a valid SPDX license file. 103 | # [possible values: any between 0.0 and 1.0]. 104 | confidence-threshold = 0.8 105 | # Allow 1 or more licenses on a per-crate basis, so that particular licenses 106 | # aren't accepted for every possible crate as with the normal allow list 107 | exceptions = [ 108 | # Each entry is the crate and version constraint, and its specific allow 109 | # list 110 | #{ allow = ["Zlib"], name = "adler32", version = "*" }, 111 | ] 112 | 113 | # Some crates don't have (easily) machine readable licensing information, 114 | # adding a clarification entry for it allows you to manually specify the 115 | # licensing information 116 | #[[licenses.clarify]] 117 | # The name of the crate the clarification applies to 118 | #name = "ring" 119 | # The optional version constraint for the crate 120 | #version = "*" 121 | # The SPDX expression for the license requirements of the crate 122 | #expression = "MIT AND ISC AND OpenSSL" 123 | # One or more files in the crate's source used as the "source of truth" for 124 | # the license expression. If the contents match, the clarification will be used 125 | # when running the license check, otherwise the clarification will be ignored 126 | # and the crate will be checked normally, which may produce warnings or errors 127 | # depending on the rest of your configuration 128 | #license-files = [ 129 | # Each entry is a crate relative path, and the (opaque) hash of its contents 130 | #{ path = "LICENSE", hash = 0xbd0eed23 } 131 | #] 132 | 133 | [licenses.private] 134 | # If true, ignores workspace crates that aren't published, or are only 135 | # published to private registries. 136 | # To see how to mark a crate as unpublished (to the official registry), 137 | # visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field. 138 | ignore = false 139 | # One or more private registries that you might publish crates to, if a crate 140 | # is only published to private registries, and ignore is true, the crate will 141 | # not have its license(s) checked 142 | registries = [ 143 | #"https://sekretz.com/registry 144 | ] 145 | 146 | # This section is considered when running `cargo deny check bans`. 147 | # More documentation about the 'bans' section can be found here: 148 | # https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html 149 | [bans] 150 | # Lint level for when multiple versions of the same crate are detected 151 | multiple-versions = "warn" 152 | # Lint level for when a crate version requirement is `*` 153 | wildcards = "allow" 154 | # The graph highlighting used when creating dotgraphs for crates 155 | # with multiple versions 156 | # * lowest-version - The path to the lowest versioned duplicate is highlighted 157 | # * simplest-path - The path to the version with the fewest edges is highlighted 158 | # * all - Both lowest-version and simplest-path are used 159 | highlight = "all" 160 | # List of crates that are allowed. Use with care! 161 | allow = [ 162 | #{ name = "ansi_term", version = "=0.11.0" }, 163 | ] 164 | # List of crates to deny 165 | deny = [ 166 | # Each entry the name of a crate and a version range. If version is 167 | # not specified, all versions will be matched. 168 | #{ name = "ansi_term", version = "=0.11.0" }, 169 | # 170 | # Wrapper crates can optionally be specified to allow the crate when it 171 | # is a direct dependency of the otherwise banned crate 172 | #{ name = "ansi_term", version = "=0.11.0", wrappers = [] }, 173 | ] 174 | # Certain crates/versions that will be skipped when doing duplicate detection. 175 | skip = [ 176 | #{ name = "ansi_term", version = "=0.11.0" }, 177 | ] 178 | # Similarly to `skip` allows you to skip certain crates during duplicate 179 | # detection. Unlike skip, it also includes the entire tree of transitive 180 | # dependencies starting at the specified crate, up to a certain depth, which is 181 | # by default infinite 182 | skip-tree = [ 183 | #{ name = "ansi_term", version = "=0.11.0", depth = 20 }, 184 | ] 185 | 186 | # This section is considered when running `cargo deny check sources`. 187 | # More documentation about the 'sources' section can be found here: 188 | # https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html 189 | [sources] 190 | # Lint level for what to happen when a crate from a crate registry that is not 191 | # in the allow list is encountered 192 | unknown-registry = "warn" 193 | # Lint level for what to happen when a crate from a git repository that is not 194 | # in the allow list is encountered 195 | unknown-git = "warn" 196 | # List of URLs for allowed crate registries. Defaults to the crates.io index 197 | # if not specified. If it is specified but empty, no registries are allowed. 198 | allow-registry = ["https://github.com/rust-lang/crates.io-index"] 199 | # List of URLs for allowed Git repositories 200 | allow-git = [] 201 | 202 | [sources.allow-org] 203 | ## 1 or more github.com organizations to allow git sources for 204 | #github = [""] 205 | ## 1 or more gitlab.com organizations to allow git sources for 206 | #gitlab = [""] 207 | ## 1 or more bitbucket.org organizations to allow git sources for 208 | #bitbucket = [""] 209 | -------------------------------------------------------------------------------- /gameplay-plugins/unreal-movement/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "unreal-movement" 3 | version = "0.1.0" 4 | edition = "2021" 5 | license = "MIT OR Apache-2.0" 6 | 7 | [dependencies] 8 | unreal-api = { path = "../../unreal-api"} 9 | unreal-reflect = { path = "../../unreal-reflect" } 10 | bevy_ecs = "0.8" 11 | log = "0.4" 12 | -------------------------------------------------------------------------------- /gameplay-plugins/unreal-movement/src/lib.rs: -------------------------------------------------------------------------------- 1 | use bevy_ecs::{prelude::*, query::WorldQuery}; 2 | use unreal_api::api::{SweepHit, SweepParams, UnrealApi}; 3 | use unreal_api::Component; 4 | use unreal_api::{ 5 | core::{ActorComponent, CoreStage, Frame, TransformComponent}, 6 | ffi, 7 | input::Input, 8 | log::LogCategory, 9 | math::{Quat, Vec3, Vec3Swizzles}, 10 | module::Module, 11 | physics::PhysicsComponent, 12 | plugin::Plugin, 13 | register_components, 14 | }; 15 | fn project_onto_plane(dir: Vec3, normal: Vec3) -> Vec3 { 16 | dir - normal * Vec3::dot(dir, normal) 17 | } 18 | 19 | #[derive(Debug, Copy, Clone)] 20 | pub enum MovementState { 21 | Walking, 22 | Falling, 23 | Gliding, 24 | } 25 | 26 | pub struct PlayerInput; 27 | impl PlayerInput { 28 | pub const MOVE_FORWARD: &'static str = "MoveForward"; 29 | pub const MOVE_RIGHT: &'static str = "MoveRight"; 30 | pub const LOOK_UP: &'static str = "LookUp"; 31 | pub const TURN_RIGHT: &'static str = "TurnRight"; 32 | pub const TOGGLE_CAMERA: &'static str = "ToggleCamera"; 33 | pub const JUMP: &'static str = "Jump"; 34 | } 35 | 36 | impl Default for MovementState { 37 | fn default() -> Self { 38 | Self::Walking 39 | } 40 | } 41 | 42 | #[derive(Default, Debug, Component)] 43 | #[uuid = "fc8bd668-fc0a-4ab7-8b3d-f0f22bb539e2"] 44 | pub struct MovementComponent { 45 | pub velocity: Vec3, 46 | pub is_falling: bool, 47 | pub is_flying: bool, 48 | pub view: Quat, 49 | } 50 | 51 | #[derive(Default, Debug, Component)] 52 | #[uuid = "ac41cdd4-3311-45ef-815c-9a31adbe4098"] 53 | pub struct CharacterControllerComponent { 54 | pub horizontal_velocity: Vec3, 55 | pub vertical_velocity: Vec3, 56 | pub camera_view: Quat, 57 | #[reflect(skip)] 58 | pub movement_state: MovementState, 59 | pub visual_rotation: Quat, 60 | } 61 | 62 | #[derive(Debug, Component)] 63 | #[uuid = "16ca6de6-7a30-412d-8bef-4ee96e18a101"] 64 | #[reflect(editor)] 65 | pub struct CharacterConfigComponent { 66 | pub max_movement_speed: f32, 67 | pub gravity_dir: Vec3, 68 | pub gravity_strength: f32, 69 | pub max_walkable_slope: f32, 70 | pub step_size: f32, 71 | pub walk_offset: f32, 72 | pub jump_velocity: f32, 73 | pub max_gliding_downwards_speed: f32, 74 | pub gliding_gravity_scale: f32, 75 | pub ground_offset: f32, 76 | } 77 | 78 | impl CharacterConfigComponent { 79 | pub fn is_walkable(&self, normal: Vec3) -> bool { 80 | Vec3::dot(normal, Vec3::Z) > f32::to_radians(self.max_walkable_slope).cos() 81 | } 82 | } 83 | impl Default for CharacterConfigComponent { 84 | fn default() -> Self { 85 | Self { 86 | max_movement_speed: 500.0, 87 | gravity_dir: -Vec3::Z, 88 | gravity_strength: 981.0, 89 | max_walkable_slope: 50.0, 90 | step_size: 65.0, 91 | walk_offset: 2.0, 92 | jump_velocity: 600.0, 93 | max_gliding_downwards_speed: 100.0, 94 | gliding_gravity_scale: 0.2, 95 | ground_offset: 2.0, 96 | } 97 | } 98 | } 99 | 100 | pub struct MovementLog; 101 | impl MovementLog { 102 | pub const STEP_UP: LogCategory = LogCategory::new("StepUp"); 103 | pub const PENETRATION: LogCategory = LogCategory::new("Penetration"); 104 | } 105 | pub enum MovementHit { 106 | Slope { normal: Vec3 }, 107 | Wall { hit: SweepHit }, 108 | } 109 | 110 | pub struct FloorHit { 111 | pub entity: Entity, 112 | pub impact_location: Vec3, 113 | } 114 | 115 | pub struct StepUpResult { 116 | location: Vec3, 117 | } 118 | 119 | fn do_walking( 120 | movement: &mut MovementQueryItem, 121 | input: &Input, 122 | dt: f32, 123 | query: &Query<&PhysicsComponent>, 124 | api: &UnrealApi, 125 | ) -> Option { 126 | if let Some(hit) = movement.find_floor(api) { 127 | movement.controller.vertical_velocity = Vec3::ZERO; 128 | movement.transform.position.z = hit.impact_location.z 129 | + movement.physics.get_collision_shape().extent().z 130 | + movement.config.walk_offset; 131 | 132 | if input.is_action_pressed(PlayerInput::JUMP) { 133 | movement.controller.vertical_velocity.z += movement.config.jump_velocity; 134 | return Some(MovementState::Falling); 135 | } 136 | let phys = query.get(hit.entity).ok(); 137 | let velocity = phys.map(|p| p.velocity).unwrap_or_default(); 138 | movement.do_movement(velocity, dt, api); 139 | None 140 | } else { 141 | Some(MovementState::Falling) 142 | } 143 | } 144 | 145 | fn do_falling( 146 | movement: &mut MovementQueryItem, 147 | input: &Input, 148 | dt: f32, 149 | api: &UnrealApi, 150 | ) -> Option { 151 | let is_downwards = movement.controller.vertical_velocity.z < 0.0; 152 | if movement.find_floor(api).is_some() && is_downwards { 153 | Some(MovementState::Walking) 154 | } else if input.is_action_pressed(PlayerInput::JUMP) { 155 | Some(MovementState::Gliding) 156 | } else { 157 | movement.controller.vertical_velocity += 158 | movement.config.gravity_dir * movement.config.gravity_strength * dt; 159 | movement.do_movement(Vec3::ZERO, dt, api); 160 | None 161 | } 162 | } 163 | 164 | fn do_gliding( 165 | movement: &mut MovementQueryItem, 166 | input: &Input, 167 | dt: f32, 168 | api: &UnrealApi, 169 | ) -> Option { 170 | let is_downwards = movement.controller.vertical_velocity.z < 0.0; 171 | 172 | if movement.find_floor(api).is_some() && is_downwards { 173 | Some(MovementState::Walking) 174 | } else if input.is_action_pressed(PlayerInput::JUMP) { 175 | Some(MovementState::Falling) 176 | } else { 177 | movement.controller.vertical_velocity += movement.config.gravity_dir 178 | * movement.config.gravity_strength 179 | * movement.config.gliding_gravity_scale 180 | * dt; 181 | 182 | movement.controller.vertical_velocity.z = f32::max( 183 | -movement.config.max_gliding_downwards_speed, 184 | movement.controller.vertical_velocity.z, 185 | ); 186 | movement.do_movement(Vec3::ZERO, dt, api); 187 | 188 | None 189 | } 190 | } 191 | 192 | #[derive(WorldQuery)] 193 | #[world_query(mutable)] 194 | pub struct MovementQuery { 195 | entity: Entity, 196 | actor: &'static ActorComponent, 197 | transform: &'static mut TransformComponent, 198 | physics: &'static PhysicsComponent, 199 | controller: &'static mut CharacterControllerComponent, 200 | config: &'static CharacterConfigComponent, 201 | } 202 | impl<'w> MovementQueryItem<'w> { 203 | pub fn try_step_up(&self, move_result: &SweepHit, api: &UnrealApi) -> Option { 204 | let params = SweepParams::default() 205 | // Don't test against ourselves 206 | .add_ignored_entity(self.entity); 207 | 208 | // We want to slighty offset the start of the sweep in the direction we were moving, 209 | // otherwise we might miss the downwards sweep test 210 | let offset = self.controller.horizontal_velocity.normalize_or_zero() * 5.0; 211 | 212 | let sweep_start = move_result.location 213 | + offset 214 | + Vec3::Z * (self.config.step_size + self.config.ground_offset); 215 | 216 | api.sweep( 217 | sweep_start, 218 | // We want to sweep downards but no more than we can step 219 | sweep_start - Vec3::Z * self.config.step_size, 220 | self.transform.rotation, 221 | self.physics.get_collision_shape(), 222 | params, 223 | ) 224 | .and_then(|hit| { 225 | // Nope we started in penetration which means the object in front of us is higher than 226 | // we can step 227 | // Or there could not be enough space on top of the object we are trying to step on 228 | if hit.start_in_penentration { 229 | return None; 230 | } 231 | unreal_api::log::visual_log_shape( 232 | MovementLog::STEP_UP, 233 | self.actor.actor, 234 | sweep_start, 235 | self.transform.rotation, 236 | self.physics.get_collision_shape(), 237 | ffi::Color::BLUE, 238 | ); 239 | 240 | unreal_api::log::visual_log_shape( 241 | MovementLog::STEP_UP, 242 | self.actor.actor, 243 | hit.location, 244 | self.transform.rotation, 245 | self.physics.get_collision_shape(), 246 | ffi::Color::RED, 247 | ); 248 | 249 | unreal_api::log::visual_log_location( 250 | MovementLog::STEP_UP, 251 | self.actor.actor, 252 | hit.impact_location, 253 | 5.0, 254 | ffi::Color::GREEN, 255 | ); 256 | Some(StepUpResult { 257 | location: hit.location, 258 | }) 259 | }) 260 | } 261 | pub fn resolve_possible_penetration(&self, api: &UnrealApi) -> Option { 262 | let params = SweepParams::default().add_ignored_entity(self.entity); 263 | let shape = self.physics.get_collision_shape(); 264 | api.sweep( 265 | self.transform.position, 266 | self.transform.position + Vec3::Z * 10.0, 267 | self.transform.rotation, 268 | shape, 269 | params, 270 | ) 271 | .and_then(|hit| { 272 | if hit.start_in_penentration { 273 | let new_location = hit.location + hit.normal * (hit.penetration_depth + 2.0); 274 | unreal_api::log::visual_log_location( 275 | MovementLog::PENETRATION, 276 | self.actor.actor, 277 | hit.location, 278 | 10.0, 279 | ffi::Color::RED, 280 | ); 281 | unreal_api::log::visual_log_location( 282 | MovementLog::PENETRATION, 283 | self.actor.actor, 284 | new_location, 285 | 10.0, 286 | ffi::Color::GREEN, 287 | ); 288 | 289 | Some(new_location) 290 | } else { 291 | None 292 | } 293 | }) 294 | } 295 | 296 | pub fn do_movement(&mut self, velocity: Vec3, dt: f32, api: &UnrealApi) { 297 | if let Some(hit) = self.movement_hit(self.controller.horizontal_velocity, dt, api) { 298 | match hit { 299 | MovementHit::Slope { normal } => { 300 | self.controller.horizontal_velocity = 301 | project_onto_plane(self.controller.horizontal_velocity, normal) 302 | .normalize_or_zero() 303 | * self.config.max_movement_speed 304 | } 305 | MovementHit::Wall { hit } => { 306 | let is_wall = Vec3::dot(hit.impact_normal, Vec3::Z) < 0.2; 307 | 308 | if is_wall { 309 | if let Some(step_result) = self.try_step_up(&hit, api) { 310 | self.transform.position = 311 | step_result.location + Vec3::Z * self.config.ground_offset; 312 | } else { 313 | let wall_normal = 314 | hit.impact_normal.xy().extend(0.0).normalize_or_zero(); 315 | 316 | self.controller.horizontal_velocity = 317 | project_onto_plane(self.controller.horizontal_velocity, wall_normal) 318 | } 319 | } else { 320 | let wall_normal = hit.impact_normal.xy().extend(0.0).normalize_or_zero(); 321 | 322 | self.controller.horizontal_velocity = 323 | project_onto_plane(self.controller.horizontal_velocity, wall_normal) 324 | } 325 | } 326 | } 327 | } 328 | { 329 | let params = SweepParams::default().add_ignored_entity(self.entity); 330 | 331 | if let Some(hit) = api.sweep( 332 | self.transform.position, 333 | self.transform.position + self.controller.vertical_velocity * dt, 334 | self.transform.rotation, 335 | self.physics.get_collision_shape(), 336 | params, 337 | ) { 338 | // Lazy: lets just set the vertical_velocity to zero if we hit something 339 | if !hit.start_in_penentration { 340 | self.controller.vertical_velocity = Vec3::ZERO; 341 | } 342 | } 343 | } 344 | self.transform.position += 345 | (self.controller.horizontal_velocity + self.controller.vertical_velocity + velocity) 346 | * dt; 347 | 348 | if self.controller.horizontal_velocity.length() > 0.2 { 349 | let velocity_dir = self.controller.horizontal_velocity.normalize_or_zero(); 350 | let target_rot = Quat::from_rotation_z(f32::atan2(velocity_dir.y, velocity_dir.x)); 351 | self.transform.rotation = Quat::lerp(self.transform.rotation, target_rot, dt * 10.0); 352 | } 353 | } 354 | pub fn movement_hit(&self, velocity: Vec3, dt: f32, api: &UnrealApi) -> Option { 355 | let params = SweepParams::default().add_ignored_entity(self.entity); 356 | 357 | if let Some(hit) = api.sweep( 358 | self.transform.position, 359 | self.transform.position + velocity * dt, 360 | self.transform.rotation, 361 | self.physics.get_collision_shape(), 362 | params, 363 | ) { 364 | let is_moving_against = Vec3::dot(hit.impact_normal, velocity) < 0.0; 365 | 366 | let is_walkable = self.config.is_walkable(hit.impact_normal); 367 | if is_walkable && is_moving_against { 368 | return Some(MovementHit::Slope { 369 | normal: hit.impact_normal, 370 | }); 371 | } else { 372 | return Some(MovementHit::Wall { hit }); 373 | } 374 | } 375 | None 376 | } 377 | 378 | pub fn find_floor(&self, api: &UnrealApi) -> Option { 379 | let params = SweepParams::default().add_ignored_entity(self.entity); 380 | let shape = self.physics.get_collision_shape(); 381 | api.sweep( 382 | self.transform.position, 383 | self.transform.position + self.config.gravity_dir * 500.0, 384 | self.transform.rotation, 385 | // we scale the physics shape down to avoid collision with nearby walls 386 | shape.scale(0.9), 387 | params, 388 | ) 389 | .and_then(|hit| { 390 | let is_walkable = self.config.is_walkable(hit.impact_normal); 391 | 392 | let is_within_range = self.config.step_size + hit.impact_location.z 393 | >= self.transform.position.z - shape.extent().z; 394 | if is_walkable && is_within_range { 395 | Some(FloorHit { 396 | entity: hit.entity, 397 | impact_location: hit.impact_location, 398 | }) 399 | } else { 400 | None 401 | } 402 | }) 403 | } 404 | } 405 | 406 | fn character_control_system( 407 | input: Res, 408 | frame: Res, 409 | api: Res, 410 | mut query: Query, 411 | phys: Query<&PhysicsComponent>, 412 | ) { 413 | let api = &api; 414 | let forward = input 415 | .get_axis_value(PlayerInput::MOVE_FORWARD) 416 | .unwrap_or(0.0); 417 | let right = input.get_axis_value(PlayerInput::MOVE_RIGHT).unwrap_or(0.0); 418 | let player_input = Vec3::new(forward, right, 0.0).normalize_or_zero(); 419 | 420 | for mut movement in query.iter_mut() { 421 | let mut input_dir = movement.controller.camera_view * player_input; 422 | input_dir.z = 0.0; 423 | movement.controller.horizontal_velocity = 424 | input_dir.normalize_or_zero() * movement.config.max_movement_speed; 425 | 426 | if let Some(new_position) = movement.resolve_possible_penetration(api) { 427 | movement.transform.position = new_position; 428 | } 429 | 430 | let new_state = match movement.controller.movement_state { 431 | MovementState::Walking => do_walking(&mut movement, &input, frame.dt, &phys, api), 432 | MovementState::Falling => do_falling(&mut movement, &input, frame.dt, api), 433 | MovementState::Gliding => do_gliding(&mut movement, &input, frame.dt, api), 434 | }; 435 | 436 | if let Some(new_state) = new_state { 437 | movement.controller.movement_state = new_state; 438 | } 439 | } 440 | } 441 | 442 | fn update_movement_component( 443 | mut query: Query<(&CharacterControllerComponent, &mut MovementComponent)>, 444 | ) { 445 | for (controller, mut movement) in query.iter_mut() { 446 | movement.velocity = controller.horizontal_velocity + controller.vertical_velocity; 447 | movement.is_falling = matches!(controller.movement_state, MovementState::Falling); 448 | movement.is_flying = matches!(controller.movement_state, MovementState::Gliding); 449 | } 450 | } 451 | 452 | pub struct MovementPlugin; 453 | 454 | impl Plugin for MovementPlugin { 455 | fn build(&self, module: &mut Module) { 456 | register_components! { 457 | MovementComponent, 458 | CharacterConfigComponent, 459 | => module 460 | }; 461 | 462 | module.add_system_set_to_stage( 463 | CoreStage::Update, 464 | SystemSet::new() 465 | .with_system(character_control_system) 466 | .with_system(update_movement_component.after(character_control_system)), 467 | ); 468 | } 469 | } 470 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | echo "Symlinking RustPlugin into RustExample" 2 | mkdir -p example/RustExample/Plugins 3 | ln -s ../../../RustPlugin example/RustExample/Plugins/RustPlugin 4 | mkdir -p example/RustExample/Binaries 5 | -------------------------------------------------------------------------------- /unreal-api-derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "unreal-api-derive" 3 | version = "0.1.0" 4 | edition = "2021" 5 | license = "MIT OR Apache-2.0" 6 | 7 | [lib] 8 | proc-macro = true 9 | 10 | [dependencies] 11 | 12 | syn = "1.0" 13 | proc-macro2 = "1.0" 14 | quote = "1.0" 15 | uuid = { version = "1", features = ["v4", "serde"] } 16 | darling = "0.14.1" 17 | -------------------------------------------------------------------------------- /unreal-api-derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | use syn::DeriveInput; 2 | 3 | mod reflect; 4 | mod type_uuid; 5 | use quote::quote; 6 | 7 | #[proc_macro_derive(Component, attributes(uuid, reflect))] 8 | pub fn component_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 9 | let ast: DeriveInput = syn::parse(input).unwrap(); 10 | 11 | let reflect = reflect::reflect_derive(&ast); 12 | let type_uuid = type_uuid::type_uuid_derive(&ast); 13 | quote! { 14 | #reflect 15 | #type_uuid 16 | } 17 | .into() 18 | } 19 | -------------------------------------------------------------------------------- /unreal-api-derive/src/reflect.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | 3 | use darling::{FromDeriveInput, FromField}; 4 | use proc_macro2::Span; 5 | use quote::quote; 6 | use syn::*; 7 | 8 | #[derive(Debug, FromField)] 9 | #[darling(attributes(reflect))] 10 | pub struct ReflectField { 11 | ident: Option, 12 | ty: Type, 13 | #[darling(default)] 14 | skip: bool, 15 | } 16 | #[derive(Debug, FromDeriveInput)] 17 | #[darling(attributes(reflect))] 18 | pub struct ReflectEditor { 19 | #[darling(default)] 20 | editor: bool, 21 | } 22 | 23 | pub fn reflect_derive(ast: &DeriveInput) -> proc_macro2::TokenStream { 24 | let is_editor_component = 25 | ReflectEditor::from_derive_input(ast).map_or(false, |reflect| reflect.editor); 26 | 27 | if let Data::Struct(data) = &ast.data { 28 | let literal_name = LitStr::new(&ast.ident.to_string(), Span::call_site()); 29 | let reflect_struct_ident = Ident::new(&format!("{}Reflect", ast.ident), Span::call_site()); 30 | let insert_struct_ident = 31 | Ident::new(&format!("{}InsertComponent", ast.ident), Span::call_site()); 32 | let struct_ident = &ast.ident; 33 | 34 | let fields: Vec = data 35 | .fields 36 | .iter() 37 | .map(|field| ReflectField::from_field(field).unwrap()) 38 | .collect(); 39 | 40 | let reflect_fields: Vec<&ReflectField> = 41 | fields.iter().filter(|field| !field.skip).collect(); 42 | 43 | let number_of_fields = reflect_fields.len() as u32; 44 | 45 | let field_indices: Vec = (0..number_of_fields).collect(); 46 | let field_types: Vec<&'_ Type> = reflect_fields.iter().map(|field| &field.ty).collect(); 47 | let field_names: Vec = reflect_fields 48 | .iter() 49 | .map(|field| { 50 | let ident = field.ident.as_ref().unwrap(); 51 | LitStr::new(&ident.to_string(), Span::call_site()) 52 | }) 53 | .collect(); 54 | 55 | let field_idents: Vec<&'_ Ident> = reflect_fields 56 | .iter() 57 | .map(|field| field.ident.as_ref().unwrap()) 58 | .collect(); 59 | 60 | let self_ty: Type = syn::parse_str(&ast.ident.to_string()).unwrap(); 61 | 62 | let field_methods = if number_of_fields > 0 { 63 | quote! { 64 | fn get_field_type(&self, idx: u32) -> Option { 65 | match idx { 66 | #( 67 | #field_indices => Some(<#field_types as unreal_api::registry::ReflectStatic>::TYPE), 68 | )* 69 | _ => None 70 | } 71 | } 72 | fn get_field_name(&self, idx: u32) -> Option<&'static str> { 73 | match idx { 74 | #( 75 | #field_indices => Some(#field_names), 76 | )* 77 | _ => None 78 | } 79 | } 80 | fn has_component(&self, world: &unreal_api::World, entity: unreal_api::Entity) -> bool { 81 | world 82 | .get_entity(entity) 83 | .and_then(|entity_ref| entity_ref.get::<#self_ty>()).is_some() 84 | } 85 | fn get_field_value(&self, world: &unreal_api::World, entity: unreal_api::Entity, idx: u32) -> Option { 86 | world 87 | .get_entity(entity) 88 | .and_then(|entity_ref| entity_ref.get::<#self_ty>()) 89 | .and_then(|component| { 90 | let ty = match idx { 91 | #( 92 | #field_indices => component.#field_idents.get_value(), 93 | )* 94 | _ => return None, 95 | }; 96 | Some(ty) 97 | }) 98 | } 99 | } 100 | } else { 101 | quote!() 102 | }; 103 | 104 | let insert_component = if is_editor_component { 105 | quote! { 106 | impl unreal_api::editor_component::InsertEditorComponent for #insert_struct_ident { 107 | unsafe fn insert_component( 108 | &self, 109 | actor: *const unreal_api::ffi::AActorOpaque, 110 | uuid: unreal_api::uuid::Uuid, 111 | commands: &mut unreal_api::ecs::system::EntityCommands<'_, '_, '_>, 112 | ) { 113 | use unreal_api::editor_component::GetEditorComponentValue; 114 | let component = #struct_ident { 115 | #( 116 | #field_idents: #field_types::get(actor, uuid, #field_names).expect(#field_names), 117 | )* 118 | }; 119 | commands.insert(component); 120 | } 121 | } 122 | } 123 | } else { 124 | quote! {} 125 | }; 126 | 127 | let register_editor_component = if is_editor_component { 128 | quote! { 129 | registry.insert_editor_component.insert( 130 | <#struct_ident as unreal_api::TypeUuid>::TYPE_UUID, 131 | Box::new(#insert_struct_ident), 132 | ); 133 | 134 | } 135 | } else { 136 | quote!() 137 | }; 138 | 139 | quote! { 140 | pub struct #reflect_struct_ident; 141 | 142 | impl unreal_api::registry::ReflectDyn for #reflect_struct_ident { 143 | fn name(&self) -> &'static str { 144 | #literal_name 145 | } 146 | 147 | fn number_of_fields(&self) -> u32 { 148 | #number_of_fields 149 | } 150 | 151 | #field_methods 152 | 153 | fn get_value(&self) -> unreal_api::registry::ReflectValue { 154 | unreal_api::registry::ReflectValue::Composite 155 | } 156 | 157 | } 158 | impl unreal_api::registry::ReflectStatic for #reflect_struct_ident { 159 | const TYPE: unreal_api::registry::ReflectType = unreal_api::registry::ReflectType::Composite; 160 | } 161 | impl unreal_api::ecs::component::Component for #struct_ident { 162 | type Storage = unreal_api::ecs::component::TableStorage; 163 | } 164 | pub struct #insert_struct_ident; 165 | #insert_component 166 | 167 | impl unreal_api::module::InsertReflectionStruct for #struct_ident { 168 | fn insert(registry: &mut unreal_api::module::ReflectionRegistry) { 169 | registry.reflect.insert( 170 | <#struct_ident as unreal_api::TypeUuid>::TYPE_UUID, 171 | Box::new(#reflect_struct_ident), 172 | ); 173 | #register_editor_component 174 | 175 | } 176 | } 177 | } 178 | } else { 179 | panic!("Only structs are currently supported in `unreal_api_derive`") 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /unreal-api-derive/src/type_uuid.rs: -------------------------------------------------------------------------------- 1 | // This code was copied and modified from https://github.com/bevyengine/bevy/blob/f000c2b951f4c519416ffda70281b2284d37f9f8/crates/bevy_reflect/bevy_reflect_derive/src/type_uuid.rs 2 | 3 | extern crate proc_macro; 4 | 5 | use quote::{quote, ToTokens}; 6 | use syn::*; 7 | use uuid::Uuid; 8 | 9 | pub fn type_uuid_derive(ast: &DeriveInput) -> proc_macro2::TokenStream { 10 | // Build the trait implementation 11 | let name = &ast.ident; 12 | 13 | let (impl_generics, type_generics, _) = &ast.generics.split_for_impl(); 14 | if !impl_generics.to_token_stream().is_empty() || !type_generics.to_token_stream().is_empty() { 15 | panic!("#[derive(TypeUuid)] is not supported for generics."); 16 | } 17 | 18 | let mut uuid = None; 19 | for attribute in ast.attrs.iter().filter_map(|attr| attr.parse_meta().ok()) { 20 | let name_value = if let Meta::NameValue(name_value) = attribute { 21 | name_value 22 | } else { 23 | continue; 24 | }; 25 | 26 | if name_value 27 | .path 28 | .get_ident() 29 | .map(|i| i != "uuid") 30 | .unwrap_or(true) 31 | { 32 | continue; 33 | } 34 | 35 | let uuid_str = match name_value.lit { 36 | Lit::Str(lit_str) => lit_str, 37 | _ => panic!("`uuid` attribute must take the form `#[uuid = \"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\"`."), 38 | }; 39 | 40 | uuid = Some( 41 | Uuid::parse_str(&uuid_str.value()) 42 | .expect("Value specified to `#[uuid]` attribute is not a valid UUID."), 43 | ); 44 | } 45 | 46 | let uuid = 47 | uuid.expect("No `#[uuid = \"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\"` attribute found."); 48 | let bytes = uuid 49 | .as_bytes() 50 | .iter() 51 | .map(|byte| format!("{:#X}", byte)) 52 | .map(|byte_str| syn::parse_str::(&byte_str).unwrap()); 53 | 54 | quote! { 55 | impl unreal_api::TypeUuid for #name { 56 | const TYPE_UUID: unreal_api::uuid::Uuid = unreal_api::uuid::Uuid::from_bytes([ 57 | #( #bytes ),* 58 | ]); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /unreal-api/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | .vs 4 | .vscode 5 | Content 6 | DerivedDataCache 7 | Intermediate 8 | Saved 9 | Script 10 | Plugins/RustPlugin/Intermediate 11 | rusttemp 12 | *.pdb 13 | *.exp 14 | *.lib 15 | -------------------------------------------------------------------------------- /unreal-api/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "unreal-api" 3 | version = "0.1.0" 4 | edition = "2021" 5 | license = "MIT OR Apache-2.0" 6 | 7 | [dependencies] 8 | bevy_ecs = "0.8" 9 | glam = "0.21" 10 | log = { version = "0.4.14", features =["std"] } 11 | unreal-api-derive= { path = "../unreal-api-derive" } 12 | unreal-reflect= { path = "../unreal-reflect" } 13 | unreal-ffi= { path = "../unreal-ffi" } 14 | -------------------------------------------------------------------------------- /unreal-api/src/api.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::ffi; 4 | use glam::{Quat, Vec3}; 5 | 6 | use crate::core::ActorPtr; 7 | use crate::ecs::entity::Entity; 8 | use crate::module::bindings; 9 | use crate::physics::CollisionShape; 10 | 11 | #[derive(Default)] 12 | pub struct UnrealApi { 13 | // TODO: Implement unregister. 14 | pub actor_to_entity: HashMap, 15 | pub entity_to_actor: HashMap, 16 | } 17 | 18 | #[derive(Default)] 19 | pub struct SweepParams { 20 | pub ignored_entities: Vec, 21 | } 22 | 23 | impl SweepParams { 24 | pub fn add_ignored_entity(mut self, entity: Entity) -> Self { 25 | self.ignored_entities.push(entity); 26 | self 27 | } 28 | } 29 | 30 | #[derive(Debug)] 31 | pub struct SweepHit { 32 | /// The entity that was hit 33 | pub entity: Entity, 34 | /// Location in world space of the actual contact of the trace shape (box, sphere, ray, etc) with the impacted object. 35 | pub impact_location: Vec3, 36 | /// Normal of the hit in world space, for the object that was hit by the sweep, if any 37 | pub impact_normal: Vec3, 38 | /// If this test started in penetration (bStartPenetrating is true) and a depenetration vector can be computed, this value is the distance along Normal that will result in moving out of penetration. 39 | pub penetration_depth: f32, 40 | /// The location in world space where the moving shape would end up against the impacted object, if there is a hit. 41 | pub location: Vec3, 42 | /// Normal of the hit in world space, for the object that was swept. 43 | pub normal: Vec3, 44 | pub start_in_penentration: bool, 45 | } 46 | 47 | #[derive(Default)] 48 | pub struct LineTraceParams { 49 | pub ignored_entities: Vec, 50 | } 51 | 52 | impl LineTraceParams { 53 | pub fn add_ignored_entity(mut self, entity: Entity) -> Self { 54 | self.ignored_entities.push(entity); 55 | self 56 | } 57 | } 58 | 59 | #[derive(Debug)] 60 | pub struct LineTraceHit { 61 | /// The entity that was hit 62 | pub entity: Entity, 63 | pub location: Vec3, 64 | pub normal: Vec3, 65 | } 66 | 67 | impl UnrealApi { 68 | pub fn register_actor(&mut self, actor: ActorPtr, entity: Entity) { 69 | self.actor_to_entity.insert(actor, entity); 70 | self.entity_to_actor.insert(entity, actor); 71 | } 72 | pub fn sweep( 73 | &self, 74 | start: Vec3, 75 | end: Vec3, 76 | rotation: Quat, 77 | collision_shape: CollisionShape, 78 | params: SweepParams, 79 | ) -> Option { 80 | let ignored_actors: Vec<_> = params 81 | .ignored_entities 82 | .iter() 83 | .filter_map(|entity| self.entity_to_actor.get(entity)) 84 | .map(|actor| actor.0) 85 | .collect(); 86 | let params = ffi::LineTraceParams { 87 | ignored_actors: ignored_actors.as_ptr(), 88 | ignored_actors_len: ignored_actors.len(), 89 | }; 90 | let mut hit = ffi::HitResult::default(); 91 | unsafe { 92 | if (bindings().physics_fns.sweep)( 93 | start.into(), 94 | end.into(), 95 | rotation.into(), 96 | params, 97 | collision_shape.into(), 98 | &mut hit, 99 | ) == 1 100 | { 101 | let entity = self 102 | .actor_to_entity 103 | .get(&ActorPtr(hit.actor)) 104 | .copied() 105 | .expect("We hit an unknown actor. Please create an issue."); 106 | 107 | Some(SweepHit { 108 | entity, 109 | impact_location: hit.impact_location.into(), 110 | location: hit.location.into(), 111 | normal: hit.normal.into(), 112 | penetration_depth: hit.pentration_depth, 113 | start_in_penentration: hit.start_penetrating == 1, 114 | impact_normal: hit.impact_normal.into(), 115 | }) 116 | } else { 117 | None 118 | } 119 | } 120 | } 121 | 122 | pub fn line_trace( 123 | &self, 124 | start: Vec3, 125 | end: Vec3, 126 | params: LineTraceParams, 127 | ) -> Option { 128 | let ignored_actors: Vec<_> = params 129 | .ignored_entities 130 | .iter() 131 | .filter_map(|entity| self.entity_to_actor.get(entity)) 132 | .map(|actor| actor.0) 133 | .collect(); 134 | let params = ffi::LineTraceParams { 135 | ignored_actors: ignored_actors.as_ptr(), 136 | ignored_actors_len: ignored_actors.len(), 137 | }; 138 | let mut hit = ffi::HitResult::default(); 139 | unsafe { 140 | if (bindings().physics_fns.line_trace)(start.into(), end.into(), params, &mut hit) == 1 141 | { 142 | let entity = self 143 | .actor_to_entity 144 | .get(&ActorPtr(hit.actor)) 145 | .copied() 146 | .expect("We hit an unknown actor. Please create an issue."); 147 | Some(LineTraceHit { 148 | entity, 149 | location: hit.location.into(), 150 | normal: hit.normal.into(), 151 | }) 152 | } else { 153 | None 154 | } 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /unreal-api/src/editor_component.rs: -------------------------------------------------------------------------------- 1 | use bevy_ecs::system::EntityCommands; 2 | use glam::{Quat, Vec3}; 3 | use unreal_ffi as ffi; 4 | use unreal_reflect::{ 5 | registry::{UClass, USound}, 6 | Uuid, 7 | }; 8 | 9 | use crate::{core::to_ffi_uuid, module::bindings}; 10 | 11 | pub trait InsertEditorComponent { 12 | /// # Safety 13 | unsafe fn insert_component( 14 | &self, 15 | actor: *const ffi::AActorOpaque, 16 | uuid: Uuid, 17 | commands: &mut EntityCommands<'_, '_, '_>, 18 | ); 19 | } 20 | 21 | pub trait GetEditorComponentValue: Sized { 22 | unsafe fn get(actor: *const ffi::AActorOpaque, uuid: Uuid, field: &'static str) 23 | -> Option; 24 | } 25 | 26 | impl GetEditorComponentValue for Vec3 { 27 | unsafe fn get( 28 | actor: *const ffi::AActorOpaque, 29 | uuid: Uuid, 30 | field: &'static str, 31 | ) -> Option { 32 | let mut data = ffi::Vector3::default(); 33 | let code = (bindings().editor_component_fns.get_editor_component_vector)( 34 | actor, 35 | to_ffi_uuid(uuid), 36 | ffi::Utf8Str::from(field), 37 | &mut data, 38 | ); 39 | if code == 1 { 40 | Some(data.into()) 41 | } else { 42 | None 43 | } 44 | } 45 | } 46 | impl GetEditorComponentValue for Quat { 47 | unsafe fn get( 48 | actor: *const ffi::AActorOpaque, 49 | uuid: Uuid, 50 | field: &'static str, 51 | ) -> Option { 52 | let mut data = ffi::Quaternion::default(); 53 | let code = (bindings().editor_component_fns.get_editor_component_quat)( 54 | actor, 55 | to_ffi_uuid(uuid), 56 | ffi::Utf8Str::from(field), 57 | &mut data, 58 | ); 59 | if code == 1 { 60 | Some(data.into()) 61 | } else { 62 | None 63 | } 64 | } 65 | } 66 | 67 | impl GetEditorComponentValue for f32 { 68 | unsafe fn get( 69 | actor: *const ffi::AActorOpaque, 70 | uuid: Uuid, 71 | field: &'static str, 72 | ) -> Option { 73 | let mut data = 0.0f32; 74 | let code = (bindings().editor_component_fns.get_editor_component_float)( 75 | actor, 76 | to_ffi_uuid(uuid), 77 | ffi::Utf8Str::from(field), 78 | &mut data, 79 | ); 80 | if code == 1 { 81 | Some(data) 82 | } else { 83 | None 84 | } 85 | } 86 | } 87 | 88 | impl GetEditorComponentValue for bool { 89 | unsafe fn get( 90 | actor: *const ffi::AActorOpaque, 91 | uuid: Uuid, 92 | field: &'static str, 93 | ) -> Option { 94 | let mut data: u32 = 0; 95 | let code = (bindings().editor_component_fns.get_editor_component_bool)( 96 | actor, 97 | to_ffi_uuid(uuid), 98 | ffi::Utf8Str::from(field), 99 | &mut data, 100 | ); 101 | if code == 1 { 102 | Some(data == 1) 103 | } else { 104 | None 105 | } 106 | } 107 | } 108 | impl GetEditorComponentValue for UClass { 109 | unsafe fn get( 110 | actor: *const ffi::AActorOpaque, 111 | uuid: Uuid, 112 | field: &'static str, 113 | ) -> Option { 114 | let mut data: *mut ffi::UObjectOpague = std::ptr::null_mut(); 115 | let code = (bindings().editor_component_fns.get_editor_component_uobject)( 116 | actor, 117 | to_ffi_uuid(uuid), 118 | ffi::Utf8Str::from(field), 119 | ffi::UObjectType::UClass, 120 | &mut data, 121 | ); 122 | if code == 1 { 123 | Some(UClass { ptr: data }) 124 | } else { 125 | None 126 | } 127 | } 128 | } 129 | 130 | impl GetEditorComponentValue for USound { 131 | unsafe fn get( 132 | actor: *const ffi::AActorOpaque, 133 | uuid: Uuid, 134 | field: &'static str, 135 | ) -> Option { 136 | let mut data: *mut ffi::UObjectOpague = std::ptr::null_mut(); 137 | let code = (bindings().editor_component_fns.get_editor_component_uobject)( 138 | actor, 139 | to_ffi_uuid(uuid), 140 | ffi::Utf8Str::from(field), 141 | ffi::UObjectType::UClass, 142 | &mut data, 143 | ); 144 | if code == 1 { 145 | Some(USound { ptr: data }) 146 | } else { 147 | None 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /unreal-api/src/input.rs: -------------------------------------------------------------------------------- 1 | use unreal_ffi::ActionState; 2 | 3 | use crate::module::bindings; 4 | use std::{collections::HashMap, os::raw::c_char}; 5 | 6 | pub enum Action { 7 | Pressed, 8 | Released, 9 | } 10 | 11 | pub type Binding = &'static str; 12 | #[derive(Default)] 13 | pub struct Input { 14 | axis: HashMap, 15 | action: HashMap, 16 | 17 | action_bindings: Vec, 18 | axis_bindings: Vec, 19 | } 20 | impl Input { 21 | pub fn register_action_binding(&mut self, binding: Binding) { 22 | self.action_bindings.push(binding); 23 | } 24 | pub fn register_axis_binding(&mut self, binding: Binding) { 25 | self.axis_bindings.push(binding); 26 | } 27 | 28 | pub fn update(&mut self) { 29 | self.axis.clear(); 30 | self.action.clear(); 31 | 32 | for binding in &self.action_bindings { 33 | let check_state = |state: ActionState| -> bool { 34 | let mut out = 0; 35 | unsafe { 36 | (bindings().get_action_state)( 37 | binding.as_ptr() as *const c_char, 38 | binding.len(), 39 | state, 40 | &mut out, 41 | ); 42 | } 43 | out == 1 44 | }; 45 | 46 | if check_state(ActionState::Pressed) { 47 | self.action.insert(binding, Action::Pressed); 48 | } 49 | if check_state(ActionState::Released) { 50 | self.action.insert(binding, Action::Released); 51 | } 52 | } 53 | for binding in &self.axis_bindings { 54 | let mut value: f32 = 0.0; 55 | unsafe { 56 | (bindings().get_axis_value)( 57 | binding.as_ptr() as *const c_char, 58 | binding.len(), 59 | &mut value, 60 | ); 61 | } 62 | self.axis.insert(binding, value); 63 | } 64 | } 65 | 66 | pub fn get_axis_value(&self, binding: Binding) -> Option { 67 | self.axis.get(&binding).copied() 68 | } 69 | 70 | pub fn is_action_pressed(&self, binding: Binding) -> bool { 71 | self.action 72 | .get(binding) 73 | .map_or(false, |state| matches!(state, Action::Pressed)) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /unreal-api/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::missing_safety_doc)] 2 | extern crate self as unreal_api; 3 | 4 | pub mod api; 5 | pub use unreal_ffi as ffi; 6 | pub mod core; 7 | pub mod editor_component; 8 | pub mod input; 9 | pub mod log; 10 | pub mod module; 11 | pub mod physics; 12 | pub mod plugin; 13 | pub mod sound; 14 | pub use unreal_api_derive::Component; 15 | 16 | // TODO: Here for the unreal_api_derive macro. Lets restructure this 17 | pub use bevy_ecs as ecs; 18 | pub use glam as math; 19 | pub use unreal_reflect::*; 20 | 21 | pub use uuid; 22 | 23 | pub fn iterate_actors(bindings: &ffi::UnrealBindings) -> Vec<*mut ffi::AActorOpaque> { 24 | unsafe { 25 | let mut v: Vec<*mut ffi::AActorOpaque> = Vec::with_capacity(200); 26 | 27 | let mut len = 200; 28 | (bindings.iterate_actors)(v.as_mut_ptr(), &mut len); 29 | v.set_len(len as usize); 30 | v 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /unreal-api/src/log.rs: -------------------------------------------------------------------------------- 1 | use glam::{Quat, Vec3}; 2 | use log::{set_boxed_logger, set_max_level, LevelFilter, Metadata, Record, SetLoggerError}; 3 | use unreal_ffi as ffi; 4 | use unreal_ffi::Color; 5 | 6 | use crate::{core::ActorPtr, module::bindings, physics::CollisionShape}; 7 | struct UnrealLogger; 8 | 9 | impl log::Log for UnrealLogger { 10 | fn enabled(&self, _metadata: &Metadata) -> bool { 11 | // TODO 12 | true 13 | } 14 | 15 | fn log(&self, record: &Record) { 16 | if self.enabled(record.metadata()) { 17 | let text = record.args().to_string(); 18 | (crate::module::bindings().log)(text.as_ptr() as *const _, text.len() as i32); 19 | } 20 | } 21 | 22 | fn flush(&self) {} 23 | } 24 | 25 | pub fn init() -> Result<(), SetLoggerError> { 26 | set_boxed_logger(Box::new(UnrealLogger)).map(|()| set_max_level(LevelFilter::Info)) 27 | } 28 | 29 | pub fn visual_log_capsule( 30 | category: LogCategory, 31 | actor: ActorPtr, 32 | position: Vec3, 33 | rotation: Quat, 34 | half_height: f32, 35 | radius: f32, 36 | color: Color, 37 | ) { 38 | unsafe { 39 | (bindings().visual_log_capsule)( 40 | ffi::Utf8Str::from(category.name), 41 | actor.0, 42 | position.into(), 43 | rotation.into(), 44 | half_height, 45 | radius, 46 | color, 47 | ); 48 | } 49 | } 50 | 51 | pub fn visual_log_shape( 52 | category: LogCategory, 53 | actor: ActorPtr, 54 | position: Vec3, 55 | rotation: Quat, 56 | shape: CollisionShape, 57 | color: Color, 58 | ) { 59 | match shape { 60 | CollisionShape::Capsule { 61 | half_height, 62 | radius, 63 | } => unsafe { 64 | (bindings().visual_log_capsule)( 65 | ffi::Utf8Str::from(category.name), 66 | actor.0, 67 | position.into(), 68 | rotation.into(), 69 | half_height, 70 | radius, 71 | color, 72 | ); 73 | }, 74 | CollisionShape::Box { half_extent: _ } => { 75 | unimplemented!() 76 | } 77 | CollisionShape::Sphere { radius } => unsafe { 78 | (bindings().visual_log_location)( 79 | ffi::Utf8Str::from(category.name), 80 | actor.0, 81 | position.into(), 82 | radius, 83 | color, 84 | ); 85 | }, 86 | } 87 | } 88 | 89 | pub fn visual_log_location( 90 | category: LogCategory, 91 | actor: ActorPtr, 92 | position: Vec3, 93 | radius: f32, 94 | color: Color, 95 | ) { 96 | unsafe { 97 | (bindings().visual_log_location)( 98 | ffi::Utf8Str::from(category.name), 99 | actor.0, 100 | position.into(), 101 | radius, 102 | color, 103 | ); 104 | } 105 | } 106 | 107 | #[derive(Copy, Clone)] 108 | pub struct LogCategory { 109 | pub name: &'static str, 110 | } 111 | 112 | impl LogCategory { 113 | pub const fn new(name: &'static str) -> Self { 114 | Self { name } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /unreal-api/src/module.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, HashSet}; 2 | 3 | use bevy_ecs::{ 4 | event::Event, 5 | prelude::{Events, System}, 6 | schedule::{Schedule, StageLabel, SystemSet, SystemStage}, 7 | system::Resource, 8 | }; 9 | use unreal_reflect::{registry::ReflectDyn, uuid, TypeUuid, World}; 10 | 11 | use crate::{ 12 | core::{CoreStage, StartupStage, UnrealCore}, 13 | editor_component::InsertEditorComponent, 14 | ffi::UnrealBindings, 15 | plugin::Plugin, 16 | }; 17 | 18 | pub static mut MODULE: Option = None; 19 | pub struct Global { 20 | pub core: UnrealCore, 21 | pub module: Box, 22 | } 23 | 24 | pub trait InitUserModule { 25 | fn initialize() -> Self; 26 | } 27 | 28 | pub type EmptySystem = &'static dyn System; 29 | #[macro_export] 30 | macro_rules! register_components { 31 | ($($ty: ty,)* => $module: expr) => { 32 | $( 33 | $module.register_component::<$ty>(); 34 | )* 35 | }; 36 | } 37 | pub trait InsertReflectionStruct { 38 | fn insert(registry: &mut ReflectionRegistry); 39 | } 40 | 41 | #[derive(Default)] 42 | pub struct ReflectionRegistry { 43 | pub uuid_set: HashSet, 44 | pub reflect: HashMap>, 45 | pub insert_editor_component: HashMap>, 46 | } 47 | 48 | impl ReflectionRegistry { 49 | pub fn register(&mut self) 50 | where 51 | T: InsertReflectionStruct + TypeUuid + 'static, 52 | { 53 | if self.uuid_set.contains(&T::TYPE_UUID) { 54 | panic!( 55 | "Duplicated UUID {} for {}", 56 | T::TYPE_UUID, 57 | std::any::type_name::() 58 | ); 59 | } 60 | T::insert(self); 61 | self.uuid_set.insert(T::TYPE_UUID); 62 | } 63 | } 64 | 65 | pub struct Module { 66 | pub(crate) schedule: Schedule, 67 | pub(crate) startup: Schedule, 68 | pub(crate) reflection_registry: ReflectionRegistry, 69 | pub(crate) world: World, 70 | } 71 | 72 | impl Module { 73 | pub fn new() -> Self { 74 | let mut startup = Schedule::default(); 75 | startup.add_stage(StartupStage, SystemStage::single_threaded()); 76 | 77 | Self { 78 | schedule: Schedule::default(), 79 | startup, 80 | reflection_registry: ReflectionRegistry::default(), 81 | world: World::new(), 82 | } 83 | } 84 | pub fn insert_resource(&mut self, resource: impl Resource) -> &mut Self { 85 | self.world.insert_resource(resource); 86 | self 87 | } 88 | 89 | pub fn add_stage(&mut self, label: impl StageLabel) -> &mut Self { 90 | self.schedule 91 | .add_stage(label, SystemStage::single_threaded()); 92 | self 93 | } 94 | 95 | pub fn add_stage_after( 96 | &mut self, 97 | label: impl StageLabel, 98 | insert: impl StageLabel, 99 | ) -> &mut Self { 100 | self.schedule 101 | .add_stage_after(label, insert, SystemStage::single_threaded()); 102 | self 103 | } 104 | 105 | pub fn add_stage_before( 106 | &mut self, 107 | label: impl StageLabel, 108 | insert: impl StageLabel, 109 | ) -> &mut Self { 110 | self.schedule 111 | .add_stage_before(label, insert, SystemStage::single_threaded()); 112 | self 113 | } 114 | 115 | pub fn add_system_set_to_stage(&mut self, label: impl StageLabel, set: SystemSet) -> &mut Self { 116 | self.schedule.add_system_set_to_stage(label, set); 117 | self 118 | } 119 | 120 | pub fn register_component(&mut self) 121 | where 122 | T: InsertReflectionStruct + TypeUuid + 'static, 123 | { 124 | self.reflection_registry.register::(); 125 | } 126 | 127 | pub fn add_plugin(&mut self, plugin: P) -> &mut Self { 128 | plugin.build(self); 129 | self 130 | } 131 | 132 | pub fn add_startup_system_set(&mut self, system_set: SystemSet) -> &mut Self { 133 | self.startup 134 | .add_system_set_to_stage(StartupStage, system_set); 135 | self 136 | } 137 | 138 | pub fn add_event(&mut self) -> &mut Self { 139 | self.world.init_resource::>(); 140 | self.add_system_set_to_stage( 141 | CoreStage::RegisterEvent, 142 | SystemSet::new().with_system(Events::::update_system), 143 | ); 144 | self 145 | } 146 | } 147 | 148 | impl Default for Module { 149 | fn default() -> Self { 150 | Self::new() 151 | } 152 | } 153 | 154 | pub trait UserModule { 155 | fn initialize(&self, module: &mut Module); 156 | } 157 | pub static mut BINDINGS: Option = None; 158 | 159 | #[macro_export] 160 | macro_rules! implement_unreal_module { 161 | ($module: ty) => { 162 | #[no_mangle] 163 | pub unsafe extern "C" fn register_unreal_bindings( 164 | bindings: $crate::ffi::UnrealBindings, 165 | rust_bindings: *mut $crate::ffi::RustBindings, 166 | ) -> u32 { 167 | std::panic::set_hook(Box::new(|panic_info| { 168 | let info = panic_info 169 | .payload() 170 | .downcast_ref::<&'static str>() 171 | .copied() 172 | .or(panic_info 173 | .payload() 174 | .downcast_ref::() 175 | .map(String::as_str)); 176 | 177 | if let Some(s) = info { 178 | let location = panic_info.location().map_or("".to_string(), |loc| { 179 | format!("{}, at line {}", loc.file(), loc.line()) 180 | }); 181 | log::error!("Panic: {} => {}", location, s); 182 | } else { 183 | log::error!("panic occurred"); 184 | } 185 | })); 186 | $crate::module::BINDINGS = Some(bindings); 187 | let _ = $crate::log::init(); 188 | 189 | let r = std::panic::catch_unwind(|| unsafe { 190 | let module = Box::new(<$module as $crate::module::InitUserModule>::initialize()); 191 | let core = $crate::core::UnrealCore::new(module.as_ref()); 192 | 193 | $crate::module::MODULE = Some($crate::module::Global { core, module }); 194 | $crate::ffi::RustBindings { 195 | retrieve_uuids: $crate::core::retrieve_uuids, 196 | tick: $crate::core::tick, 197 | begin_play: $crate::core::begin_play, 198 | unreal_event: $crate::core::unreal_event, 199 | reflection_fns: $crate::core::create_reflection_fns(), 200 | allocate_fns: $crate::core::create_allocate_fns(), 201 | } 202 | }); 203 | match r { 204 | Ok(bindings) => { 205 | *rust_bindings = bindings; 206 | 1 207 | } 208 | Err(_) => 0, 209 | } 210 | } 211 | }; 212 | } 213 | 214 | pub fn bindings() -> &'static UnrealBindings { 215 | unsafe { BINDINGS.as_ref().unwrap() } 216 | } 217 | -------------------------------------------------------------------------------- /unreal-api/src/physics.rs: -------------------------------------------------------------------------------- 1 | use crate::Component; 2 | use ffi::AActorOpaque; 3 | use glam::{Quat, Vec3}; 4 | use unreal_ffi as ffi; 5 | 6 | use crate::{ 7 | core::{ActorPtr, Primitive, UnrealPtr}, 8 | module::bindings, 9 | }; 10 | 11 | #[derive(Debug)] 12 | pub struct SweepResult { 13 | pub actor: Option, 14 | pub impact_location: Vec3, 15 | pub location: Vec3, 16 | pub penetration_depth: f32, 17 | pub normal: Vec3, 18 | pub impact_normal: Vec3, 19 | pub start_in_penentration: bool, 20 | } 21 | 22 | #[derive(Clone, Default)] 23 | pub struct SweepParams { 24 | pub ignored_actors: Vec<*mut AActorOpaque>, 25 | } 26 | 27 | impl SweepParams { 28 | pub fn add_ignored_actor(mut self, actor: ActorPtr) -> Self { 29 | self.ignored_actors.push(actor.0); 30 | self 31 | } 32 | } 33 | pub fn sweep_multi( 34 | start: Vec3, 35 | end: Vec3, 36 | rotation: Quat, 37 | collision_shape: CollisionShape, 38 | max_results: usize, 39 | params: SweepParams, 40 | ) -> Option> { 41 | let params = ffi::LineTraceParams { 42 | ignored_actors: params.ignored_actors.as_ptr(), 43 | ignored_actors_len: params.ignored_actors.len(), 44 | }; 45 | let mut hits: Vec = Vec::new(); 46 | hits.resize_with(max_results, Default::default); 47 | unsafe { 48 | let len = (bindings().physics_fns.sweep_multi)( 49 | start.into(), 50 | end.into(), 51 | rotation.into(), 52 | params, 53 | collision_shape.into(), 54 | max_results, 55 | hits.as_mut_ptr(), 56 | ); 57 | hits.truncate(len as usize); 58 | if len > 0 { 59 | Some( 60 | hits.into_iter() 61 | .map(|hit| { 62 | let actor = if hit.actor.is_null() { 63 | None 64 | } else { 65 | Some(ActorPtr(hit.actor)) 66 | }; 67 | SweepResult { 68 | actor, 69 | impact_location: hit.impact_location.into(), 70 | location: hit.location.into(), 71 | normal: hit.normal.into(), 72 | penetration_depth: hit.pentration_depth, 73 | start_in_penentration: hit.start_penetrating == 1, 74 | impact_normal: hit.impact_normal.into(), 75 | } 76 | }) 77 | .collect(), 78 | ) 79 | } else { 80 | None 81 | } 82 | } 83 | } 84 | #[derive(Copy, Clone)] 85 | pub enum CollisionShape { 86 | Capsule { half_height: f32, radius: f32 }, 87 | Box { half_extent: Vec3 }, 88 | Sphere { radius: f32 }, 89 | } 90 | 91 | impl CollisionShape { 92 | pub fn extent(self) -> Vec3 { 93 | match self { 94 | CollisionShape::Capsule { 95 | half_height, 96 | radius, 97 | } => Vec3::new(radius, radius, half_height), 98 | CollisionShape::Box { half_extent } => half_extent, 99 | CollisionShape::Sphere { radius } => Vec3::splat(radius), 100 | } 101 | } 102 | 103 | pub fn scale(self, amount: f32) -> Self { 104 | match self { 105 | CollisionShape::Capsule { 106 | half_height, 107 | radius, 108 | } => CollisionShape::Capsule { 109 | half_height: half_height * amount, 110 | radius: radius * amount, 111 | }, 112 | CollisionShape::Box { half_extent } => CollisionShape::Box { 113 | half_extent: half_extent * amount, 114 | }, 115 | CollisionShape::Sphere { radius } => CollisionShape::Sphere { 116 | radius: radius * amount, 117 | }, 118 | } 119 | } 120 | 121 | pub fn inflate(self, amount: f32) -> Self { 122 | match self { 123 | CollisionShape::Capsule { 124 | half_height, 125 | radius, 126 | } => CollisionShape::Capsule { 127 | half_height: half_height + amount, 128 | radius: radius + amount, 129 | }, 130 | CollisionShape::Box { half_extent } => CollisionShape::Box { 131 | half_extent: half_extent + amount, 132 | }, 133 | CollisionShape::Sphere { radius } => CollisionShape::Sphere { 134 | radius: radius + amount, 135 | }, 136 | } 137 | } 138 | } 139 | 140 | impl From for ffi::CollisionShape { 141 | fn from(val: CollisionShape) -> Self { 142 | match val { 143 | CollisionShape::Box { half_extent } => ffi::CollisionShape { 144 | ty: ffi::CollisionShapeType::Box, 145 | data: ffi::CollisionShapeUnion { 146 | collision_box: ffi::CollisionBox { 147 | half_extent_x: half_extent.x, 148 | half_extent_y: half_extent.y, 149 | half_extent_z: half_extent.z, 150 | }, 151 | }, 152 | }, 153 | CollisionShape::Capsule { 154 | half_height, 155 | radius, 156 | } => ffi::CollisionShape { 157 | data: ffi::CollisionShapeUnion { 158 | capsule: ffi::CollisionCapsule { 159 | radius, 160 | half_height, 161 | }, 162 | }, 163 | ty: ffi::CollisionShapeType::Capsule, 164 | }, 165 | CollisionShape::Sphere { radius } => ffi::CollisionShape { 166 | data: ffi::CollisionShapeUnion { 167 | sphere: ffi::CollisionSphere { radius }, 168 | }, 169 | ty: ffi::CollisionShapeType::Sphere, 170 | }, 171 | } 172 | } 173 | } 174 | 175 | #[derive(Default, Component)] 176 | #[uuid = "ffc10b5c-635c-43ce-8288-e3c6f6d67e36"] 177 | pub struct PhysicsComponent { 178 | #[reflect(skip)] 179 | pub ptr: UnrealPtr, 180 | pub is_simulating: bool, 181 | pub velocity: Vec3, 182 | } 183 | 184 | impl PhysicsComponent { 185 | pub fn new(ptr: UnrealPtr) -> Self { 186 | let mut p = Self { 187 | ptr, 188 | ..Default::default() 189 | }; 190 | p.download_state(); 191 | p 192 | } 193 | 194 | pub fn get_collision_shape(&self) -> CollisionShape { 195 | unsafe { 196 | let mut shape = ffi::CollisionShape::default(); 197 | assert!((bindings().physics_fns.get_collision_shape)(self.ptr.ptr, &mut shape) == 1); 198 | match shape.ty { 199 | ffi::CollisionShapeType::Capsule => CollisionShape::Capsule { 200 | half_height: shape.data.capsule.half_height, 201 | radius: shape.data.capsule.radius, 202 | }, 203 | ffi::CollisionShapeType::Box => CollisionShape::Box { 204 | half_extent: Vec3::new( 205 | shape.data.collision_box.half_extent_x, 206 | shape.data.collision_box.half_extent_y, 207 | shape.data.collision_box.half_extent_y, 208 | ), 209 | }, 210 | ffi::CollisionShapeType::Sphere => CollisionShape::Sphere { 211 | radius: shape.data.sphere.radius, 212 | }, 213 | } 214 | } 215 | } 216 | pub fn download_state(&mut self) { 217 | unsafe { 218 | self.is_simulating = (bindings().physics_fns.is_simulating)(self.ptr.ptr) == 1; 219 | self.velocity = (bindings().physics_fns.get_velocity)(self.ptr.ptr).into(); 220 | } 221 | } 222 | 223 | pub fn upload_state(&mut self) { 224 | unsafe { 225 | (bindings().physics_fns.set_velocity)(self.ptr.ptr, self.velocity.into()); 226 | } 227 | } 228 | 229 | pub fn add_impulse(&mut self, impulse: Vec3) { 230 | unsafe { 231 | (bindings().physics_fns.add_impulse)(self.ptr.ptr, impulse.into()); 232 | } 233 | } 234 | 235 | pub fn add_force(&mut self, force: Vec3) { 236 | unsafe { 237 | (bindings().physics_fns.add_force)(self.ptr.ptr, force.into()); 238 | } 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /unreal-api/src/plugin.rs: -------------------------------------------------------------------------------- 1 | use crate::module::Module; 2 | 3 | pub trait Plugin { 4 | fn build(&self, module: &mut Module); 5 | } 6 | -------------------------------------------------------------------------------- /unreal-api/src/sound.rs: -------------------------------------------------------------------------------- 1 | use glam::{Quat, Vec3}; 2 | use unreal_reflect::registry::USound; 3 | 4 | pub use crate::ffi::SoundSettings; 5 | use crate::module::bindings; 6 | 7 | pub fn play_sound_at_location( 8 | sound: USound, 9 | location: Vec3, 10 | rotation: Quat, 11 | settings: &SoundSettings, 12 | ) { 13 | unsafe { 14 | (bindings().sound_fns.play_sound_at_location)( 15 | sound.ptr, 16 | location.into(), 17 | rotation.into(), 18 | settings, 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /unreal-api/wrapper.h: -------------------------------------------------------------------------------- 1 | #include "../RustPlugin/Source/RustPlugin/Public/Bindings.h" -------------------------------------------------------------------------------- /unreal-ffi/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "unreal-ffi" 3 | version = "0.1.0" 4 | edition = "2021" 5 | license = "MIT OR Apache-2.0" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | bevy_ecs = "0.8" 11 | glam = "0.21" 12 | 13 | [build-dependencies] 14 | cbindgen = "0.24" 15 | -------------------------------------------------------------------------------- /unreal-ffi/build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | fn main() { 4 | let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); 5 | 6 | cbindgen::Builder::new() 7 | .with_crate(crate_dir) 8 | // TODO: This is stupid and easy to forgot to add things here. Can we auto detect those? 9 | .include_item("UnrealBindings") 10 | .include_item("RustBindings") 11 | .include_item("CreateRustBindings") 12 | .include_item("EntryUnrealBindingsFn") 13 | .include_item("EntryBeginPlayFn") 14 | .include_item("EntryTickFn") 15 | .include_item("RetrieveUuids") 16 | .include_item("ActorSpawnedEvent") 17 | .include_item("ActorBeginOverlap") 18 | .include_item("ActorEndOverlap") 19 | .include_item("ActorHitEvent") 20 | .include_item("ActorDestroyEvent") 21 | .with_pragma_once(true) 22 | //.with_config(Config { 23 | // structure: StructConfig { 24 | // derive_constructor: true, 25 | // ..Default::default() 26 | // }, 27 | // ..Default::default() 28 | //}) 29 | .generate() 30 | .expect("Unable to generate bindings") 31 | .write_to_file("../RustPlugin/Source/RustPlugin/Public/Bindings.h"); 32 | } 33 | -------------------------------------------------------------------------------- /unreal-ffi/src/actor.rs: -------------------------------------------------------------------------------- 1 | use std::os::raw::c_char; 2 | 3 | use crate::{ 4 | AActorOpaque, ActorComponentPtr, Entity, Quaternion, RustAlloc, UClassOpague, Vector3, 5 | }; 6 | 7 | pub type GetSpatialDataFn = extern "C" fn( 8 | actor: *const AActorOpaque, 9 | position: &mut Vector3, 10 | rotation: &mut Quaternion, 11 | scale: &mut Vector3, 12 | ); 13 | 14 | pub type LogFn = extern "C" fn(*const c_char, i32); 15 | 16 | pub type SetSpatialDataFn = extern "C" fn( 17 | actor: *mut AActorOpaque, 18 | position: Vector3, 19 | rotation: Quaternion, 20 | scale: Vector3, 21 | ); 22 | 23 | pub type SetEntityForActorFn = unsafe extern "C" fn(name: *mut AActorOpaque, entity: Entity); 24 | 25 | pub type GetActorComponentsFn = 26 | unsafe extern "C" fn(actor: *const AActorOpaque, data: *mut ActorComponentPtr, len: &mut usize); 27 | 28 | pub type GetRootComponentFn = 29 | unsafe extern "C" fn(actor: *const AActorOpaque, data: *mut ActorComponentPtr); 30 | 31 | pub type GetRegisteredClassesFn = 32 | unsafe extern "C" fn(classes: *mut *mut UClassOpague, len: *mut usize); 33 | 34 | pub type GetClassFn = unsafe extern "C" fn(actor: *const AActorOpaque) -> *mut UClassOpague; 35 | 36 | pub type IsMoveableFn = unsafe extern "C" fn(actor: *const AActorOpaque) -> u32; 37 | 38 | pub type GetActorNameFn = unsafe extern "C" fn(actor: *const AActorOpaque, data: *mut RustAlloc); 39 | 40 | pub type SetOwnerFn = 41 | unsafe extern "C" fn(actor: *mut AActorOpaque, new_owner: *const AActorOpaque); 42 | 43 | pub type RegisterActorOnOverlapFn = unsafe extern "C" fn(actor: *mut AActorOpaque); 44 | pub type RegisterActorOnHitFn = unsafe extern "C" fn(actor: *mut AActorOpaque); 45 | 46 | pub type SetViewTargetFn = unsafe extern "C" fn(actor: *const AActorOpaque); 47 | 48 | pub type DestroyActorFn = unsafe extern "C" fn(actor: *const AActorOpaque); 49 | 50 | extern "C" { 51 | pub fn RegisterActorOnHit(actor: *mut AActorOpaque); 52 | pub fn RegisterActorOnOverlap(actor: *mut AActorOpaque); 53 | 54 | pub fn SetOwner(actor: *mut AActorOpaque, new_owner: *const AActorOpaque); 55 | 56 | pub fn SetSpatialData( 57 | actor: *mut AActorOpaque, 58 | position: Vector3, 59 | rotation: Quaternion, 60 | scale: Vector3, 61 | ); 62 | 63 | pub fn GetSpatialData( 64 | actor: *const AActorOpaque, 65 | position: &mut Vector3, 66 | rotation: &mut Quaternion, 67 | scale: &mut Vector3, 68 | ); 69 | pub fn SetEntityForActor(name: *mut AActorOpaque, entity: Entity); 70 | 71 | pub fn GetActorComponents( 72 | actor: *const AActorOpaque, 73 | data: *mut ActorComponentPtr, 74 | len: &mut usize, 75 | ); 76 | 77 | pub fn GetRootComponent(actor: *const AActorOpaque, data: *mut ActorComponentPtr); 78 | 79 | pub fn GetRegisteredClasses(classes: *mut *mut UClassOpague, len: *mut usize); 80 | 81 | pub fn GetClass(actor: *const AActorOpaque) -> *mut UClassOpague; 82 | 83 | pub fn IsMoveable(actor: *const AActorOpaque) -> u32; 84 | 85 | pub fn GetActorName(actor: *const AActorOpaque, data: *mut RustAlloc); 86 | 87 | pub fn DestroyActor(actor: *const AActorOpaque); 88 | 89 | pub fn SetViewTarget(actor: *const AActorOpaque); 90 | } 91 | 92 | #[repr(C)] 93 | pub struct ActorFns { 94 | pub get_spatial_data: GetSpatialDataFn, 95 | pub set_spatial_data: SetSpatialDataFn, 96 | pub set_entity_for_actor: SetEntityForActorFn, 97 | pub get_actor_components: GetActorComponentsFn, 98 | pub register_actor_on_overlap: RegisterActorOnOverlapFn, 99 | pub register_actor_on_hit: RegisterActorOnHitFn, 100 | pub get_root_component: GetRootComponentFn, 101 | pub get_registered_classes: GetRegisteredClassesFn, 102 | pub get_class: GetClassFn, 103 | pub set_view_target: SetViewTargetFn, 104 | pub get_actor_name: GetActorNameFn, 105 | pub set_owner: SetOwnerFn, 106 | pub is_moveable: IsMoveableFn, 107 | pub destroy_actor: DestroyActorFn, 108 | } 109 | -------------------------------------------------------------------------------- /unreal-ffi/src/lib.rs: -------------------------------------------------------------------------------- 1 | use glam::{Quat, Vec3}; 2 | use std::{ffi::c_void, os::raw::c_char}; 3 | pub mod actor; 4 | pub mod physics; 5 | pub mod sound; 6 | 7 | pub use actor::*; 8 | pub use physics::*; 9 | pub use sound::*; 10 | 11 | #[repr(u8)] 12 | #[derive(Debug)] 13 | pub enum ResultCode { 14 | Success = 0, 15 | Panic = 1, 16 | } 17 | #[repr(u32)] 18 | #[derive(Copy, Clone)] 19 | pub enum UObjectType { 20 | UClass, 21 | } 22 | 23 | #[repr(C)] 24 | pub struct Utf8Str { 25 | pub ptr: *const c_char, 26 | pub len: usize, 27 | } 28 | 29 | impl<'a> From<&'a str> for Utf8Str { 30 | fn from(s: &'a str) -> Self { 31 | Self { 32 | ptr: s.as_ptr() as *const c_char, 33 | len: s.len(), 34 | } 35 | } 36 | } 37 | 38 | #[repr(C)] 39 | #[derive(Default, Debug, Copy, Clone)] 40 | pub struct Quaternion { 41 | x: f32, 42 | y: f32, 43 | z: f32, 44 | w: f32, 45 | } 46 | #[repr(C)] 47 | #[derive(Clone, Copy)] 48 | pub struct Entity { 49 | pub id: u64, 50 | } 51 | #[repr(C)] 52 | #[derive(Default, Debug, Copy, Clone)] 53 | pub struct Color { 54 | pub r: u8, 55 | pub g: u8, 56 | pub b: u8, 57 | pub a: u8, 58 | } 59 | 60 | /// cbindgen:ignore 61 | impl Color { 62 | pub const RED: Self = Self { 63 | r: 255, 64 | g: 0, 65 | b: 0, 66 | a: 255, 67 | }; 68 | pub const GREEN: Self = Self { 69 | r: 0, 70 | g: 255, 71 | b: 0, 72 | a: 255, 73 | }; 74 | pub const BLUE: Self = Self { 75 | r: 0, 76 | g: 0, 77 | b: 255, 78 | a: 255, 79 | }; 80 | } 81 | 82 | #[repr(C)] 83 | #[derive(Default, Debug, Copy, Clone)] 84 | pub struct Vector3 { 85 | pub x: f32, 86 | pub y: f32, 87 | pub z: f32, 88 | } 89 | 90 | #[repr(C)] 91 | #[derive(Default, Debug, Copy, Clone)] 92 | pub struct Movement { 93 | pub velocity: Vector3, 94 | pub is_falling: u32, 95 | } 96 | 97 | impl From for Quaternion { 98 | fn from(v: Quat) -> Self { 99 | Quaternion { 100 | x: v.x, 101 | y: v.y, 102 | z: v.z, 103 | w: v.w, 104 | } 105 | } 106 | } 107 | impl From for Quat { 108 | fn from(val: Quaternion) -> Self { 109 | Quat::from_xyzw(val.x, val.y, val.z, val.w) 110 | } 111 | } 112 | 113 | impl From for Vec3 { 114 | fn from(val: Vector3) -> Self { 115 | Vec3::new(val.x, val.y, val.z) 116 | } 117 | } 118 | 119 | impl From for Vector3 { 120 | fn from(v: Vec3) -> Self { 121 | Vector3 { 122 | x: v.x, 123 | y: v.y, 124 | z: v.z, 125 | } 126 | } 127 | } 128 | 129 | // TODO: Is there a more typesafe way of defining an opaque type that 130 | // is c ffi safe in Rust without nightly? 131 | pub type AActorOpaque = c_void; 132 | pub type UPrimtiveOpaque = c_void; 133 | pub type UCapsuleOpaque = c_void; 134 | pub type UClassOpague = c_void; 135 | pub type UObjectOpague = c_void; 136 | pub type USoundBaseOpague = c_void; 137 | 138 | pub type LogFn = extern "C" fn(*const c_char, i32); 139 | pub type IterateActorsFn = unsafe extern "C" fn(array: *mut *mut AActorOpaque, len: *mut u64); 140 | pub type GetActionStateFn = 141 | unsafe extern "C" fn(name: *const c_char, len: usize, state: ActionState, out: *mut u32); 142 | pub type GetAxisValueFn = unsafe extern "C" fn(name: *const c_char, len: usize, value: &mut f32); 143 | pub type SpawnActorFn = unsafe extern "C" fn( 144 | actor_class: ActorClass, 145 | position: Vector3, 146 | rotation: Quaternion, 147 | scale: Vector3, 148 | ) -> *mut AActorOpaque; 149 | pub type GetMouseDeltaFn = unsafe extern "C" fn(x: &mut f32, y: &mut f32); 150 | pub type VisualLogSegmentFn = 151 | unsafe extern "C" fn(owner: *const AActorOpaque, start: Vector3, end: Vector3, color: Color); 152 | pub type VisualLogCapsuleFn = unsafe extern "C" fn( 153 | category: Utf8Str, 154 | owner: *const AActorOpaque, 155 | position: Vector3, 156 | rotation: Quaternion, 157 | half_height: f32, 158 | radius: f32, 159 | color: Color, 160 | ); 161 | 162 | pub type VisualLogLocationFn = unsafe extern "C" fn( 163 | category: Utf8Str, 164 | owner: *const AActorOpaque, 165 | position: Vector3, 166 | radius: f32, 167 | color: Color, 168 | ); 169 | 170 | extern "C" { 171 | pub fn TickActor(actor: *mut AActorOpaque, dt: f32); 172 | pub fn Log(s: *const c_char, len: i32); 173 | pub fn IterateActors(array: *mut *mut AActorOpaque, len: *mut u64); 174 | pub fn GetActionState(name: *const c_char, len: usize, state: ActionState, out: *mut u32); 175 | pub fn GetAxisValue(name: *const c_char, len: usize, value: &mut f32); 176 | pub fn SpawnActor( 177 | actor_class: ActorClass, 178 | position: Vector3, 179 | rotation: Quaternion, 180 | scale: Vector3, 181 | ) -> *mut AActorOpaque; 182 | pub fn GetMouseDelta(x: &mut f32, y: &mut f32); 183 | 184 | pub fn VisualLogSegment(owner: *const AActorOpaque, start: Vector3, end: Vector3, color: Color); 185 | pub fn VisualLogCapsule( 186 | category: Utf8Str, 187 | owner: *const AActorOpaque, 188 | position: Vector3, 189 | rotation: Quaternion, 190 | half_height: f32, 191 | radius: f32, 192 | color: Color, 193 | ); 194 | pub fn VisualLogLocation( 195 | category: Utf8Str, 196 | owner: *const AActorOpaque, 197 | position: Vector3, 198 | radius: f32, 199 | color: Color, 200 | ); 201 | } 202 | 203 | #[repr(C)] 204 | pub struct UnrealBindings { 205 | pub actor_fns: ActorFns, 206 | pub physics_fns: PhysicsFns, 207 | pub log: LogFn, 208 | pub iterate_actors: IterateActorsFn, 209 | pub get_action_state: GetActionStateFn, 210 | pub get_axis_value: GetAxisValueFn, 211 | pub spawn_actor: SpawnActorFn, 212 | pub get_mouse_delta: GetMouseDeltaFn, 213 | pub visual_log_segment: VisualLogSegmentFn, 214 | pub visual_log_capsule: VisualLogCapsuleFn, 215 | pub visual_log_location: VisualLogLocationFn, 216 | pub editor_component_fns: EditorComponentFns, 217 | pub sound_fns: SoundFns, 218 | } 219 | unsafe impl Sync for UnrealBindings {} 220 | unsafe impl Send for UnrealBindings {} 221 | 222 | #[repr(u8)] 223 | #[derive(Debug)] 224 | pub enum ActionState { 225 | Pressed = 0, 226 | Released = 1, 227 | Held = 2, 228 | } 229 | #[repr(u32)] 230 | #[derive(Debug)] 231 | pub enum ActorClass { 232 | RustActor = 0, 233 | CameraActor = 1, 234 | } 235 | #[repr(u32)] 236 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 237 | pub enum ActorComponentType { 238 | Primitive, 239 | } 240 | 241 | #[repr(u8)] 242 | #[derive(Debug)] 243 | pub enum Mobility { 244 | Static = 0, 245 | Stationary = 1, 246 | Moveable = 2, 247 | } 248 | 249 | #[repr(C)] 250 | #[derive(Debug)] 251 | pub struct ActorComponentPtr { 252 | pub ty: ActorComponentType, 253 | pub ptr: *mut c_void, 254 | } 255 | 256 | impl Default for ActorComponentPtr { 257 | fn default() -> Self { 258 | Self { 259 | ty: ActorComponentType::Primitive, 260 | ptr: std::ptr::null_mut(), 261 | } 262 | } 263 | } 264 | 265 | #[repr(C)] 266 | #[derive(Default, Clone, Copy)] 267 | pub struct Uuid { 268 | pub a: u32, 269 | pub b: u32, 270 | pub c: u32, 271 | pub d: u32, 272 | } 273 | 274 | pub type EntryUnrealBindingsFn = 275 | unsafe extern "C" fn(bindings: UnrealBindings, rust_bindings: *mut RustBindings) -> u32; 276 | pub type BeginPlayFn = unsafe extern "C" fn() -> ResultCode; 277 | pub type TickFn = unsafe extern "C" fn(dt: f32) -> ResultCode; 278 | pub type RetrieveUuids = unsafe extern "C" fn(ptr: *mut Uuid, len: *mut usize); 279 | pub type GetVelocityRustFn = 280 | unsafe extern "C" fn(actor: *const AActorOpaque, velocity: &mut Vector3); 281 | 282 | #[repr(u32)] 283 | pub enum EventType { 284 | ActorSpawned = 0, 285 | ActorBeginOverlap = 1, 286 | ActorEndOverlap = 2, 287 | ActorOnHit = 3, 288 | ActorDestroy = 4, 289 | } 290 | 291 | #[repr(C)] 292 | pub struct ActorSpawnedEvent { 293 | pub actor: *mut AActorOpaque, 294 | } 295 | 296 | #[repr(C)] 297 | pub struct ActorBeginOverlap { 298 | pub overlapped_actor: *mut AActorOpaque, 299 | pub other: *mut AActorOpaque, 300 | } 301 | #[repr(C)] 302 | pub struct ActorEndOverlap { 303 | pub overlapped_actor: *mut AActorOpaque, 304 | pub other: *mut AActorOpaque, 305 | } 306 | 307 | #[repr(C)] 308 | pub struct ActorHitEvent { 309 | pub self_actor: *mut AActorOpaque, 310 | pub other: *mut AActorOpaque, 311 | pub normal_impulse: Vector3, 312 | } 313 | 314 | #[repr(C)] 315 | pub struct ActorDestroyEvent { 316 | pub actor: *mut AActorOpaque, 317 | } 318 | 319 | #[repr(C)] 320 | pub struct RustBindings { 321 | pub retrieve_uuids: RetrieveUuids, 322 | pub tick: TickFn, 323 | pub begin_play: BeginPlayFn, 324 | pub unreal_event: UnrealEventFn, 325 | pub reflection_fns: ReflectionFns, 326 | pub allocate_fns: AllocateFns, 327 | } 328 | 329 | pub type UnrealEventFn = unsafe extern "C" fn(ty: *const EventType, data: *const c_void); 330 | 331 | #[repr(u32)] 332 | pub enum ReflectionType { 333 | Float, 334 | Vector3, 335 | Bool, 336 | Quaternion, 337 | UClass, 338 | USound, 339 | Composite, 340 | } 341 | 342 | pub type NumberOfFieldsFn = unsafe extern "C" fn(uuid: Uuid, out: *mut u32) -> u32; 343 | pub type GetTypeNameFn = unsafe extern "C" fn(uuid: Uuid, name: *mut Utf8Str) -> u32; 344 | pub type GetFieldNameFn = 345 | unsafe extern "C" fn(uuid: Uuid, field_idx: u32, name: *mut Utf8Str) -> u32; 346 | pub type GetFieldTypeFn = 347 | unsafe extern "C" fn(uuid: Uuid, field_idx: u32, ty: *mut ReflectionType) -> u32; 348 | 349 | pub type GetFieldFloatValueFn = 350 | unsafe extern "C" fn(uuid: Uuid, entity: Entity, field_idx: u32, out: *mut f32) -> u32; 351 | pub type GetFieldVector3ValueFn = 352 | unsafe extern "C" fn(uuid: Uuid, entity: Entity, field_idx: u32, out: *mut Vector3) -> u32; 353 | pub type GetFieldBoolValueFn = 354 | unsafe extern "C" fn(uuid: Uuid, entity: Entity, field_idx: u32, out: *mut u32) -> u32; 355 | pub type GetFieldQuatValueFn = 356 | unsafe extern "C" fn(uuid: Uuid, entity: Entity, field_idx: u32, out: *mut Quaternion) -> u32; 357 | pub type HasComponentFn = unsafe extern "C" fn(entity: Entity, uuid: Uuid) -> u32; 358 | pub type IsEditorComponentFn = unsafe extern "C" fn(uuid: Uuid) -> u32; 359 | 360 | #[repr(C)] 361 | pub struct ReflectionFns { 362 | pub is_editor_component: IsEditorComponentFn, 363 | pub number_of_fields: NumberOfFieldsFn, 364 | pub has_component: HasComponentFn, 365 | pub get_type_name: GetTypeNameFn, 366 | pub get_field_type: GetFieldTypeFn, 367 | pub get_field_name: GetFieldNameFn, 368 | pub get_field_vector3_value: GetFieldVector3ValueFn, 369 | pub get_field_bool_value: GetFieldBoolValueFn, 370 | pub get_field_float_value: GetFieldFloatValueFn, 371 | pub get_field_quat_value: GetFieldQuatValueFn, 372 | } 373 | 374 | #[repr(C)] 375 | pub struct RustAlloc { 376 | pub ptr: *mut u8, 377 | pub size: usize, 378 | pub align: usize, 379 | } 380 | 381 | impl RustAlloc { 382 | pub fn empty() -> Self { 383 | Self { 384 | ptr: std::ptr::null_mut(), 385 | size: 0, 386 | align: 0, 387 | } 388 | } 389 | /// # Safety 390 | /// Must have a valid allocation from within unreal c++ 391 | /// Only free if the ptr is not already in use 392 | /// Ptr must be valid, and allocated from the same allocator 393 | pub unsafe fn free(self) { 394 | if self.size == 0 || self.ptr.is_null() { 395 | return; 396 | } 397 | std::alloc::dealloc( 398 | self.ptr, 399 | std::alloc::Layout::from_size_align(self.size, self.align).unwrap(), 400 | ); 401 | } 402 | } 403 | 404 | pub type AllocateFn = unsafe extern "C" fn(size: usize, align: usize, ptr: *mut RustAlloc) -> u32; 405 | 406 | #[repr(C)] 407 | pub struct AllocateFns { 408 | pub allocate: AllocateFn, 409 | } 410 | 411 | extern "C" { 412 | pub fn GetEditorComponentUuids( 413 | actor: *const AActorOpaque, 414 | data: *mut Uuid, 415 | len: *mut usize, 416 | ) -> u32; 417 | 418 | pub fn GetEditorComponentVector( 419 | actor: *const AActorOpaque, 420 | uuid: Uuid, 421 | field: Utf8Str, 422 | out: *mut Vector3, 423 | ) -> u32; 424 | pub fn GetEditorComponentFloat( 425 | actor: *const AActorOpaque, 426 | uuid: Uuid, 427 | field: Utf8Str, 428 | out: *mut f32, 429 | ) -> u32; 430 | pub fn GetEditorComponentBool( 431 | actor: *const AActorOpaque, 432 | uuid: Uuid, 433 | field: Utf8Str, 434 | out: *mut u32, 435 | ) -> u32; 436 | pub fn GetEditorComponentQuat( 437 | actor: *const AActorOpaque, 438 | uuid: Uuid, 439 | field: Utf8Str, 440 | out: *mut Quaternion, 441 | ) -> u32; 442 | pub fn GetEditorComponentUObject( 443 | actor: *const AActorOpaque, 444 | uuid: Uuid, 445 | field: Utf8Str, 446 | ty: UObjectType, 447 | out: *mut *mut UObjectOpague, 448 | ) -> u32; 449 | } 450 | 451 | pub type GetEditorComponentUuidsFn = 452 | unsafe extern "C" fn(actor: *const AActorOpaque, data: *mut Uuid, len: *mut usize) -> u32; 453 | 454 | pub type GetEditorComponentQuatFn = unsafe extern "C" fn( 455 | actor: *const AActorOpaque, 456 | uuid: Uuid, 457 | field: Utf8Str, 458 | out: *mut Quaternion, 459 | ) -> u32; 460 | 461 | pub type GetEditorComponentVectorFn = unsafe extern "C" fn( 462 | actor: *const AActorOpaque, 463 | uuid: Uuid, 464 | field: Utf8Str, 465 | out: *mut Vector3, 466 | ) -> u32; 467 | 468 | pub type GetEditorComponentFloatFn = unsafe extern "C" fn( 469 | actor: *const AActorOpaque, 470 | uuid: Uuid, 471 | field: Utf8Str, 472 | out: *mut f32, 473 | ) -> u32; 474 | 475 | pub type GetEditorComponentBoolFn = unsafe extern "C" fn( 476 | actor: *const AActorOpaque, 477 | uuid: Uuid, 478 | field: Utf8Str, 479 | out: *mut u32, 480 | ) -> u32; 481 | pub type GetEditorComponentUObjectFn = unsafe extern "C" fn( 482 | actor: *const AActorOpaque, 483 | uuid: Uuid, 484 | field: Utf8Str, 485 | ty: UObjectType, 486 | out: *mut *mut UObjectOpague, 487 | ) -> u32; 488 | 489 | #[repr(C)] 490 | pub struct EditorComponentFns { 491 | pub get_editor_components: GetEditorComponentUuidsFn, 492 | pub get_editor_component_quat: GetEditorComponentQuatFn, 493 | pub get_editor_component_vector: GetEditorComponentVectorFn, 494 | pub get_editor_component_bool: GetEditorComponentBoolFn, 495 | pub get_editor_component_float: GetEditorComponentFloatFn, 496 | pub get_editor_component_uobject: GetEditorComponentUObjectFn, 497 | } 498 | -------------------------------------------------------------------------------- /unreal-ffi/src/physics.rs: -------------------------------------------------------------------------------- 1 | use crate::{AActorOpaque, Quaternion, UPrimtiveOpaque, Vector3}; 2 | 3 | #[repr(C)] 4 | #[derive(Copy, Clone)] 5 | pub struct CollisionBox { 6 | pub half_extent_x: f32, 7 | pub half_extent_y: f32, 8 | pub half_extent_z: f32, 9 | } 10 | 11 | #[repr(C)] 12 | #[derive(Copy, Clone)] 13 | pub struct CollisionSphere { 14 | pub radius: f32, 15 | } 16 | 17 | #[repr(C)] 18 | #[derive(Copy, Clone)] 19 | pub struct CollisionCapsule { 20 | pub radius: f32, 21 | pub half_height: f32, 22 | } 23 | 24 | #[repr(C)] 25 | #[derive(Copy, Clone)] 26 | pub struct CollisionShape { 27 | pub data: CollisionShapeUnion, 28 | pub ty: CollisionShapeType, 29 | } 30 | 31 | impl Default for CollisionShape { 32 | fn default() -> Self { 33 | Self { 34 | data: CollisionShapeUnion { 35 | sphere: CollisionSphere { radius: 0.0 }, 36 | }, 37 | ty: CollisionShapeType::Sphere, 38 | } 39 | } 40 | } 41 | 42 | #[repr(u32)] 43 | #[derive(Copy, Clone)] 44 | pub enum CollisionShapeType { 45 | Box, 46 | Capsule, 47 | Sphere, 48 | } 49 | 50 | #[repr(C)] 51 | #[derive(Copy, Clone)] 52 | pub union CollisionShapeUnion { 53 | pub collision_box: CollisionBox, 54 | pub sphere: CollisionSphere, 55 | pub capsule: CollisionCapsule, 56 | } 57 | 58 | #[repr(C)] 59 | pub struct LineTraceParams { 60 | pub ignored_actors: *const *mut AActorOpaque, 61 | pub ignored_actors_len: usize, 62 | } 63 | 64 | #[repr(C)] 65 | #[derive(Debug)] 66 | pub struct OverlapResult { 67 | pub actor: *mut AActorOpaque, 68 | pub primtive: *mut UPrimtiveOpaque, 69 | } 70 | 71 | impl Default for OverlapResult { 72 | fn default() -> Self { 73 | Self { 74 | actor: std::ptr::null_mut(), 75 | primtive: std::ptr::null_mut(), 76 | } 77 | } 78 | } 79 | 80 | #[repr(C)] 81 | #[derive(Debug)] 82 | pub struct HitResult { 83 | pub actor: *mut AActorOpaque, 84 | pub primtive: *mut UPrimtiveOpaque, 85 | pub distance: f32, 86 | pub normal: Vector3, 87 | pub location: Vector3, 88 | pub impact_normal: Vector3, 89 | pub impact_location: Vector3, 90 | pub pentration_depth: f32, 91 | pub start_penetrating: u32, 92 | } 93 | 94 | impl Default for HitResult { 95 | fn default() -> Self { 96 | Self { 97 | actor: std::ptr::null_mut(), 98 | primtive: std::ptr::null_mut(), 99 | distance: Default::default(), 100 | normal: Default::default(), 101 | location: Default::default(), 102 | impact_location: Default::default(), 103 | impact_normal: Default::default(), 104 | pentration_depth: Default::default(), 105 | start_penetrating: Default::default(), 106 | } 107 | } 108 | } 109 | 110 | pub type GetVelocityFn = unsafe extern "C" fn(primitive: *const UPrimtiveOpaque) -> Vector3; 111 | 112 | pub type SetVelocityFn = unsafe extern "C" fn(primitive: *mut UPrimtiveOpaque, velocity: Vector3); 113 | 114 | pub type IsSimulatingFn = unsafe extern "C" fn(primitive: *const UPrimtiveOpaque) -> u32; 115 | 116 | pub type AddForceFn = unsafe extern "C" fn(actor: *mut UPrimtiveOpaque, force: Vector3); 117 | 118 | pub type AddImpulseFn = unsafe extern "C" fn(actor: *mut UPrimtiveOpaque, force: Vector3); 119 | 120 | pub type LineTraceFn = unsafe extern "C" fn( 121 | start: Vector3, 122 | end: Vector3, 123 | params: LineTraceParams, 124 | result: &mut HitResult, 125 | ) -> u32; 126 | pub type GetBoundingBoxExtentFn = 127 | unsafe extern "C" fn(primitive: *const UPrimtiveOpaque) -> Vector3; 128 | 129 | pub type SweepFn = unsafe extern "C" fn( 130 | start: Vector3, 131 | end: Vector3, 132 | rotation: Quaternion, 133 | params: LineTraceParams, 134 | collision_shape: CollisionShape, 135 | result: &mut HitResult, 136 | ) -> u32; 137 | 138 | pub type OverlapMultiFn = unsafe extern "C" fn( 139 | collision_shape: CollisionShape, 140 | position: Vector3, 141 | rotation: Quaternion, 142 | params: LineTraceParams, 143 | max_results: usize, 144 | result: *mut *mut OverlapResult, 145 | ) -> u32; 146 | 147 | pub type GetCollisionShapeFn = 148 | unsafe extern "C" fn(primitive: *const UPrimtiveOpaque, shape: *mut CollisionShape) -> u32; 149 | 150 | pub type SweepMultiFn = unsafe extern "C" fn( 151 | start: Vector3, 152 | end: Vector3, 153 | rotation: Quaternion, 154 | params: LineTraceParams, 155 | collision_shape: CollisionShape, 156 | max_results: usize, 157 | results: *mut HitResult, 158 | ) -> u32; 159 | 160 | extern "C" { 161 | pub fn GetVelocity(primitive: *const UPrimtiveOpaque) -> Vector3; 162 | 163 | pub fn SetVelocity(primitive: *mut UPrimtiveOpaque, velocity: Vector3); 164 | 165 | pub fn IsSimulating(primitive: *const UPrimtiveOpaque) -> u32; 166 | 167 | pub fn AddForce(actor: *mut UPrimtiveOpaque, force: Vector3); 168 | 169 | pub fn AddImpulse(actor: *mut UPrimtiveOpaque, force: Vector3); 170 | 171 | pub fn LineTrace( 172 | start: Vector3, 173 | end: Vector3, 174 | params: LineTraceParams, 175 | result: &mut HitResult, 176 | ) -> u32; 177 | 178 | pub fn GetBoundingBoxExtent(primitive: *const UPrimtiveOpaque) -> Vector3; 179 | 180 | pub fn Sweep( 181 | start: Vector3, 182 | end: Vector3, 183 | rotation: Quaternion, 184 | params: LineTraceParams, 185 | collision_shape: CollisionShape, 186 | result: &mut HitResult, 187 | ) -> u32; 188 | 189 | pub fn SweepMulti( 190 | start: Vector3, 191 | end: Vector3, 192 | rotation: Quaternion, 193 | params: LineTraceParams, 194 | collision_shape: CollisionShape, 195 | max_results: usize, 196 | results: *mut HitResult, 197 | ) -> u32; 198 | 199 | pub fn OverlapMulti( 200 | collision_shape: CollisionShape, 201 | position: Vector3, 202 | rotation: Quaternion, 203 | params: LineTraceParams, 204 | max_results: usize, 205 | result: *mut *mut OverlapResult, 206 | ) -> u32; 207 | 208 | pub fn GetCollisionShape(primitive: *const UPrimtiveOpaque, shape: *mut CollisionShape) -> u32; 209 | } 210 | 211 | #[repr(C)] 212 | pub struct PhysicsFns { 213 | pub get_velocity: GetVelocityFn, 214 | pub set_velocity: SetVelocityFn, 215 | pub is_simulating: IsSimulatingFn, 216 | pub add_force: AddForceFn, 217 | pub add_impulse: AddImpulseFn, 218 | pub line_trace: LineTraceFn, 219 | pub get_bounding_box_extent: GetBoundingBoxExtentFn, 220 | pub sweep: SweepFn, 221 | pub sweep_multi: SweepMultiFn, 222 | pub overlap_multi: OverlapMultiFn, 223 | pub get_collision_shape: GetCollisionShapeFn, 224 | } 225 | -------------------------------------------------------------------------------- /unreal-ffi/src/sound.rs: -------------------------------------------------------------------------------- 1 | use crate::{Quaternion, USoundBaseOpague, Vector3}; 2 | 3 | #[repr(C)] 4 | pub struct SoundSettings { 5 | pub volume: f32, 6 | pub pitch: f32, 7 | } 8 | impl Default for SoundSettings { 9 | fn default() -> Self { 10 | Self { 11 | volume: 1.0, 12 | pitch: 1.0, 13 | } 14 | } 15 | } 16 | 17 | extern "C" { 18 | pub fn PlaySoundAtLocation( 19 | sound: *const USoundBaseOpague, 20 | location: Vector3, 21 | rotation: Quaternion, 22 | settings: *const SoundSettings, 23 | ); 24 | } 25 | pub type PlaySoundAtLocationFn = unsafe extern "C" fn( 26 | sound: *const USoundBaseOpague, 27 | location: Vector3, 28 | rotation: Quaternion, 29 | settings: *const SoundSettings, 30 | ); 31 | 32 | #[repr(C)] 33 | pub struct SoundFns { 34 | pub play_sound_at_location: PlaySoundAtLocationFn, 35 | } 36 | -------------------------------------------------------------------------------- /unreal-ffi/wrapper.h: -------------------------------------------------------------------------------- 1 | #include "../RustPlugin/Source/RustPlugin/Public/Bindings.h" -------------------------------------------------------------------------------- /unreal-reflect/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "unreal-reflect" 3 | version = "0.1.0" 4 | edition = "2021" 5 | license = "MIT OR Apache-2.0" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | 11 | uuid = { version = "1", features = ["v4", "serde"] } 12 | unreal-ffi = { path = "../unreal-ffi" } 13 | bevy_ecs = "0.8" 14 | log = "0.4" 15 | glam = "0.21" 16 | -------------------------------------------------------------------------------- /unreal-reflect/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use bevy_ecs as ecs; 2 | pub use bevy_ecs::{entity::Entity, world::World}; 3 | pub use uuid; 4 | pub use uuid::Uuid; 5 | 6 | pub mod registry; 7 | 8 | pub trait TypeUuid { 9 | const TYPE_UUID: Uuid; 10 | } 11 | 12 | pub trait TypeUuidDynamic { 13 | fn type_uuid(&self) -> Uuid; 14 | fn type_name(&self) -> &'static str; 15 | } 16 | 17 | impl TypeUuidDynamic for T 18 | where 19 | T: TypeUuid, 20 | { 21 | fn type_uuid(&self) -> Uuid { 22 | Self::TYPE_UUID 23 | } 24 | 25 | fn type_name(&self) -> &'static str { 26 | std::any::type_name::() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /unreal-reflect/src/registry.rs: -------------------------------------------------------------------------------- 1 | use bevy_ecs::{entity::Entity, prelude::World}; 2 | use glam::{Quat, Vec3}; 3 | use unreal_ffi as ffi; 4 | 5 | #[derive(Copy, Clone, Debug)] 6 | pub struct UClass { 7 | pub ptr: *mut ffi::UObjectOpague, 8 | } 9 | unsafe impl Send for UClass {} 10 | unsafe impl Sync for UClass {} 11 | 12 | #[derive(Copy, Clone, Debug)] 13 | pub struct USound { 14 | pub ptr: *mut ffi::UObjectOpague, 15 | } 16 | unsafe impl Send for USound {} 17 | unsafe impl Sync for USound {} 18 | 19 | pub enum ReflectValue { 20 | Float(f32), 21 | Vector3(Vec3), 22 | Bool(bool), 23 | Quat(Quat), 24 | UClass(UClass), 25 | USound(USound), 26 | Composite, 27 | } 28 | 29 | pub enum ReflectType { 30 | Float, 31 | Vector3, 32 | Bool, 33 | Quat, 34 | UClass, 35 | USound, 36 | Composite, 37 | } 38 | 39 | pub trait ReflectDyn { 40 | fn name(&self) -> &'static str; 41 | fn number_of_fields(&self) -> u32 { 42 | 0 43 | } 44 | fn get_field_name(&self, _idx: u32) -> Option<&'static str> { 45 | None 46 | } 47 | fn get_field_type(&self, _idx: u32) -> Option { 48 | None 49 | } 50 | fn has_component(&self, _world: &World, _entity: Entity) -> bool { 51 | false 52 | } 53 | fn get_field_value(&self, _world: &World, _entity: Entity, _idx: u32) -> Option { 54 | None 55 | } 56 | fn get_value(&self) -> ReflectValue; 57 | } 58 | 59 | impl ReflectDyn for UClass { 60 | fn name(&self) -> &'static str { 61 | "UClass" 62 | } 63 | 64 | fn get_value(&self) -> ReflectValue { 65 | ReflectValue::UClass(*self) 66 | } 67 | } 68 | impl ReflectStatic for UClass { 69 | const TYPE: ReflectType = ReflectType::UClass; 70 | } 71 | impl ReflectDyn for USound { 72 | fn name(&self) -> &'static str { 73 | "USound" 74 | } 75 | 76 | fn get_value(&self) -> ReflectValue { 77 | ReflectValue::USound(*self) 78 | } 79 | } 80 | 81 | impl ReflectStatic for USound { 82 | const TYPE: ReflectType = ReflectType::USound; 83 | } 84 | 85 | impl ReflectDyn for Vec3 { 86 | fn name(&self) -> &'static str { 87 | "Vec3" 88 | } 89 | 90 | fn get_value(&self) -> ReflectValue { 91 | ReflectValue::Vector3(*self) 92 | } 93 | } 94 | 95 | impl ReflectStatic for Vec3 { 96 | const TYPE: ReflectType = ReflectType::Vector3; 97 | } 98 | 99 | impl ReflectDyn for Quat { 100 | fn name(&self) -> &'static str { 101 | "Quat" 102 | } 103 | 104 | fn get_value(&self) -> ReflectValue { 105 | ReflectValue::Quat(*self) 106 | } 107 | } 108 | impl ReflectStatic for Quat { 109 | const TYPE: ReflectType = ReflectType::Quat; 110 | } 111 | 112 | impl ReflectDyn for f32 { 113 | fn name(&self) -> &'static str { 114 | "f32" 115 | } 116 | 117 | fn get_value(&self) -> ReflectValue { 118 | ReflectValue::Float(*self) 119 | } 120 | } 121 | 122 | impl ReflectStatic for f32 { 123 | const TYPE: ReflectType = ReflectType::Float; 124 | } 125 | impl ReflectDyn for bool { 126 | fn name(&self) -> &'static str { 127 | "bool" 128 | } 129 | 130 | fn get_value(&self) -> ReflectValue { 131 | ReflectValue::Bool(*self) 132 | } 133 | } 134 | 135 | impl ReflectStatic for bool { 136 | const TYPE: ReflectType = ReflectType::Bool; 137 | } 138 | 139 | pub trait ReflectStatic { 140 | const TYPE: ReflectType; 141 | } 142 | -------------------------------------------------------------------------------- /unreal-rust-example/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "unreal-rust-example" 3 | version = "0.1.0" 4 | edition = "2021" 5 | license = "MIT OR Apache-2.0" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | unreal-api = { path = "../unreal-api"} 11 | unreal-reflect = { path = "../unreal-reflect" } 12 | bevy_ecs = "0.8" 13 | log = "0.4" 14 | unreal-movement = { path = "../gameplay-plugins/unreal-movement" } 15 | 16 | [lib] 17 | crate-type = ["cdylib"] 18 | -------------------------------------------------------------------------------- /unreal-rust-example/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use bevy_ecs::prelude::*; 4 | use unreal_api::api::UnrealApi; 5 | use unreal_api::core::{ActorHitEvent, Despawn}; 6 | use unreal_api::registry::USound; 7 | use unreal_api::sound::{play_sound_at_location, SoundSettings}; 8 | use unreal_api::Component; 9 | use unreal_api::{ 10 | core::{ActorComponent, ActorPtr, CoreStage, ParentComponent, TransformComponent}, 11 | ffi::{self, UClassOpague}, 12 | input::Input, 13 | math::{Quat, Vec3}, 14 | module::{bindings, InitUserModule, Module, UserModule}, 15 | register_components, 16 | }; 17 | use unreal_movement::{ 18 | CharacterConfigComponent, CharacterControllerComponent, MovementComponent, MovementPlugin, 19 | }; 20 | 21 | #[repr(u32)] 22 | #[derive(Copy, Clone)] 23 | pub enum Class { 24 | Player = 0, 25 | } 26 | 27 | impl Class { 28 | pub fn from(i: u32) -> Option { 29 | match i { 30 | 0 => Some(Self::Player), 31 | _ => None, 32 | } 33 | } 34 | } 35 | 36 | #[derive(Default)] 37 | pub struct ClassesResource { 38 | classes: HashMap<*mut ffi::UClassOpague, Class>, 39 | } 40 | unsafe impl Send for ClassesResource {} 41 | unsafe impl Sync for ClassesResource {} 42 | 43 | #[derive(Default, Debug, Copy, Clone)] 44 | pub enum CameraMode { 45 | #[default] 46 | ThirdPerson, 47 | FirstPerson, 48 | } 49 | 50 | impl CameraMode { 51 | pub fn toggle(&mut self) { 52 | *self = match *self { 53 | CameraMode::ThirdPerson => CameraMode::FirstPerson, 54 | CameraMode::FirstPerson => CameraMode::ThirdPerson, 55 | }; 56 | } 57 | } 58 | #[derive(Debug, Component)] 59 | #[uuid = "b6addc7d-03b1-4b06-9328-f26c71997ee6"] 60 | #[reflect(editor)] 61 | pub struct PlaySoundOnImpactComponent { 62 | pub sound: USound, 63 | } 64 | impl PlaySoundOnImpactComponent { 65 | const MINIMUM_FORCE: f32 = 30000.0; 66 | } 67 | 68 | #[derive(Debug, Component)] 69 | #[uuid = "52788d7e-017b-42cd-b3bf-aa616315c0c4"] 70 | #[reflect(editor)] 71 | pub struct CharacterSoundsComponent { 72 | pub camera_toggle: USound, 73 | } 74 | 75 | #[derive(Default, Debug, Component)] 76 | #[uuid = "8d2df877-499b-46f3-9660-bd2e1867af0d"] 77 | pub struct CameraComponent { 78 | pub x: f32, 79 | pub y: f32, 80 | pub current_x: f32, 81 | pub current_y: f32, 82 | #[reflect(skip)] 83 | pub mode: CameraMode, 84 | } 85 | 86 | pub struct PlayerInput; 87 | impl PlayerInput { 88 | pub const MOVE_FORWARD: &'static str = "MoveForward"; 89 | pub const MOVE_RIGHT: &'static str = "MoveRight"; 90 | pub const LOOK_UP: &'static str = "LookUp"; 91 | pub const TURN_RIGHT: &'static str = "TurnRight"; 92 | pub const TOGGLE_CAMERA: &'static str = "ToggleCamera"; 93 | pub const JUMP: &'static str = "Jump"; 94 | } 95 | 96 | // TODO: We probably don't need that anymore 97 | fn register_class_resource(mut commands: Commands) { 98 | let mut len: usize = 0; 99 | unsafe { 100 | (bindings().actor_fns.get_registered_classes)(std::ptr::null_mut(), &mut len); 101 | } 102 | let mut classes: Vec<*mut UClassOpague> = Vec::with_capacity(len); 103 | unsafe { 104 | (bindings().actor_fns.get_registered_classes)(classes.as_mut_ptr(), &mut len); 105 | classes.set_len(len); 106 | } 107 | 108 | let mut classes_resource = ClassesResource::default(); 109 | 110 | for (id, class_ptr) in classes.into_iter().enumerate() { 111 | if let Some(class) = Class::from(id as u32) { 112 | classes_resource.classes.insert(class_ptr, class); 113 | } 114 | } 115 | commands.insert_resource(classes_resource); 116 | } 117 | 118 | fn register_hit_events(mut query: Query<(&mut ActorComponent, Added)>) { 119 | for (mut actor, added) in &mut query { 120 | if added { 121 | actor.register_on_hit(); 122 | } 123 | } 124 | } 125 | 126 | fn play_sound_on_hit( 127 | api: Res, 128 | mut events: EventReader, 129 | query: Query<(&TransformComponent, &PlaySoundOnImpactComponent)>, 130 | mut commands: Commands, 131 | ) { 132 | for event in events.iter() { 133 | if event.normal_impulse.length() <= PlaySoundOnImpactComponent::MINIMUM_FORCE { 134 | continue; 135 | } 136 | 137 | if let Some(&entity) = api.actor_to_entity.get(&event.self_actor) { 138 | if let Ok((trans, sound)) = query.get(entity) { 139 | play_sound_at_location( 140 | sound.sound, 141 | trans.position, 142 | trans.rotation, 143 | &SoundSettings::default(), 144 | ) 145 | } 146 | commands.add(Despawn { entity }); 147 | } 148 | } 149 | } 150 | 151 | fn spawn_class( 152 | class_resource: Res, 153 | query: Query<(Entity, &ActorComponent), Added>, 154 | mut commands: Commands, 155 | ) { 156 | for (entity, actor) in query.iter() { 157 | unsafe { 158 | (bindings().actor_fns.register_actor_on_overlap)(actor.actor.0); 159 | } 160 | unsafe { 161 | (bindings().actor_fns.register_actor_on_hit)(actor.actor.0); 162 | } 163 | unsafe { 164 | let class_ptr = (bindings().actor_fns.get_class)(actor.actor.0); 165 | if let Some(&class) = class_resource.classes.get(&class_ptr) { 166 | match class { 167 | Class::Player => { 168 | commands.entity(entity).insert_bundle(( 169 | CharacterConfigComponent::default(), 170 | CharacterControllerComponent::default(), 171 | MovementComponent::default(), 172 | )); 173 | } 174 | } 175 | } 176 | } 177 | } 178 | } 179 | 180 | fn register_player_input(mut input: ResMut) { 181 | input.register_axis_binding(PlayerInput::MOVE_FORWARD); 182 | input.register_axis_binding(PlayerInput::MOVE_RIGHT); 183 | input.register_axis_binding(PlayerInput::LOOK_UP); 184 | input.register_axis_binding(PlayerInput::TURN_RIGHT); 185 | input.register_action_binding(PlayerInput::JUMP); 186 | input.register_action_binding(PlayerInput::TOGGLE_CAMERA); 187 | } 188 | 189 | fn toggle_camera( 190 | input: Res, 191 | mut camera_query: Query<(Entity, &mut CameraComponent, &ParentComponent)>, 192 | mut actor_query: Query<&mut ActorComponent>, 193 | sound: Query<(&TransformComponent, &CharacterSoundsComponent)>, 194 | ) { 195 | if input.is_action_pressed(PlayerInput::TOGGLE_CAMERA) { 196 | for (entity, mut camera, parent) in camera_query.iter_mut() { 197 | camera.mode.toggle(); 198 | if let Ok([camera_actor, mut parent_actor]) = 199 | actor_query.get_many_mut([entity, parent.parent]) 200 | { 201 | match camera.mode { 202 | CameraMode::FirstPerson => parent_actor.set_owner(Some(&camera_actor)), 203 | CameraMode::ThirdPerson => parent_actor.set_owner(None), 204 | }; 205 | } 206 | 207 | if let Ok((transform, sound)) = sound.get(parent.parent) { 208 | play_sound_at_location( 209 | sound.camera_toggle, 210 | transform.position, 211 | transform.rotation, 212 | &ffi::SoundSettings::default(), 213 | ) 214 | } 215 | } 216 | } 217 | } 218 | fn update_controller_view( 219 | mut movement: Query<&mut CharacterControllerComponent>, 220 | camera: Query<(&ParentComponent, &TransformComponent, &CameraComponent)>, 221 | ) { 222 | for (parent, spatial, _) in camera.iter() { 223 | if let Ok(mut movement) = 224 | movement.get_component_mut::(parent.parent) 225 | { 226 | movement.camera_view = spatial.rotation; 227 | } 228 | } 229 | } 230 | fn rotate_camera(mut query: Query<(&mut TransformComponent, &mut CameraComponent)>) { 231 | fn lerp(start: f32, end: f32, t: f32) -> f32 { 232 | start * (1.0 - t) + end * t 233 | } 234 | let mut x = 0.0; 235 | let mut y = 0.0; 236 | 237 | let max_angle = 85.0f32.to_radians(); 238 | unsafe { 239 | (bindings().get_mouse_delta)(&mut x, &mut y); 240 | } 241 | 242 | for (mut spatial, mut cam) in query.iter_mut() { 243 | let speed = 0.05; 244 | cam.x += x * speed; 245 | cam.y = f32::clamp(cam.y + y * speed, -max_angle, max_angle); 246 | 247 | let smooth = 0.4; 248 | cam.current_x = lerp(cam.current_x, cam.x, smooth); 249 | cam.current_y = lerp(cam.current_y, cam.y, smooth); 250 | 251 | spatial.rotation = 252 | Quat::from_rotation_z(cam.current_x) * Quat::from_rotation_y(-cam.current_y); 253 | } 254 | } 255 | 256 | fn spawn_camera( 257 | mut commands: Commands, 258 | mut query: Query<(Entity, &ActorComponent, Added)>, 259 | ) { 260 | for (entity, _, added) in query.iter_mut() { 261 | if !added { 262 | continue; 263 | } 264 | let pos = Vec3::new(-2587.0, -1800.0, 150.0); 265 | unsafe { 266 | let actor = (bindings().spawn_actor)( 267 | ffi::ActorClass::CameraActor, 268 | pos.into(), 269 | Quat::from_rotation_x(0.0).into(), 270 | Vec3::ONE.into(), 271 | ); 272 | (bindings().actor_fns.set_view_target)(actor); 273 | commands.spawn().insert_bundle(( 274 | TransformComponent { 275 | position: pos, 276 | ..Default::default() 277 | }, 278 | ActorComponent { 279 | actor: ActorPtr(actor), 280 | }, 281 | CameraComponent::default(), 282 | ParentComponent { parent: entity }, 283 | )); 284 | } 285 | } 286 | } 287 | 288 | fn update_camera( 289 | mut query: Query<(Entity, &ParentComponent, &CameraComponent)>, 290 | mut spatial_query: Query<&mut TransformComponent>, 291 | ) { 292 | for (entity, parent, camera) in query.iter_mut() { 293 | let spatial_parent = spatial_query 294 | .get_component::(parent.parent) 295 | .ok() 296 | .cloned(); 297 | let spatial = spatial_query 298 | .get_component_mut::(entity) 299 | .ok(); 300 | if let (Some(mut spatial), Some(parent)) = (spatial, spatial_parent) { 301 | let local_offset = match camera.mode { 302 | CameraMode::ThirdPerson => spatial.rotation * Vec3::new(-500.0, 0.0, 150.0), 303 | CameraMode::FirstPerson => Vec3::new(0.0, 0.0, 50.0), 304 | }; 305 | 306 | spatial.position = parent.position + local_offset; 307 | } 308 | } 309 | } 310 | 311 | pub struct MyModule; 312 | 313 | impl InitUserModule for MyModule { 314 | fn initialize() -> Self { 315 | Self {} 316 | } 317 | } 318 | 319 | impl UserModule for MyModule { 320 | fn initialize(&self, module: &mut Module) { 321 | register_components! { 322 | CharacterSoundsComponent, 323 | PlaySoundOnImpactComponent, 324 | CameraComponent, 325 | => module 326 | 327 | }; 328 | 329 | module 330 | .add_plugin(MovementPlugin) 331 | .add_startup_system_set( 332 | SystemSet::new() 333 | .with_system(register_class_resource) 334 | .with_system(register_player_input) 335 | .with_system(register_hit_events), 336 | ) 337 | .add_system_set_to_stage( 338 | CoreStage::Update, 339 | SystemSet::new() 340 | .with_system(spawn_class) 341 | .with_system(spawn_camera) 342 | .with_system(update_controller_view) 343 | .with_system(rotate_camera) 344 | .with_system(update_camera.after(rotate_camera)) 345 | .with_system(toggle_camera) 346 | .with_system(play_sound_on_hit), 347 | ); 348 | } 349 | } 350 | 351 | unreal_api::implement_unreal_module!(MyModule); 352 | --------------------------------------------------------------------------------