├── .github
└── workflows
│ └── rust.yml
├── .gitignore
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── Cargo.toml
├── LICENSE-APACHE
├── LICENSE-MIT
├── README.md
├── bevy_wasm
├── Cargo.toml
└── src
│ ├── components
│ ├── mod.rs
│ └── wasm_mod.rs
│ ├── lib.rs
│ ├── mod_state.rs
│ ├── plugin.rs
│ ├── runtime
│ ├── mod.rs
│ ├── native
│ │ ├── linker.rs
│ │ └── mod.rs
│ └── web
│ │ ├── linker.rs
│ │ └── mod.rs
│ ├── systems
│ ├── load_instances.rs
│ ├── mod.rs
│ ├── tick_mods.rs
│ └── update_shared_resource.rs
│ └── wasm_asset.rs
├── bevy_wasm_shared
├── Cargo.toml
└── src
│ ├── lib.rs
│ └── version.rs
├── bevy_wasm_sys
├── Cargo.toml
└── src
│ ├── ecs
│ ├── extern_res.rs
│ └── mod.rs
│ ├── events.rs
│ ├── ffi.rs
│ ├── ffi_plugin.rs
│ ├── lib.rs
│ ├── macros.rs
│ └── time.rs
└── examples
├── cubes
├── README.md
├── cubes
│ ├── Cargo.toml
│ ├── README.md
│ ├── assets
│ │ └── .gitkeep
│ ├── build.rs
│ └── src
│ │ └── main.rs
├── cubes_protocol
│ ├── Cargo.toml
│ └── src
│ │ └── lib.rs
├── mod_with_bevy
│ ├── Cargo.toml
│ └── src
│ │ └── lib.rs
└── mod_without_bevy
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
├── shared_resources
├── README.md
├── shared_resources
│ ├── Cargo.toml
│ ├── README.md
│ ├── assets
│ │ └── .gitkeep
│ ├── build.rs
│ └── src
│ │ └── main.rs
├── shared_resources_mod
│ ├── Cargo.toml
│ └── src
│ │ └── lib.rs
└── shared_resources_protocol
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
├── simple
├── simple
│ ├── Cargo.toml
│ ├── assets
│ │ └── .gitkeep
│ ├── build.rs
│ └── src
│ │ └── main.rs
├── simple_mod
│ ├── Cargo.toml
│ └── src
│ │ └── lib.rs
└── simple_protocol
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
└── web
└── index.html
/.github/workflows/rust.yml:
--------------------------------------------------------------------------------
1 | name: Rust
2 |
3 | on:
4 | push:
5 | branches: [ "main" ]
6 | pull_request:
7 | branches: [ "main" ]
8 |
9 | env:
10 | CARGO_TERM_COLOR: always
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: ubuntu-latest
16 |
17 | steps:
18 | - uses: actions/checkout@v3
19 | - uses: dtolnay/rust-toolchain@stable
20 | with:
21 | components: rustfmt, clippy
22 | targets: wasm32-unknown-unknown
23 | - name: Install alsa and udev
24 | run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev libwayland-dev libxkbcommon-dev
25 | - name: Build
26 | run: cargo build --verbose
27 | - name: Run clippy
28 | run: cargo clippy --verbose
29 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | /examples/target
3 | /examples/*/target
4 | Cargo.lock
5 | .DS_Store
6 | **.wasm
7 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 0.10.1
4 |
5 | - Browser support
6 | - WASM files are now Assets
7 |
8 | ## 0.10.0
9 |
10 | - Upgraded to Bevy v0.10
11 |
12 | ## 0.9.9
13 |
14 | - Code is now dual licensed under Apache-2.0 and MIT.
15 | - Added [anyhow](https://crates.io/crates/anyhow)
16 | - Removed unwraps
17 | - Added GitHub Actions workflow
18 |
19 | ## 0.9.8
20 |
21 | - The CHANGELOG.md file
22 | - Mods are now entities with a `WasmMod` component.
23 | - Mods can now be removed
24 | - Mods can now be sent serialized events individually.
25 | - Files restructured to be easier to understand.
26 | - Protocol version calculation now correctly parses integers.
27 |
28 | ## 0.9.7
29 |
30 | - Enabled sharing resources with mods.
31 | - Mod startup systems are now run _after_ the first update
32 | - Updated cubes example to orbit around a center point
33 |
34 | ## 0.9.6
35 |
36 | - Fixed issues with the README
37 |
38 | ## 0.9.5
39 |
40 | - Protocol version checking
41 | - Crate version now matches the major and minor version of Bevy
42 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | brandondyer64+ghcontact@gmail.com.
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series
86 | of actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or
93 | permanent ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120 |
121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
122 | enforcement ladder](https://github.com/mozilla/diversity).
123 |
124 | [homepage]: https://www.contributor-covenant.org
125 |
126 | For answers to common questions about this code of conduct, see the FAQ at
127 | https://www.contributor-covenant.org/faq. Translations are available at
128 | https://www.contributor-covenant.org/translations.
129 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 | members = [
3 | "bevy_wasm",
4 | "bevy_wasm_shared",
5 | "bevy_wasm_sys",
6 | "examples/cubes/cubes",
7 | "examples/cubes/cubes_protocol",
8 | "examples/cubes/mod_with_bevy",
9 | "examples/cubes/mod_without_bevy",
10 | "examples/simple/simple",
11 | "examples/simple/simple_mod",
12 | "examples/simple/simple_protocol",
13 | "examples/shared_resources/shared_resources",
14 | "examples/shared_resources/shared_resources_mod",
15 | "examples/shared_resources/shared_resources_protocol",
16 | ]
17 | resolver = "2"
18 |
19 | [profile.release-wasm]
20 | debug = false
21 | inherits = "release"
22 | lto = true
23 | opt-level = 's'
24 | panic = "abort"
25 | strip = true
26 |
--------------------------------------------------------------------------------
/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 | --- LLVM Exceptions to the Apache 2.0 License ----
179 |
180 | As an exception, if, as a result of your compiling your source code, portions
181 | of this Software are embedded into an Object form of such source code, you
182 | may redistribute such embedded portions in such Object form without complying
183 | with the conditions of Sections 4(a), 4(b) and 4(d) of the License.
184 |
185 | In addition, if you combine or link compiled forms of this Software with
186 | software that is licensed under the GPLv2 ("Combined Software") and if a
187 | court of competent jurisdiction determines that the patent provision (Section
188 | 3), the indemnity provision (Section 9) or other Section of the License
189 | conflicts with the conditions of the GPLv2, you may retroactively and
190 | prospectively choose to deem waived or otherwise exclude such Section(s) of
191 | the License, but only in their entirety and only with respect to the Combined
192 | Software.
193 |
--------------------------------------------------------------------------------
/LICENSE-MIT:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Bevy WASM
2 |
3 | Mod your Bevy games with WebAssembly!
4 |
5 | [](https://github.com/BrandonDyer64/bevy_wasm/actions)
6 | [](https://github.com/BrandonDyer64/bevy_wasm#license)
7 | [](https://crates.io/crates/bevy_wasm)
8 | [](https://crates.io/crates/bevy)
9 |
10 | | | | |
11 | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------- |
12 | | `bevy_wasm` | [](https://crates.io/crates/bevy_wasm) [](https://docs.rs/bevy_wasm) | For games |
13 | | `bevy_wasm_sys` | [](https://crates.io/crates/bevy_wasm_sys) [](https://docs.rs/bevy_wasm_sys) | For mods |
14 | | `bevy_wasm_shared` | [](https://crates.io/crates/bevy_wasm_shared) [](https://docs.rs/bevy_wasm_shared) | For protocols |
15 |
16 | See [examples/cubes](https://github.com/BrandonDyer64/bevy_wasm/tree/main/examples/cubes) for a comprehensive example of how to use this.
17 |
18 | [Changelog](https://github.com/BrandonDyer64/bevy_wasm/blob/main/CHANGELOG.md)
19 |
20 | ## Protocol
21 |
22 | Our protocol crate defines the two message types for communicating between the game and mods.
23 |
24 | ```toml
25 | [dependencies]
26 | bevy_wasm_shared = "0.10"
27 | serde = { version = "1.0", features = ["derive"] }
28 | ```
29 |
30 | ```rust
31 | use bevy_wasm_shared::prelude::*;
32 | use serde::{Deserialize, Serialize};
33 |
34 | /// The version of the protocol. Automatically set from the `CARGO_PKG_XXX` environment variables.
35 | pub const PROTOCOL_VERSION: Version = version!();
36 |
37 | /// A message to be sent Mod -> Game.
38 | #[derive(Debug, Clone, Serialize, Deserialize)]
39 | pub enum ModMessage {
40 | Hello,
41 | }
42 |
43 | /// A message to be sent Game -> Mod.
44 | #[derive(Debug, Clone, Serialize, Deserialize)]
45 | pub enum GameMessage {
46 | HiThere,
47 | }
48 | ```
49 |
50 | ## Game
51 |
52 | Our game will import `WasmPlugin` from [`bevy_wasm`](https://crates.io/crates/bevy_wasm), and use it to automatically send and receive messages with the mods.
53 |
54 | ```toml
55 | [dependencies]
56 | bevy = "0.10"
57 | bevy_wasm = "0.10"
58 | my_game_protocol = { git = "https://github.com/username/my_game_protocol" }
59 | ```
60 |
61 | ```rust
62 | use bevy::prelude::*;
63 | use bevy_wasm::prelude::*;
64 | use my_game_protocol::{GameMessage, ModMessage, PROTOCOL_VERSION};
65 |
66 | fn main() {
67 | App::new()
68 | .add_plugins(DefaultPlugins)
69 | .add_plugin(WasmPlugin::::new(PROTOCOL_VERSION))
70 | .add_startup_system(add_mods)
71 | .add_system(listen_for_mod_messages)
72 | .add_system(send_messages_to_mods)
73 | .run();
74 | }
75 |
76 | fn add_mods(mut commands: Commands, asset_server: Res) {
77 | commands.spawn(WasmMod {
78 | wasm: asset_server.load("some_mod.wasm"),
79 | });
80 | commands.spawn(WasmMod {
81 | wasm: asset_server.load("some_other_mod.wasm"),
82 | })
83 | }
84 |
85 | fn listen_for_mod_messages(mut events: EventReader) {
86 | for event in events.iter() {
87 | match event {
88 | ModMessage::Hello => {
89 | println!("The mod said hello!");
90 | }
91 | }
92 | }
93 | }
94 |
95 | fn send_messages_to_mods(mut events: EventWriter) {
96 | events.send(GameMessage::HiThere);
97 | }
98 | ```
99 |
100 | ## Mod
101 |
102 | Our mod will import `FFIPlugin` from [`bevy_wasm_sys`](https://crates.io/crates/bevy_wasm_sys), and use it to automatically send and receive messages with the game.
103 |
104 | ```toml
105 | [dependencies]
106 | bevy_wasm_sys = "0.10"
107 | my_game_protocol = { git = "https://github.com/username/my_game_protocol" }
108 | ```
109 |
110 | ```rust
111 | use bevy_wasm_sys::prelude::*;
112 | use my_game_protocol::{GameMessage, ModMessage, PROTOCOL_VERSION};
113 |
114 | #[no_mangle]
115 | pub unsafe extern "C" fn build_app() {
116 | App::new()
117 | .add_plugin(FFIPlugin::::new(PROTOCOL_VERSION))
118 | .add_system(listen_for_game_messages)
119 | .add_system(send_messages_to_game)
120 | .run();
121 | }
122 |
123 | fn listen_for_game_messages(mut events: EventReader) {
124 | for event in events.iter() {
125 | match event {
126 | GameMessage::HiThere => {
127 | println!("The game said hi there!");
128 | }
129 | }
130 | }
131 | }
132 |
133 | fn send_messages_to_game(mut events: EventWriter) {
134 | events.send(ModMessage::Hello);
135 | }
136 | ```
137 |
138 | ## Sharing Resources
139 |
140 | **Protocol:**
141 |
142 | ```rust
143 | #[derive(Resource, Default, Serialize, Deserialize)]
144 | pub struct MyResource {
145 | pub value: i32,
146 | }
147 | ```
148 |
149 | **Game:**
150 |
151 | ```rust
152 | App::new()
153 | ...
154 | .add_resource(MyResource { value: 0 })
155 | .add_plugin(
156 | WasmPlugin::::new(PROTOCOL_VERSION)
157 | .share_resource::()
158 | )
159 | .add_system(change_resource_value)
160 | ...
161 |
162 | fn change_resource_value(mut resource: ResMut) {
163 | resource.value += 1;
164 | }
165 | ```
166 |
167 | **Mod:**
168 |
169 | ```rust
170 | App::new()
171 | ...
172 | .add_plugin(FFIPlugin::::new(PROTOCOL_VERSION))
173 | .add_startup_system(setup)
174 | .add_system(print_resource_value)
175 | ...
176 |
177 | fn setup(mut extern_resource: ResMut) {
178 | extern_resources.insert::();
179 | }
180 |
181 | fn print_resource_value(resource: ExternRes) {
182 | println!("MyResource value: {}", resource.value);
183 | }
184 | ```
185 |
186 | See [examples/shared_resources](https://github.com/BrandonDyer64/bevy_wasm/tree/main/examples/shared_resources) for a full example.
187 |
188 | ## Roadmap
189 |
190 | | | |
191 | | --- | ------------------------------------------------ |
192 | | ✅ | wasmtime runtime in games |
193 | | ✅ | Send messages from mods to game |
194 | | ✅ | Send messages from game to mods |
195 | | ✅ | Multi-mod support |
196 | | ✅ | Time keeping |
197 | | ✅ | Protocol version checking |
198 | | ✅ | Extern Resource |
199 | | ✅ | Startup system mod loading |
200 | | ✅ | Direct update control |
201 | | ✅ | Mod unloading |
202 | | ✅ | Mod discrimination (events aren't broadcast all) |
203 | | ✅ | Browser support |
204 | | ⬜ | Extern Query |
205 | | ⬜ | Synced time |
206 | | ⬜ | Mod hotloading |
207 | | ⬜ | Automatic component syncing |
208 |
209 | ## License
210 |
211 | Bevy WASM is free, open source and permissively licensed!
212 | Except where noted (below and/or in individual files), all code in this repository is dual-licensed under either:
213 |
214 | - MIT License ([LICENSE-MIT](LICENSE-MIT) or [http://opensource.org/licenses/MIT](http://opensource.org/licenses/MIT))
215 | - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0))
216 |
217 | at your option.
218 | This means you can select the license you prefer!
219 | This dual-licensing approach is the de-facto standard in the Rust ecosystem and there are [very good reasons](https://github.com/bevyengine/bevy/issues/2373) to include both.
220 |
221 | ### Your contributions
222 |
223 | Unless you explicitly state otherwise,
224 | any contribution intentionally submitted for inclusion in the work by you,
225 | as defined in the Apache-2.0 license,
226 | shall be dual licensed as above,
227 | without any additional terms or conditions.
228 |
--------------------------------------------------------------------------------
/bevy_wasm/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | categories = ["wasm", "game-development"]
3 | description = "Run WASM systems in Bevy"
4 | edition = "2021"
5 | keywords = ["bevy", "wasm", "webassembly", "game", "gamedev"]
6 | license = "MIT OR Apache-2.0"
7 | name = "bevy_wasm"
8 | readme = "../README.md"
9 | repository = "https://github.com/BrandonDyer64/bevy_wasm"
10 | version = "0.10.1"
11 |
12 | [dependencies]
13 | anyhow = "1.0"
14 | bevy_wasm_shared = {path = "../bevy_wasm_shared", version = "0.10"}
15 | bincode = "1.3"
16 | colored = "2.0"
17 | serde = "1.0"
18 | tracing = "0.1"
19 |
20 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
21 | wasmtime = "5"
22 |
23 | [target.'cfg(target_arch = "wasm32")'.dependencies]
24 | js-sys = "0.3"
25 | wasm-bindgen = "0.2"
26 | web-sys = "0.3"
27 |
28 | [dependencies.bevy]
29 | default-features = false
30 | features = ["bevy_asset"]
31 | version = "0.10"
32 |
--------------------------------------------------------------------------------
/bevy_wasm/src/components/mod.rs:
--------------------------------------------------------------------------------
1 | //! Components
2 |
3 | pub use wasm_mod::WasmMod;
4 |
5 | mod wasm_mod;
6 |
--------------------------------------------------------------------------------
/bevy_wasm/src/components/wasm_mod.rs:
--------------------------------------------------------------------------------
1 | use bevy::prelude::*;
2 |
3 | use crate::wasm_asset::WasmAsset;
4 |
5 | /// The [`WasmMod`] component is used to spawn a new WebAssembly Mod into the world
6 | ///
7 | /// # Example
8 | ///
9 | /// ```
10 | /// commands.spawn(WasmMod {
11 | /// wasm: asset_server.load("my_mod.wasm"),
12 | /// });
13 | /// ```
14 | #[derive(Component)]
15 | pub struct WasmMod {
16 | /// Handle to the underlying WebAssembly binary
17 | pub wasm: Handle,
18 | }
19 |
--------------------------------------------------------------------------------
/bevy_wasm/src/lib.rs:
--------------------------------------------------------------------------------
1 | //! Mod Bevy games with WebAssembly
2 | //!
3 | //! See [examples/cubes](https://github.com/BrandonDyer64/bevy_wasm/tree/main/examples/cubes)
4 | //! for a comprehensive example of how to use this.
5 | //!
6 | //! For building mods, see the sister crate [bevy_wasm_sys](https://docs.rs/bevy_wasm_sys).
7 |
8 | #![deny(missing_docs)]
9 |
10 | use bevy::{prelude::Resource, reflect::TypeUuid};
11 | use serde::{de::DeserializeOwned, Serialize};
12 |
13 | pub mod components;
14 | mod mod_state;
15 | pub mod plugin;
16 | mod runtime;
17 | mod systems;
18 | mod wasm_asset;
19 |
20 | /// Any data type that can be used as a Host <-> Mod message
21 | ///
22 | /// Must be [`Clone`], [`Send`], and [`Sync`], and must be (de)serializable with serde.
23 | ///
24 | /// `bevy_wasm` uses `bincode` for serialization, so it's relatively fast.
25 | pub trait Message: Send + Sync + Serialize + DeserializeOwned + Clone + 'static {}
26 |
27 | impl Message for T where T: Send + Sync + Serialize + DeserializeOwned + Clone + 'static {}
28 |
29 | /// Any data type that can be used as a shared resource from Host to Mod
30 | ///
31 | /// Must be [`Clone`], [`Send`], [`Sync`], and [`TypeUuid`], and must be (de)serializable with serde.
32 | pub trait SharedResource: Resource + Serialize + DeserializeOwned + TypeUuid {}
33 |
34 | impl SharedResource for T where T: Resource + Serialize + DeserializeOwned + TypeUuid {}
35 |
36 | /// Convinience exports
37 | pub mod prelude {
38 | pub use crate::{components::*, plugin::WasmPlugin, Message};
39 | pub use bevy_wasm_shared::prelude::*;
40 | }
41 |
--------------------------------------------------------------------------------
/bevy_wasm/src/mod_state.rs:
--------------------------------------------------------------------------------
1 | use std::{collections::VecDeque, sync::Arc};
2 |
3 | use bevy::utils::{HashMap, Instant, Uuid};
4 |
5 | /// Internal mod state
6 | pub struct ModState {
7 | /// Time when the mod was loaded
8 | pub startup_time: Instant,
9 |
10 | /// Pointer given to us in `store_app`
11 | pub app_ptr: i32,
12 |
13 | /// Events that have been sent to the mod
14 | pub events_in: VecDeque>,
15 |
16 | /// Events that have been sent to the host
17 | pub events_out: Vec>,
18 |
19 | /// Resources that have changed since the last update
20 | pub shared_resource_values: HashMap>,
21 | }
22 |
--------------------------------------------------------------------------------
/bevy_wasm/src/plugin.rs:
--------------------------------------------------------------------------------
1 | //! Add this plugin to your Bevy app to enable WASM-based modding
2 |
3 | use bevy::prelude::*;
4 | use bevy_wasm_shared::prelude::*;
5 | use colored::*;
6 |
7 | use crate::{
8 | runtime::WasmRuntime,
9 | systems::{self, load_instances},
10 | wasm_asset::{WasmAsset, WasmAssetLoader},
11 | Message, SharedResource,
12 | };
13 |
14 | trait AddSystemToApp: Send + Sync + 'static {
15 | fn add_system_to_app(&self, app: &mut App);
16 | }
17 |
18 | struct ResourceUpdater {
19 | _r: std::marker::PhantomData,
20 | }
21 |
22 | impl AddSystemToApp for ResourceUpdater {
23 | fn add_system_to_app(&self, app: &mut App) {
24 | app.add_system(systems::update_shared_resource::);
25 | }
26 | }
27 |
28 | /// Add this plugin to your Bevy app to enable WASM-based modding
29 | ///
30 | /// Give [`WasmPlugin::new`] a list of wasm files to load at startup.
31 | /// Further mods can be added at any time with [`WasmMod::new()`].
32 | pub struct WasmPlugin
33 | where
34 | In: Message,
35 | Out: Message,
36 | {
37 | protocol_version: Version,
38 | shared_resources: Vec>,
39 | _in: std::marker::PhantomData,
40 | _out: std::marker::PhantomData,
41 | }
42 |
43 | impl WasmPlugin {
44 | /// Create a WasmPlugin with a list of wasm files to load at startup
45 | pub fn new(protocol_version: Version) -> Self {
46 | info!(
47 | "Starting {}{}{}{} {}{}{}{} with protocol version {}.{}.{}",
48 | "B".bold().red(),
49 | "E".bold().yellow(),
50 | "V".bold().green(),
51 | "Y".bold().cyan(),
52 | "W".bold().blue(),
53 | "A".bold().magenta(),
54 | "S".bold().red(),
55 | "M".bold().yellow(),
56 | protocol_version.major,
57 | protocol_version.minor,
58 | protocol_version.patch,
59 | );
60 | WasmPlugin {
61 | protocol_version,
62 | shared_resources: Vec::new(),
63 | _in: std::marker::PhantomData,
64 | _out: std::marker::PhantomData,
65 | }
66 | }
67 |
68 | /// Register a resource to be shared with mods. THIS SHOULD COME FROM YOUR PROTOCOL CRATE
69 | pub fn share_resource(mut self) -> Self {
70 | self.shared_resources.push(Box::new(ResourceUpdater:: {
71 | _r: std::marker::PhantomData,
72 | }));
73 | self
74 | }
75 | }
76 |
77 | impl Plugin for WasmPlugin {
78 | fn build(&self, app: &mut App) {
79 | let wasm_resource = WasmRuntime::new(self.protocol_version);
80 |
81 | app.insert_resource(wasm_resource)
82 | .add_asset::()
83 | .init_asset_loader::()
84 | .add_event::()
85 | .add_event::()
86 | .add_system(load_instances)
87 | .add_system(systems::tick_mods::);
88 |
89 | for system in self.shared_resources.iter() {
90 | system.add_system_to_app(app);
91 | }
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/bevy_wasm/src/runtime/mod.rs:
--------------------------------------------------------------------------------
1 | #[cfg(target_arch = "wasm32")]
2 | pub use web::{WasmInstance, WasmRuntime};
3 |
4 | #[cfg(not(target_arch = "wasm32"))]
5 | pub use native::{WasmInstance, WasmRuntime};
6 |
7 | #[cfg(target_arch = "wasm32")]
8 | pub mod web;
9 |
10 | #[cfg(not(target_arch = "wasm32"))]
11 | pub mod native;
12 |
--------------------------------------------------------------------------------
/bevy_wasm/src/runtime/native/linker.rs:
--------------------------------------------------------------------------------
1 | use std::time::Instant;
2 |
3 | use anyhow::Result;
4 | use bevy::{prelude::*, utils::Uuid};
5 | use bevy_wasm_shared::prelude::*;
6 | use colored::*;
7 | use wasmtime::*;
8 |
9 | use crate::mod_state::ModState;
10 |
11 | pub(crate) fn build_linker(engine: &Engine, protocol_version: Version) -> Result> {
12 | let mut linker: Linker = Linker::new(engine);
13 |
14 | linker.func_wrap(
15 | "host",
16 | "console_info",
17 | |mut caller: Caller<'_, ModState>, msg: i32, len: u32| {
18 | let mem = match caller.get_export("memory") {
19 | Some(Extern::Memory(mem)) => mem,
20 | _ => panic!("failed to find mod memory"),
21 | };
22 |
23 | let Some(data) = mem
24 | .data(&caller)
25 | .get(msg as u32 as usize..)
26 | .and_then(|arr| arr.get(..len as usize)) else {
27 | error!("Failed to get data from memory");
28 | return;
29 | };
30 |
31 | // SAFETY: We know that the memory is valid UTF-8 because it was written from a string in the mod
32 | let string = unsafe { std::str::from_utf8_unchecked(data) };
33 | info!(target: "MOD", "{}", string);
34 | },
35 | )?;
36 | linker.func_wrap(
37 | "host",
38 | "console_warn",
39 | |mut caller: Caller<'_, ModState>, msg: i32, len: u32| {
40 | let mem = match caller.get_export("memory") {
41 | Some(Extern::Memory(mem)) => mem,
42 | _ => panic!("failed to find mod memory"),
43 | };
44 |
45 | let Some(data) = mem
46 | .data(&caller)
47 | .get(msg as u32 as usize..)
48 | .and_then(|arr| arr.get(..len as usize)) else {
49 | error!("Failed to get data from memory");
50 | return;
51 | };
52 |
53 | // SAFETY: We know that the memory is valid UTF-8 because it was written from a string in the mod
54 | let string = unsafe { std::str::from_utf8_unchecked(data) };
55 | warn!(target: "MOD", "{}", string);
56 | },
57 | )?;
58 | linker.func_wrap(
59 | "host",
60 | "console_error",
61 | |mut caller: Caller<'_, ModState>, msg: i32, len: u32| {
62 | let mem = match caller.get_export("memory") {
63 | Some(Extern::Memory(mem)) => mem,
64 | _ => panic!("failed to find mod memory"),
65 | };
66 |
67 | let Some(data) = mem
68 | .data(&caller)
69 | .get(msg as u32 as usize..)
70 | .and_then(|arr| arr.get(..len as usize)) else {
71 | error!("Failed to get data from memory");
72 | return;
73 | };
74 |
75 | // SAFETY: We know that the memory is valid UTF-8 because it was written from a string in the mod
76 | let string = unsafe { std::str::from_utf8_unchecked(data) };
77 | error!(target: "MOD", "{}", string);
78 | },
79 | )?;
80 | linker.func_wrap(
81 | "host",
82 | "store_app",
83 | |mut caller: Caller<'_, ModState>, app_ptr: i32| {
84 | caller.data_mut().app_ptr = app_ptr;
85 | info!("{} 0x{:X}", "Storing app pointer:".italic(), app_ptr);
86 | },
87 | )?;
88 | linker.func_wrap(
89 | "host",
90 | "send_serialized_event",
91 | |mut caller: Caller<'_, ModState>, msg: i32, len: u32| {
92 | let mem = match caller.get_export("memory") {
93 | Some(Extern::Memory(mem)) => mem,
94 | _ => panic!("failed to find mod memory"),
95 | };
96 |
97 | let Some(data) = mem
98 | .data(&caller)
99 | .get(msg as u32 as usize..)
100 | .and_then(|arr| arr.get(..len as usize))
101 | .map(|x| x.into()) else {
102 | error!("Failed to get data from memory");
103 | return;
104 | };
105 |
106 | caller.data_mut().events_out.push(data);
107 | },
108 | )?;
109 | linker.func_wrap(
110 | "host",
111 | "get_next_event",
112 | |mut caller: Caller<'_, ModState>, arena: i32, len: u32| -> u32 {
113 | let mem = match caller.get_export("memory") {
114 | Some(Extern::Memory(mem)) => mem,
115 | _ => panic!("failed to find mod memory"),
116 | };
117 |
118 | let Some(serialized_event) = caller.data_mut().events_in.pop_front() else { return 0 };
119 |
120 | let Some(buffer) = mem
121 | .data_mut(&mut caller)
122 | .get_mut(arena as u32 as usize..)
123 | .and_then(|arr| arr.get_mut(..len as usize)) else {
124 | error!("Failed to get data from memory");
125 | return 0;
126 | };
127 |
128 | buffer[..serialized_event.len()].copy_from_slice(&serialized_event);
129 | serialized_event.len() as u32
130 | },
131 | )?;
132 | linker.func_wrap(
133 | "host",
134 | "get_resource",
135 | |mut caller: Caller<'_, ModState>,
136 | uuid_0: u64,
137 | uuid_1: u64,
138 | buffer: i32,
139 | buffer_len: u32|
140 | -> u32 {
141 | let mem = match caller.get_export("memory") {
142 | Some(Extern::Memory(mem)) => mem,
143 | _ => panic!("failed to find mod memory"),
144 | };
145 |
146 | let uuid = Uuid::from_u64_pair(uuid_0, uuid_1);
147 | let resource_bytes = caller.data_mut().shared_resource_values.remove(&uuid);
148 |
149 | let resource_bytes = match resource_bytes {
150 | Some(resource_bytes) => resource_bytes,
151 | None => return 0,
152 | };
153 |
154 | let Some(buffer) = mem
155 | .data_mut(&mut caller)
156 | .get_mut(buffer as u32 as usize..)
157 | .and_then(|arr| arr.get_mut(..buffer_len as usize)) else {
158 | error!("Failed to get data from memory");
159 | return 0;
160 | };
161 |
162 | buffer[..resource_bytes.len()].copy_from_slice(&resource_bytes);
163 | resource_bytes.len() as u32
164 | },
165 | )?;
166 | linker.func_wrap(
167 | "host",
168 | "get_time_since_startup",
169 | |caller: Caller<'_, ModState>| -> u64 {
170 | let startup_time = caller.data().startup_time;
171 | let delta = Instant::now() - startup_time;
172 | delta.as_nanos() as u64
173 | },
174 | )?;
175 | linker.func_wrap("host", "get_protocol_version", move || -> u64 {
176 | protocol_version.to_u64()
177 | })?;
178 |
179 | // Because bevy wants to use wasm-bindgen
180 | linker.func_wrap(
181 | "__wbindgen_placeholder__",
182 | "__wbindgen_describe",
183 | |v: i32| {
184 | info!("__wbindgen_describe: {}", v);
185 | },
186 | )?;
187 | linker.func_wrap(
188 | "__wbindgen_placeholder__",
189 | "__wbindgen_throw",
190 | |mut caller: Caller<'_, ModState>, msg: i32, len: i32| {
191 | let mem = match caller.get_export("memory") {
192 | Some(Extern::Memory(mem)) => mem,
193 | _ => panic!("failed to find mod memory"),
194 | };
195 |
196 | let Some(data) = mem
197 | .data(&caller)
198 | .get(msg as u32 as usize..)
199 | .and_then(|arr| arr.get(..len as usize)) else {
200 | error!("Failed to get data from memory");
201 | return;
202 | };
203 |
204 | // SAFETY: We know that the memory is valid UTF-8 because it was written from a string in the mod
205 | let string = unsafe { std::str::from_utf8_unchecked(data) };
206 | info!("{}", string);
207 | },
208 | )?;
209 | linker.func_wrap(
210 | "__wbindgen_externref_xform__",
211 | "__wbindgen_externref_table_grow",
212 | |v: i32| -> i32 {
213 | info!("__wbindgen_externref_table_grow: {}", v);
214 | 0
215 | },
216 | )?;
217 | linker.func_wrap(
218 | "__wbindgen_externref_xform__",
219 | "__wbindgen_externref_table_set_null",
220 | |v: i32| {
221 | info!("__wbindgen_externref_table_set_null: {}", v);
222 | },
223 | )?;
224 | Ok(linker)
225 | }
226 |
--------------------------------------------------------------------------------
/bevy_wasm/src/runtime/native/mod.rs:
--------------------------------------------------------------------------------
1 | use std::{collections::VecDeque, sync::Arc};
2 |
3 | use anyhow::{Context, Result};
4 | use bevy::{
5 | prelude::{Component, Resource},
6 | utils::{HashMap, Instant},
7 | };
8 | use bevy_wasm_shared::version::Version;
9 | use wasmtime::*;
10 |
11 | use crate::{mod_state::ModState, SharedResource};
12 |
13 | use self::linker::build_linker;
14 |
15 | mod linker;
16 |
17 | #[derive(Resource)]
18 | pub struct WasmRuntime {
19 | engine: Engine,
20 | protocol_version: Version,
21 | }
22 |
23 | impl WasmRuntime {
24 | pub fn new(protocol_version: Version) -> Self {
25 | Self {
26 | engine: Engine::default(),
27 | protocol_version,
28 | }
29 | }
30 |
31 | pub fn create_instance(&self, wasm_bytes: &[u8]) -> Result {
32 | // Create store and instance
33 | let module = Module::new(&self.engine, wasm_bytes)?;
34 | let mut store = Store::new(
35 | &self.engine,
36 | ModState {
37 | startup_time: Instant::now(),
38 | app_ptr: 0,
39 | events_out: Vec::new(),
40 | events_in: VecDeque::new(),
41 | shared_resource_values: HashMap::new(),
42 | },
43 | );
44 | let instance = build_linker(&self.engine, self.protocol_version)
45 | .context("Failed to build a linker for bevy_wasm")?
46 | .module(&mut store, "", &module)?
47 | .instantiate(&mut store, &module)?;
48 |
49 | // Call `extern "C" fn build_app`
50 | instance
51 | .get_typed_func::<(), ()>(&mut store, "build_app")?
52 | .call(&mut store, ())
53 | .context("Failed to call build_app")?;
54 |
55 | Ok(WasmInstance { instance, store })
56 | }
57 | }
58 |
59 | #[derive(Component)]
60 | pub struct WasmInstance {
61 | instance: Instance,
62 | store: Store,
63 | }
64 |
65 | impl WasmInstance {
66 | /// Tick the internal mod state
67 | pub(crate) fn tick(&mut self, events_in: &[Arc<[u8]>]) -> Result>> {
68 | for event in events_in.iter() {
69 | self.store.data_mut().events_in.push_back(event.clone());
70 | }
71 |
72 | let app_ptr = self.store.data().app_ptr;
73 |
74 | // Call `extern "C" fn update`
75 | self.instance
76 | .get_typed_func::(&mut self.store, "update")?
77 | .call(&mut self.store, app_ptr)
78 | .context("Failed to call update")?;
79 |
80 | let serialized_events_out = std::mem::take(&mut self.store.data_mut().events_out);
81 |
82 | Ok(serialized_events_out)
83 | }
84 |
85 | /// Update the value of a shared resource as seen by the mod
86 | pub fn update_resource_value(&mut self, bytes: Arc<[u8]>) {
87 | let state = self.store.data_mut();
88 |
89 | state.shared_resource_values.insert(T::TYPE_UUID, bytes);
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/bevy_wasm/src/runtime/web/linker.rs:
--------------------------------------------------------------------------------
1 | use std::sync::{Arc, RwLock};
2 |
3 | use bevy::{
4 | prelude::{error, info, warn},
5 | utils::Uuid,
6 | };
7 | use bevy_wasm_shared::version::Version;
8 | use colored::*;
9 | use js_sys::{Object, Reflect, Uint8Array, WebAssembly};
10 | use wasm_bindgen::{
11 | closure::{IntoWasmClosure, WasmClosure},
12 | prelude::{Closure, JsValue},
13 | };
14 |
15 | use crate::mod_state::ModState;
16 |
17 | fn link(target: &JsValue, name: &str, closure: impl IntoWasmClosure + 'static)
18 | where
19 | T: WasmClosure + ?Sized,
20 | {
21 | let closure = Closure::new(closure);
22 | Reflect::set(target, &JsValue::from_str(name), closure.as_ref()).unwrap();
23 | Box::leak(Box::new(closure)); // TODO: Don't just leak the closures.
24 | }
25 |
26 | #[allow(clippy::redundant_clone)]
27 | pub fn build_linker(
28 | protocol_version: Version,
29 | mod_state: Arc>,
30 | memory: Arc>>,
31 | ) -> Object {
32 | let host = Object::new();
33 |
34 | link::(&host, "console_info", {
35 | let memory = memory.clone();
36 | move |ptr, len| {
37 | if let Some(memory) = memory.read().unwrap().as_ref() {
38 | let buffer = Uint8Array::new(&memory.buffer())
39 | .slice(ptr as u32, ptr as u32 + len)
40 | .to_vec();
41 | let text = std::str::from_utf8(&buffer).unwrap();
42 | info!("MOD: {}", text);
43 | }
44 | }
45 | });
46 |
47 | link::(&host, "console_warn", {
48 | let memory = memory.clone();
49 | move |ptr, len| {
50 | if let Some(memory) = memory.read().unwrap().as_ref() {
51 | let buffer = Uint8Array::new(&memory.buffer())
52 | .slice(ptr as u32, ptr as u32 + len)
53 | .to_vec();
54 | let text = std::str::from_utf8(&buffer).unwrap();
55 | warn!("MOD: {}", text);
56 | }
57 | }
58 | });
59 |
60 | link::(&host, "console_error", {
61 | let memory = memory.clone();
62 | move |ptr, len| {
63 | if let Some(memory) = memory.read().unwrap().as_ref() {
64 | let buffer = Uint8Array::new(&memory.buffer())
65 | .slice(ptr as u32, ptr as u32 + len)
66 | .to_vec();
67 | let text = std::str::from_utf8(&buffer).unwrap();
68 | error!("MOD: {}", text);
69 | }
70 | }
71 | });
72 |
73 | link::(&host, "store_app", {
74 | let mod_state = mod_state.clone();
75 | move |ptr| {
76 | mod_state.write().unwrap().app_ptr = ptr;
77 | info!("{} 0x{:X}", "Storing app pointer:".italic(), ptr);
78 | }
79 | });
80 |
81 | link:: u64>(&host, "get_time_since_startup", {
82 | let mod_state = mod_state.clone();
83 | move || -> u64 { mod_state.read().unwrap().startup_time.elapsed().as_nanos() as u64 }
84 | });
85 |
86 | link:: u32>(&host, "get_next_event", {
87 | let mod_state = mod_state.clone();
88 | let memory = memory.clone();
89 | move |ptr: i32, len: u32| -> u32 {
90 | let next_event = mod_state.write().unwrap().events_in.pop_front();
91 | if let Some(next_event) = next_event {
92 | if next_event.len() > len as usize {
93 | error!("Serialized event is too long");
94 | return 0;
95 | }
96 | let arr = Uint8Array::from(&next_event[..]);
97 | if let Some(memory) = memory.read().unwrap().as_ref() {
98 | Uint8Array::new(&memory.buffer()).set(&arr, ptr as u32);
99 | next_event.len() as u32
100 | } else {
101 | 0
102 | }
103 | } else {
104 | 0
105 | }
106 | }
107 | });
108 |
109 | link::(&host, "send_serialized_event", {
110 | let mod_state = mod_state.clone();
111 | let memory = memory.clone();
112 | move |ptr, len| {
113 | if let Some(memory) = memory.read().unwrap().as_ref() {
114 | let buffer = Uint8Array::new(&memory.buffer())
115 | .slice(ptr as u32, ptr as u32 + len)
116 | .to_vec();
117 | mod_state.write().unwrap().events_out.push(buffer.into());
118 | }
119 | }
120 | });
121 |
122 | link:: u64>(&host, "get_protocol_version", {
123 | move || -> u64 { protocol_version.to_u64() }
124 | });
125 |
126 | link:: u32>(&host, "get_resource", {
127 | let mod_state = mod_state.clone();
128 | let memory = memory.clone();
129 | move |uuid_0, uuid_1, buffer_ptr, buffer_len| -> u32 {
130 | let uuid = Uuid::from_u64_pair(uuid_0, uuid_1);
131 | let resource_bytes = mod_state
132 | .write()
133 | .unwrap()
134 | .shared_resource_values
135 | .remove(&uuid);
136 | let Some(resource_bytes) = resource_bytes else { return 0 };
137 | if resource_bytes.len() > buffer_len as usize {
138 | error!("Serialized event is too long");
139 | return 0;
140 | }
141 | let arr = Uint8Array::from(&resource_bytes[..]);
142 | if let Some(memory) = memory.read().unwrap().as_ref() {
143 | Uint8Array::new(&memory.buffer()).set(&arr, buffer_ptr as u32);
144 | resource_bytes.len() as u32
145 | } else {
146 | 0
147 | }
148 | }
149 | });
150 |
151 | // __wbindgen_placeholder__
152 | let wbp = Object::new();
153 |
154 | link::(&wbp, "__wbindgen_describe", {
155 | move |v| {
156 | info!("__wbindgen_describe: {}", v);
157 | }
158 | });
159 |
160 | link::(&wbp, "__wbindgen_throw", {
161 | move |msg, len| {
162 | info!("__wbindgen_throw: {} {}", msg, len);
163 | }
164 | });
165 |
166 | // __wbindgen_externref_xform__
167 | let wbxf = Object::new();
168 |
169 | link:: i32>(&wbxf, "__wbindgen_externref_table_grow", {
170 | move |v| -> i32 {
171 | info!("__wbindgen_externref_table_grow: {}", v);
172 | 0
173 | }
174 | });
175 |
176 | link::(&wbxf, "__wbindgen_externref_table_set_null", {
177 | move |v| {
178 | info!("__wbindgen_externref_table_set_null: {}", v);
179 | }
180 | });
181 |
182 | let imports = Object::new();
183 | Reflect::set(
184 | &imports,
185 | &JsValue::from_str("__wbindgen_placeholder__"),
186 | &wbp,
187 | )
188 | .unwrap();
189 | Reflect::set(
190 | &imports,
191 | &JsValue::from_str("__wbindgen_externref_xform__"),
192 | &wbxf,
193 | )
194 | .unwrap();
195 | Reflect::set(&imports, &JsValue::from_str("host"), &host).unwrap();
196 | imports
197 | }
198 |
--------------------------------------------------------------------------------
/bevy_wasm/src/runtime/web/mod.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | collections::VecDeque,
3 | sync::{Arc, RwLock},
4 | };
5 |
6 | use anyhow::Result;
7 | use bevy::{
8 | prelude::{Component, Resource},
9 | utils::{HashMap, Instant},
10 | };
11 | use js_sys::{
12 | Function, Reflect,
13 | WebAssembly::{self, Instance},
14 | };
15 | use wasm_bindgen::{prelude::Closure, JsCast, JsValue};
16 |
17 | use bevy_wasm_shared::version::Version;
18 | use web_sys::console;
19 |
20 | use crate::{mod_state::ModState, SharedResource};
21 |
22 | use self::linker::build_linker;
23 |
24 | mod linker;
25 |
26 | #[derive(Resource)]
27 | pub struct WasmRuntime {
28 | protocol_version: Version,
29 | }
30 |
31 | impl WasmRuntime {
32 | pub fn new(protocol_version: Version) -> Self {
33 | Self { protocol_version }
34 | }
35 |
36 | pub fn create_instance(&self, wasm_bytes: &[u8]) -> Result {
37 | let memory = Arc::new(RwLock::new(None));
38 | let mod_state = Arc::new(RwLock::new(ModState {
39 | startup_time: Instant::now(),
40 | app_ptr: 0,
41 | events_in: VecDeque::new(),
42 | events_out: Vec::new(),
43 | shared_resource_values: HashMap::new(),
44 | }));
45 | let imports = build_linker(self.protocol_version, mod_state.clone(), memory.clone());
46 | let promise = WebAssembly::instantiate_buffer(wasm_bytes, &imports);
47 | let instance = Arc::new(RwLock::new(None));
48 | let then = Closure::new({
49 | let instance = instance.clone();
50 | move |value| {
51 | let instance_value: WebAssembly::Instance =
52 | Reflect::get(&value, &"instance".into())
53 | .and_then(|x| x.dyn_into())
54 | .unwrap();
55 | let exports = instance_value.exports();
56 | let memory_value: WebAssembly::Memory = Reflect::get(&exports, &"memory".into())
57 | .and_then(|x| x.dyn_into())
58 | .unwrap();
59 | let build_app: Function = Reflect::get(exports.as_ref(), &"build_app".into())
60 | .and_then(|x| x.dyn_into())
61 | .expect("build_app export wasn't a function");
62 | *instance.write().unwrap() = Some(instance_value);
63 | *memory.write().unwrap() = Some(memory_value);
64 | build_app.call0(&JsValue::undefined()).unwrap();
65 | }
66 | });
67 | let catch = Closure::new({
68 | move |value| {
69 | console::warn_1(&value);
70 | }
71 | });
72 | _ = promise.then(&then).catch(&catch);
73 | Ok(WasmInstance {
74 | instance,
75 | mod_state,
76 | _then: then,
77 | _catch: catch,
78 | })
79 | }
80 | }
81 |
82 | #[derive(Component)]
83 | pub struct WasmInstance {
84 | instance: Arc>>,
85 | mod_state: Arc>,
86 | _then: Closure,
87 | _catch: Closure,
88 | }
89 |
90 | unsafe impl Send for WasmInstance {}
91 | unsafe impl Sync for WasmInstance {}
92 |
93 | impl WasmInstance {
94 | pub fn tick(&mut self, events_in: &[Arc<[u8]>]) -> Result>> {
95 | let Some(instance) = self.instance.read().unwrap().clone() else { return Ok(Vec::new()) };
96 | for event in events_in.iter() {
97 | self.mod_state
98 | .write()
99 | .unwrap()
100 | .events_in
101 | .push_back(event.clone());
102 | }
103 |
104 | let app_ptr = self.mod_state.read().unwrap().app_ptr;
105 |
106 | let exports = instance.exports();
107 |
108 | let update: Function = Reflect::get(exports.as_ref(), &"update".into())
109 | .and_then(|x| x.dyn_into())
110 | .expect("build_app export wasn't a function");
111 | match update.call1(&JsValue::undefined(), &JsValue::from_f64(app_ptr as f64)) {
112 | Ok(_) => {}
113 | Err(e) => console::error_1(&e),
114 | }
115 |
116 | let serialized_events_out = std::mem::take(&mut self.mod_state.write().unwrap().events_out);
117 |
118 | Ok(serialized_events_out)
119 | }
120 |
121 | pub fn update_resource_value(&mut self, bytes: Arc<[u8]>) {
122 | self.mod_state
123 | .write()
124 | .unwrap()
125 | .shared_resource_values
126 | .insert(T::TYPE_UUID, bytes);
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/bevy_wasm/src/systems/load_instances.rs:
--------------------------------------------------------------------------------
1 | use bevy::prelude::*;
2 |
3 | use crate::{
4 | components::WasmMod,
5 | runtime::{WasmInstance, WasmRuntime},
6 | wasm_asset::WasmAsset,
7 | };
8 |
9 | pub fn load_instances(
10 | mut commands: Commands,
11 | wasm_assets: Res>,
12 | mods_to_load: Query<(Entity, &WasmMod), Without>,
13 | wasm_runtime: Res,
14 | ) {
15 | for (entity, mod_to_load) in mods_to_load.iter() {
16 | if let Some(wasm_asset) = wasm_assets.get(&mod_to_load.wasm) {
17 | let instance = wasm_runtime.create_instance(&wasm_asset.bytes);
18 | match instance {
19 | Ok(instance) => {
20 | commands.entity(entity).insert(instance);
21 | }
22 | Err(e) => {
23 | error!("Could not initialize WASM instance: {}", e);
24 | commands.entity(entity).despawn();
25 | }
26 | }
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/bevy_wasm/src/systems/mod.rs:
--------------------------------------------------------------------------------
1 | pub use load_instances::load_instances;
2 | pub use tick_mods::tick_mods;
3 | pub use update_shared_resource::update_shared_resource;
4 |
5 | mod load_instances;
6 | mod tick_mods;
7 | mod update_shared_resource;
8 |
--------------------------------------------------------------------------------
/bevy_wasm/src/systems/tick_mods.rs:
--------------------------------------------------------------------------------
1 | use std::sync::Arc;
2 |
3 | use bevy::prelude::*;
4 |
5 | use crate::{runtime::WasmInstance, Message};
6 |
7 | pub fn tick_mods(
8 | mut events_in: EventReader,
9 | mut events_out: EventWriter,
10 | mut wasm_mods: Query<&mut WasmInstance>,
11 | ) {
12 | let serialized_events_in: Vec> = events_in
13 | .iter()
14 | .flat_map(|event| bincode::serialize(event))
15 | .map(|bytes| bytes.into())
16 | .collect();
17 |
18 | for mut wasm_mod in wasm_mods.iter_mut() {
19 | let serialized_events_out = match wasm_mod.tick(serialized_events_in.as_slice()) {
20 | Ok(events) => events,
21 | Err(err) => {
22 | error!("Error while ticking mod: {}", err);
23 | continue;
24 | }
25 | };
26 |
27 | for serialized_event_out in serialized_events_out {
28 | match bincode::deserialize(&serialized_event_out) {
29 | Ok(event_out) => events_out.send(event_out),
30 | Err(err) => error!("Error while deserializing event: {}", err),
31 | }
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/bevy_wasm/src/systems/update_shared_resource.rs:
--------------------------------------------------------------------------------
1 | use std::{ops::Deref, sync::Arc};
2 |
3 | use bevy::prelude::*;
4 |
5 | use crate::{runtime::WasmInstance, SharedResource};
6 |
7 | pub fn update_shared_resource(
8 | res: Res,
9 | mut wasm_mods: Query<&mut WasmInstance>,
10 | ) {
11 | if res.is_changed() {
12 | let v: &T = res.deref();
13 | let resource_bytes: Arc<[u8]> = match bincode::serialize(v) {
14 | Ok(bytes) => bytes.into(),
15 | Err(err) => {
16 | error!("Error while serializing resource: {}", err);
17 | return;
18 | }
19 | };
20 |
21 | for mut wasm_mod in wasm_mods.iter_mut() {
22 | wasm_mod.update_resource_value::(resource_bytes.clone());
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/bevy_wasm/src/wasm_asset.rs:
--------------------------------------------------------------------------------
1 | //! Implements loader for a custom asset type.
2 |
3 | use bevy::{
4 | asset::{AssetLoader, LoadContext, LoadedAsset},
5 | reflect::TypeUuid,
6 | utils::BoxedFuture,
7 | };
8 | use serde::Deserialize;
9 |
10 | #[derive(Debug, Deserialize, TypeUuid)]
11 | #[uuid = "4e2a45df-246a-4ab8-91ac-c24218d6a79d"]
12 | pub struct WasmAsset {
13 | pub bytes: Vec,
14 | }
15 |
16 | #[derive(Default)]
17 | pub struct WasmAssetLoader;
18 |
19 | impl AssetLoader for WasmAssetLoader {
20 | fn load<'a>(
21 | &'a self,
22 | bytes: &'a [u8],
23 | load_context: &'a mut LoadContext,
24 | ) -> BoxedFuture<'a, Result<(), bevy::asset::Error>> {
25 | Box::pin(async move {
26 | load_context.set_default_asset(LoadedAsset::new(WasmAsset {
27 | bytes: bytes.into(),
28 | }));
29 | Ok(())
30 | })
31 | }
32 |
33 | fn extensions(&self) -> &[&str] {
34 | &["wasm"]
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/bevy_wasm_shared/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | categories = ["wasm", "game-development"]
3 | description = "Run WASM systems in Bevy"
4 | edition = "2021"
5 | keywords = ["bevy", "wasm", "webassembly", "game", "gamedev"]
6 | license = "MIT OR Apache-2.0"
7 | name = "bevy_wasm_shared"
8 | readme = "../README.md"
9 | repository = "https://github.com/BrandonDyer64/bevy_wasm"
10 | version = "0.10.1"
11 |
12 | [dependencies]
13 |
--------------------------------------------------------------------------------
/bevy_wasm_shared/src/lib.rs:
--------------------------------------------------------------------------------
1 | //! This crate provides the shared code between the bevy_wasm and bevy_wasm_sys.
2 | //!
3 | //! Use this for your protocol crate.
4 |
5 | #![deny(missing_docs)]
6 |
7 | pub mod version;
8 |
9 | /// Convenience re-exports
10 | pub mod prelude {
11 | pub use crate::version;
12 | pub use crate::version::Version;
13 | }
14 |
--------------------------------------------------------------------------------
/bevy_wasm_shared/src/version.rs:
--------------------------------------------------------------------------------
1 | //! Versioning utils
2 |
3 | /// The version of the game's protocol.
4 | ///
5 | /// Used to ensure that the mod and the game are using the same protocol.
6 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
7 | pub struct Version {
8 | /// The hash of the protocol name. Not perfect, but should ensure mods aren't used with the wrong game.
9 | pub name_hash: u16,
10 |
11 | /// The major version of the protocol.
12 | pub major: u16,
13 |
14 | /// The minor version of the protocol.
15 | pub minor: u16,
16 |
17 | /// The patch version of the protocol.
18 | pub patch: u16,
19 | }
20 |
21 | impl Version {
22 | /// Convert the Version into a u64.
23 | pub fn to_u64(&self) -> u64 {
24 | let mut result: u64 = 0;
25 | result |= self.name_hash as u64;
26 | result |= (self.major as u64) << 16;
27 | result |= (self.minor as u64) << 32;
28 | result |= (self.patch as u64) << 48;
29 | result
30 | }
31 |
32 | /// Convert a u64 into a Version.
33 | pub fn from_u64(version: u64) -> Self {
34 | Self {
35 | name_hash: (version & 0xFFFF) as u16,
36 | major: ((version >> 16) & 0xFFFF) as u16,
37 | minor: ((version >> 32) & 0xFFFF) as u16,
38 | patch: ((version >> 48) & 0xFFFF) as u16,
39 | }
40 | }
41 | }
42 |
43 | /// Generate a new Version from the current crate's version.
44 | #[macro_export]
45 | macro_rules! version(
46 | () => (
47 | Version {
48 | name_hash: $crate::version::__str_hash(env!("CARGO_PKG_NAME")),
49 | major: $crate::version::__str_to_u16(env!("CARGO_PKG_VERSION_MAJOR")),
50 | minor: $crate::version::__str_to_u16(env!("CARGO_PKG_VERSION_MINOR")),
51 | patch: $crate::version::__str_to_u16(env!("CARGO_PKG_VERSION_PATCH")),
52 | }
53 | )
54 | );
55 |
56 | #[doc(hidden)]
57 | pub const fn __str_to_u16(s: &str) -> u16 {
58 | let mut result: u16 = 0;
59 | let s = s.as_bytes();
60 | let mut i = 0;
61 | while i < s.len() {
62 | result += (s[s.len() - i - 1] as u16 - 48) * 10u16.pow(i as u32);
63 | i += 1;
64 | }
65 | result
66 | }
67 |
68 | #[doc(hidden)]
69 | pub const fn __str_hash(s: &str) -> u16 {
70 | // Based on Jenkins' one-at-a-time hash.
71 | let mut result: u16 = 0;
72 | let s = s.as_bytes();
73 | let mut i = 0;
74 | while i < s.len() {
75 | result = result.wrapping_add(s[i] as u16);
76 | result = result.wrapping_add(result << 10);
77 | result ^= result >> 6;
78 | i += 1;
79 | }
80 |
81 | result = result.wrapping_add(result << 3);
82 | result ^= result >> 11;
83 | result = result.wrapping_add(result << 15);
84 |
85 | result
86 | }
87 |
--------------------------------------------------------------------------------
/bevy_wasm_sys/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | categories = ["wasm", "game-development"]
3 | description = "Import into your wasm scripts. Use with the bevy_wasm crate."
4 | edition = "2021"
5 | keywords = ["bevy", "wasm", "webassembly", "game", "gamedev"]
6 | license = "MIT OR Apache-2.0"
7 | name = "bevy_wasm_sys"
8 | readme = "../README.md"
9 | repository = "https://github.com/BrandonDyer64/bevy_wasm"
10 | version = "0.10.1"
11 |
12 | [features]
13 | bevy = ["bevy_app", "bevy_derive", "bevy_ecs", "bevy_math", "bevy_reflect", "bevy_transform"]
14 | default = ["bevy"]
15 |
16 | [dependencies]
17 | bevy_app = {version = "0.10", optional = true}
18 | bevy_derive = {version = "0.10", optional = true}
19 | bevy_ecs = {version = "0.10", optional = true}
20 | bevy_math = {version = "0.10", optional = true}
21 | bevy_reflect = {version = "0.10", optional = true}
22 | bevy_transform = {version = "0.10", optional = true, features = ["serialize"]}
23 | bevy_wasm_shared = {path = "../bevy_wasm_shared", version = "0.10"}
24 | bincode = "1.3"
25 | serde = "1.0"
26 |
--------------------------------------------------------------------------------
/bevy_wasm_sys/src/ecs/extern_res.rs:
--------------------------------------------------------------------------------
1 | //! Access host `Resource`s from inside of a WASM system
2 |
3 | use std::{
4 | any::{Any, TypeId},
5 | collections::HashMap,
6 | fmt::Debug,
7 | marker::PhantomData,
8 | ops::Deref,
9 | };
10 |
11 | use bevy_ecs::{prelude::*, system::SystemParam};
12 | use bevy_reflect::TypeUuid;
13 | use serde::{de::DeserializeOwned, Serialize};
14 |
15 | use crate::error;
16 |
17 | /// A resource that can be shared from the Host
18 | pub trait SharedResource: Resource + Default + Serialize + DeserializeOwned + TypeUuid {}
19 |
20 | impl SharedResource for T {}
21 |
22 | /// Get the value of a resource from the host
23 | pub fn get_resource() -> Option {
24 | let (uuid_0, uuid_1) = T::TYPE_UUID.as_u64_pair();
25 |
26 | let mut buffer = [0; 1024];
27 |
28 | let len = unsafe {
29 | // put serialized resource into buffer
30 | crate::ffi::get_resource(uuid_0, uuid_1, buffer.as_mut_ptr(), buffer.len())
31 | };
32 |
33 | if len == 0 {
34 | return None;
35 | }
36 |
37 | if len > buffer.len() {
38 | error!("Serialized resource is larger than buffer");
39 | return None;
40 | }
41 |
42 | let resource_bytes = &buffer[..len];
43 |
44 | match bincode::deserialize(resource_bytes) {
45 | Ok(resource) => Some(resource),
46 | Err(err) => {
47 | error!("Failed to deserialize resource from host: {}", err);
48 | None
49 | }
50 | }
51 | }
52 |
53 | trait AsAny {
54 | fn as_any(&self) -> &dyn Any;
55 | }
56 |
57 | impl AsAny for T {
58 | fn as_any(&self) -> &dyn Any {
59 | self
60 | }
61 | }
62 |
63 | trait AnyResource: AsAny + Any + Resource + Send + Sync + 'static {}
64 |
65 | impl AnyResource for T {}
66 |
67 | impl dyn AnyResource {
68 | fn downcast_ref(&self) -> Option<&T> {
69 | self.as_any().downcast_ref::()
70 | }
71 | }
72 |
73 | trait ResourceFetch: Send + Sync {
74 | fn fetch(&mut self) -> Option>;
75 | }
76 |
77 | struct ExternResourceFetchImpl(PhantomData);
78 |
79 | impl ResourceFetch for ExternResourceFetchImpl {
80 | fn fetch(&mut self) -> Option> {
81 | Some(Box::new(get_resource::()?))
82 | }
83 | }
84 |
85 | struct ExternResourceValue {
86 | value: Box,
87 | fetcher: Box,
88 | }
89 |
90 | impl ExternResourceValue {
91 | pub fn init() -> Self {
92 | Self {
93 | value: match get_resource::() {
94 | Some(v) => Box::new(v),
95 | None => Box::::default(),
96 | },
97 | fetcher: Box::new(ExternResourceFetchImpl::(PhantomData)),
98 | }
99 | }
100 |
101 | pub fn fetch(&mut self) {
102 | if let Some(new_value) = self.fetcher.fetch() {
103 | self.value = new_value;
104 | }
105 | }
106 |
107 | pub fn downcast_ref(&self) -> Option<&T> {
108 | let boxed = self.value.as_ref();
109 | (boxed as &(dyn AnyResource + 'static)).downcast_ref::()
110 | }
111 | }
112 |
113 | #[doc(hidden)]
114 | #[derive(Resource)]
115 | pub struct ExternResources {
116 | resources: HashMap,
117 | }
118 |
119 | impl Debug for ExternResources {
120 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121 | let mut debug = f.debug_map();
122 | for type_id in self.resources.keys() {
123 | debug.entry(&type_id, &());
124 | }
125 | debug.finish()
126 | }
127 | }
128 |
129 | impl ExternResources {
130 | pub fn new() -> Self {
131 | Self {
132 | resources: HashMap::new(),
133 | }
134 | }
135 |
136 | pub fn insert(&mut self) {
137 | self.resources
138 | .insert(TypeId::of::(), ExternResourceValue::init::());
139 | }
140 |
141 | pub fn fetch_all(&mut self) {
142 | for resource_value in self.resources.values_mut() {
143 | resource_value.fetch();
144 | }
145 | }
146 |
147 | pub fn get(&self) -> Option<&T> {
148 | self.resources.get(&TypeId::of::())?.downcast_ref()
149 | }
150 | }
151 |
152 | impl Default for ExternResources {
153 | fn default() -> Self {
154 | Self::new()
155 | }
156 | }
157 |
158 | /// Use a resource from the host game
159 | #[derive(SystemParam)]
160 | pub struct ExternRes<'w, 's, T: Resource + Serialize + DeserializeOwned> {
161 | res: Res<'w, ExternResources>,
162 | #[system_param(ignore)]
163 | t: PhantomData,
164 | #[system_param(ignore)]
165 | marker: PhantomData<&'s ()>,
166 | }
167 |
168 | impl<'w, 's, T: Debug + Resource + Serialize + DeserializeOwned> Debug for ExternRes<'w, 's, T> {
169 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170 | self.deref().fmt(f)
171 | }
172 | }
173 |
174 | impl<'w, 's, T: Resource + Serialize + DeserializeOwned> ExternRes<'w, 's, T> {
175 | /// Get the resource
176 | pub fn get(&self) -> Option<&T> {
177 | self.res.get::()
178 | }
179 | }
180 |
181 | impl<'w, 's, T: Resource + Serialize + DeserializeOwned> Deref for ExternRes<'w, 's, T> {
182 | type Target = T;
183 |
184 | fn deref(&self) -> &Self::Target {
185 | match self.get() {
186 | Some(v) => v,
187 | None => {
188 | error!(
189 | "FATAL: Resource was not shared with mod: {}",
190 | std::any::type_name::()
191 | );
192 | panic!();
193 | }
194 | }
195 | }
196 | }
197 |
198 | /// Convenience re-exports
199 | pub mod prelude {
200 | pub use super::ExternRes;
201 | }
202 |
--------------------------------------------------------------------------------
/bevy_wasm_sys/src/ecs/mod.rs:
--------------------------------------------------------------------------------
1 | //! ECS types
2 |
3 | pub mod extern_res;
4 |
5 | /// Convenience re-exports
6 | pub mod prelude {
7 | pub use super::extern_res::prelude::*;
8 | }
9 |
--------------------------------------------------------------------------------
/bevy_wasm_sys/src/events.rs:
--------------------------------------------------------------------------------
1 | //! Event functions. [`send_event`] and [`get_next_event`]
2 |
3 | use serde::{de::DeserializeOwned, Serialize};
4 |
5 | use crate::error;
6 |
7 | /// Send an event to the host.
8 | pub fn send_event(event: &T) {
9 | let encoded: Vec = match bincode::serialize(&event) {
10 | Ok(encoded) => encoded,
11 | Err(err) => {
12 | error!("Failed to serialize event: {}", err);
13 | return;
14 | }
15 | };
16 |
17 | unsafe {
18 | crate::ffi::send_serialized_event(encoded.as_ptr(), encoded.len());
19 | }
20 |
21 | std::mem::drop(encoded);
22 | }
23 |
24 | /// Get the next event from the host.
25 | pub fn get_next_event() -> Option {
26 | let mut buffer = [0; 1024];
27 |
28 | let len = unsafe { crate::ffi::get_next_event(buffer.as_mut_ptr(), buffer.len()) };
29 |
30 | if len == 0 {
31 | return None;
32 | }
33 |
34 | if len > buffer.len() {
35 | error!("Serialized event is larger than buffer");
36 | return None;
37 | }
38 |
39 | match bincode::deserialize(&buffer[..len]) {
40 | Ok(event) => Some(event),
41 | Err(err) => {
42 | error!("Failed to deserialize event from host: {}", err);
43 | None
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/bevy_wasm_sys/src/ffi.rs:
--------------------------------------------------------------------------------
1 | //! FFI declarations for communicating with the host.
2 |
3 | #![allow(missing_docs)]
4 |
5 | use std::ffi::c_void;
6 |
7 | #[cfg(feature = "bevy")]
8 | use bevy_app::App;
9 |
10 | #[link(wasm_import_module = "host")]
11 | extern "C" {
12 | pub fn store_app(app: *const c_void);
13 | pub fn console_info(msg: *const u8, len: usize);
14 | pub fn console_warn(msg: *const u8, len: usize);
15 | pub fn console_error(msg: *const u8, len: usize);
16 | pub fn send_serialized_event(event: *const u8, len: usize);
17 | pub fn get_next_event(event: *const u8, len: usize) -> usize;
18 | /// Nanoseconds since the mod was loaded
19 | pub fn get_time_since_startup() -> u64;
20 | pub fn get_protocol_version() -> u64;
21 | pub fn get_resource(uuid_0: u64, uuid_1: u64, buffer: *const u8, buffer_len: usize) -> usize;
22 | }
23 |
24 | /// This function is called by the host every frame.
25 | ///
26 | /// # Safety
27 | ///
28 | /// `app` is assumed to be a valid pointer to an [`App`].
29 | #[cfg(feature = "bevy")]
30 | #[no_mangle]
31 | pub unsafe extern "C" fn update(app: *mut c_void) {
32 | if app.is_null() {
33 | return;
34 | }
35 |
36 | let app = app as *mut App;
37 | (*app).update();
38 | }
39 |
--------------------------------------------------------------------------------
/bevy_wasm_sys/src/ffi_plugin.rs:
--------------------------------------------------------------------------------
1 | //! The plugin for your mod
2 |
3 | use std::ffi::c_void;
4 |
5 | use bevy_app::{App, Plugin};
6 | use bevy_ecs::{
7 | prelude::{EventReader, EventWriter},
8 | system::ResMut,
9 | };
10 | use bevy_wasm_shared::prelude::*;
11 | use serde::{de::DeserializeOwned, Serialize};
12 |
13 | use crate::{
14 | ecs::extern_res::ExternResources,
15 | error,
16 | events::{get_next_event, send_event},
17 | ffi::store_app,
18 | info,
19 | time::Time,
20 | };
21 |
22 | /// An object that can be used as a message
23 | pub trait Message: Send + Sync + Serialize + DeserializeOwned + 'static {}
24 |
25 | impl Message for T where T: Send + Sync + Serialize + DeserializeOwned + 'static {}
26 |
27 | /// Use this plugin in your app to enable communication with the host
28 | ///
29 | /// Necessary for modding support
30 | ///
31 | /// - In: Message type going from host to mod
32 | /// - Out: Message type going from mod to host
33 | pub struct FFIPlugin {
34 | protocol_version: Version,
35 | protocol_version_checker: Box bool + Send + Sync + 'static>,
36 | _in: std::marker::PhantomData,
37 | _out: std::marker::PhantomData,
38 | }
39 |
40 | impl FFIPlugin {
41 | /// Create a new FFIPlugin instance to insert into a Bevy `App`
42 | pub fn new(protocol_version: Version) -> Self {
43 | info!(
44 | "Starting mod with protocol version {}.{}.{}",
45 | protocol_version.major, protocol_version.minor, protocol_version.patch
46 | );
47 | Self {
48 | protocol_version,
49 | protocol_version_checker: Box::new(|host_version, mod_version| {
50 | // Check that the names match
51 | host_version.name_hash == mod_version.name_hash
52 | // Check that the major versions match
53 | && host_version.major == mod_version.major
54 | }),
55 | _in: std::marker::PhantomData,
56 | _out: std::marker::PhantomData,
57 | }
58 | }
59 |
60 | /// Set a custom protocol version checker to ensure your mod is compatible with the game
61 | ///
62 | /// The default version checker only ensures the major versions match.
63 | ///
64 | /// i.e. `1.0.0` is compatible with `1.1.0` but not `2.0.0`
65 | pub fn with_version_checker(self, checker: F) -> Self
66 | where
67 | F: Fn(Version, Version) -> bool + Send + Sync + 'static,
68 | {
69 | Self {
70 | protocol_version_checker: Box::new(checker),
71 | ..self
72 | }
73 | }
74 | }
75 |
76 | impl Plugin for FFIPlugin {
77 | fn build(&self, app: &mut App) {
78 | let host_version = unsafe { crate::ffi::get_protocol_version() };
79 | let host_version = Version::from_u64(host_version);
80 | if !(*self.protocol_version_checker)(self.protocol_version, host_version) {
81 | error!(
82 | "Protocol version incompatible! Host: {}.{}.{}, Mod: {}.{}.{}",
83 | host_version.major,
84 | host_version.minor,
85 | host_version.patch,
86 | self.protocol_version.major,
87 | self.protocol_version.minor,
88 | self.protocol_version.patch
89 | );
90 | return;
91 | }
92 | app.set_runner(app_runner)
93 | .add_event::()
94 | .add_event::()
95 | .insert_resource(Time::new())
96 | .insert_resource(ExternResources::new())
97 | .add_system(update_time)
98 | .add_system(fetch_resources)
99 | .add_system(event_listener::)
100 | .add_system(event_sender::);
101 | // .add_system_to_stage(CoreStage::First, update_time.at_start())
102 | // .add_system_to_stage(CoreStage::PreUpdate, fetch_resources)
103 | // .add_system_to_stage(CoreStage::PreUpdate, event_listener::)
104 | // .add_system_to_stage(CoreStage::PostUpdate, event_sender::);
105 | }
106 | }
107 |
108 | fn fetch_resources(mut resources: ResMut) {
109 | resources.fetch_all();
110 | }
111 |
112 | fn event_listener(mut events: EventWriter) {
113 | while let Some(event) = get_next_event() {
114 | events.send(event);
115 | }
116 | }
117 |
118 | fn event_sender(mut events: EventReader) {
119 | for event in events.iter() {
120 | send_event(event);
121 | }
122 | }
123 |
124 | fn app_runner(app: App) {
125 | let app = Box::new(app);
126 | let app_ptr = Box::into_raw(app);
127 | let app_ptr = app_ptr as *const c_void;
128 | unsafe { store_app(app_ptr) };
129 | }
130 |
131 | fn update_time(mut time: ResMut