diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c0ba960 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = false +insert_final_newline = false + +[*.ts] +indent_size = 2 \ No newline at end of file diff --git a/.gitignore b/.gitignore index ea8c4bf..fac7122 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +/web-target \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 2537cf7..66708ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -486,11 +486,22 @@ dependencies = [ "bevy_ecs_dynamic", "bevy_reflect", "bevy_reflect_fns", - "deno_ast", "deno_core", "fixedbitset", + "js-sys", "serde", + "serde-wasm-bindgen", "serde_json", + "slotmap", + "swc_atoms", + "swc_common", + "swc_ecma_codegen", + "swc_ecma_parser", + "swc_ecma_transforms_base", + "swc_ecma_transforms_typescript", + "swc_ecma_visit", + "wasm-bindgen", + "wasm_mutex", ] [[package]] @@ -1178,15 +1189,6 @@ dependencies = [ "parking_lot_core 0.9.3", ] -[[package]] -name = "data-url" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a30bfce702bcfa94e906ef82421f2c0e61c076ad76030c16ee5d2e9a32fe193" -dependencies = [ - "matches", -] - [[package]] name = "debug_unreachable" version = "0.1.1" @@ -1196,36 +1198,6 @@ dependencies = [ "unreachable", ] -[[package]] -name = "deno_ast" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42fb7189dc0564d7fc4d422868aad20cc6051b4469dad5a39a34bc4741cbc9ec" -dependencies = [ - "anyhow", - "base64 0.13.0", - "data-url", - "dprint-swc-ext", - "serde", - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "swc_ecma_codegen", - "swc_ecma_codegen_macros", - "swc_ecma_loader", - "swc_ecma_parser", - "swc_ecma_transforms_base", - "swc_ecma_transforms_classes", - "swc_ecma_transforms_macros", - "swc_ecma_transforms_proposal", - "swc_ecma_transforms_react", - "swc_ecma_transforms_typescript", - "swc_ecma_utils", - "swc_ecma_visit", - "text_lines", - "url", -] - [[package]] name = "deno_core" version = "0.146.0" @@ -1298,22 +1270,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" -[[package]] -name = "dprint-swc-ext" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83e1b7708a102f7c085a1d51429a3664fe4dd3f6bf67091de83c9dae4dc700e2" -dependencies = [ - "bumpalo", - "num-bigint", - "rustc-hash", - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "swc_ecma_parser", - "text_lines", -] - [[package]] name = "either" version = "1.7.0" @@ -2387,12 +2343,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "pathdiff" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" - [[package]] name = "percent-encoding" version = "2.1.0" @@ -2754,6 +2704,17 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-wasm-bindgen" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfc62771e7b829b517cb213419236475f434fb480eddd76112ae182d274434a" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "serde_bytes" version = "0.11.7" @@ -2841,6 +2802,7 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1e08e261d0e8f5c43123b7adf3e4ca1690d655377ac93a03b2c9d3e98de1342" dependencies = [ + "serde", "version_check", ] @@ -2938,9 +2900,9 @@ checksum = "8fb1df15f412ee2e9dfc1c504260fa695c1c3f10fe9f4a6ee2d2184d7d6450e2" [[package]] name = "swc_atoms" -version = "0.2.13" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d99c0ac33707dd1162a3665d6ca1a28b2f6594e9c37c4703e417fc5e1ce532e" +checksum = "013b8129d72b54f337bc3733a075389d97a5714fb58d3ead5df49977a45322fc" dependencies = [ "once_cell", "rustc-hash", @@ -2951,9 +2913,9 @@ dependencies = [ [[package]] name = "swc_common" -version = "0.23.0" +version = "0.27.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68e76a324fa0d7240e790c78914f39fdecfa9d87ef4efed591124b58607a4a4a" +checksum = "2659e2466667506a9c6994062344f8ee10ae8d48a4bd3a43e974c8945c0ae2bb" dependencies = [ "ahash", "ast_node", @@ -2967,7 +2929,6 @@ dependencies = [ "rustc-hash", "serde", "siphasher", - "sourcemap", "string_cache", "swc_atoms", "swc_eq_ignore_macros", @@ -3005,9 +2966,9 @@ dependencies = [ [[package]] name = "swc_ecma_ast" -version = "0.84.0" +version = "0.90.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cce1fb31e3a100feb31f94647fe27e457bc13b17a8931204fdc9bc58a15c936a" +checksum = "8265c7abbdf36fa17da7c276bf6dd933b0837e00655dc7452b7b23068e7de688" dependencies = [ "bitflags", "is-macro", @@ -3022,14 +2983,15 @@ dependencies = [ [[package]] name = "swc_ecma_codegen" -version = "0.115.0" +version = "0.121.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d09abf1639f76d3d174225fdb608805f9c21d4c455f4dd2ef6ab156701f1f82a" +checksum = "54c1e454253d4cda2d9e600256d5511d33d163361ef8b6165343505c48328f2b" dependencies = [ "memchr", "num-bigint", "once_cell", "rustc-hash", + "serde", "sourcemap", "swc_atoms", "swc_common", @@ -3051,25 +3013,11 @@ dependencies = [ "syn", ] -[[package]] -name = "swc_ecma_loader" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "710c86eb2b253160d4a02fa77057f1c493b3932d1b83430cbbc1e7823eb47e8c" -dependencies = [ - "ahash", - "anyhow", - "pathdiff", - "serde", - "swc_common", - "tracing", -] - [[package]] name = "swc_ecma_parser" -version = "0.111.0" +version = "0.117.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1766e5b969c59e51a5dfe9337755d7380a891e579dd6b0eb7816587c7ea7aa" +checksum = "b8a8ef64367f4f84c49599c94426254fcc2a4bfbc446c422b6208ee685f72f1b" dependencies = [ "either", "enum_kind", @@ -3086,9 +3034,9 @@ dependencies = [ [[package]] name = "swc_ecma_transforms_base" -version = "0.97.0" +version = "0.103.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66b316a99dde0ef85f1878aaa9f4bf9b15f16e999c56ed31a1433928c754ae4e" +checksum = "fdd22a6ac5bd2baa8f171a07a9c41e5d8e599c854769099817be46aef0e2c2e2" dependencies = [ "better_scoped_tls", "bitflags", @@ -3107,20 +3055,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "swc_ecma_transforms_classes" -version = "0.85.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c853c4366e81092d38b746e71adffc1150c694f02c1068c9fa24abbdc373a65f" -dependencies = [ - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "swc_ecma_transforms_base", - "swc_ecma_utils", - "swc_ecma_visit", -] - [[package]] name = "swc_ecma_transforms_macros" version = "0.5.0" @@ -3134,30 +3068,11 @@ dependencies = [ "syn", ] -[[package]] -name = "swc_ecma_transforms_proposal" -version = "0.122.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ebc6e03a51f9adcbc40ec144c9bbe78de872bf6f8f581f3abd51187ec6e648" -dependencies = [ - "either", - "serde", - "smallvec", - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "swc_ecma_transforms_base", - "swc_ecma_transforms_classes", - "swc_ecma_transforms_macros", - "swc_ecma_utils", - "swc_ecma_visit", -] - [[package]] name = "swc_ecma_transforms_react" -version = "0.133.0" +version = "0.141.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6b0516e231008722175bc0841bf4f3fdcfd3276ca0bf4878d6e87af5c50f324" +checksum = "947cdceaf6f0108d6253d02848436131061f16861df417e81b44c227285d952c" dependencies = [ "ahash", "base64 0.13.0", @@ -3181,9 +3096,9 @@ dependencies = [ [[package]] name = "swc_ecma_transforms_typescript" -version = "0.137.0" +version = "0.145.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6716a73401b5d717d6fd20159385ce09adbdd3afc765c3890859d84ada8af729" +checksum = "3f61975f11d397454db9f5046c3224813c95804e5684933c5cbf6324a4d9ff5c" dependencies = [ "serde", "swc_atoms", @@ -3197,9 +3112,9 @@ dependencies = [ [[package]] name = "swc_ecma_utils" -version = "0.93.0" +version = "0.99.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70981d5ef10c0ff0a002e21decbca9dde5b40c2fc0d0bc6eaebb219a8e0a5f7d" +checksum = "6494149979ae31a79d8a423c5da4aae9ac8ff0195d9e0135ccf569bc1a1d0d3f" dependencies = [ "indexmap", "once_cell", @@ -3213,9 +3128,9 @@ dependencies = [ [[package]] name = "swc_ecma_visit" -version = "0.70.0" +version = "0.76.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcd081250d664808fcd23110202728811236c87f527656ffc1db7f00ac1a06dd" +checksum = "d998049fbf890e6674c7c818683b3eed53d993aac7156970b03b343659aeef75" dependencies = [ "num-bigint", "swc_atoms", @@ -3251,9 +3166,9 @@ dependencies = [ [[package]] name = "swc_visit" -version = "0.4.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fafa6c946bdbe601f5511140776d59e82a03f52a5e5039192b4b96f3ca639d88" +checksum = "b754ef01f2614eb469fd5215789bfd319566a3bf1033056d7a7bfb5a3c9a69f5" dependencies = [ "either", "swc_visit_macros", @@ -3261,9 +3176,9 @@ dependencies = [ [[package]] name = "swc_visit_macros" -version = "0.4.0" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cad1b8e0b2d48660bc454f70495e9bb583f9bf501f28165568569946e62f44a2" +checksum = "c230bcd129d1fbcd1decd8b43cccd613fda11c895f7c04d6c966231dbc1959af" dependencies = [ "Inflector", "pmutil", @@ -3306,15 +3221,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "text_lines" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e49e3c53dd04de8b8e8390bc4fab57f6db7af7d33b086fe411803e6351c9f9f9" -dependencies = [ - "serde", -] - [[package]] name = "thiserror" version = "1.0.32" @@ -3650,6 +3556,15 @@ version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a" +[[package]] +name = "wasm_mutex" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efbdddc3b163fc2d639800b3411a5428d1e151ba2a400a560b1545e39f1e68cd" +dependencies = [ + "serde", +] + [[package]] name = "web-sys" version = "0.3.59" @@ -3670,7 +3585,7 @@ dependencies = [ "js-sys", "log", "naga", - "parking_lot 0.11.2", + "parking_lot 0.12.1", "raw-window-handle", "smallvec", "wasm-bindgen", @@ -3696,7 +3611,7 @@ dependencies = [ "fxhash", "log", "naga", - "parking_lot 0.11.2", + "parking_lot 0.12.1", "profiling", "raw-window-handle", "smallvec", @@ -3733,7 +3648,7 @@ dependencies = [ "metal", "naga", "objc", - "parking_lot 0.11.2", + "parking_lot 0.12.1", "profiling", "range-alloc", "raw-window-handle", diff --git a/Cargo.toml b/Cargo.toml index 7d0ccf9..2f03da1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,25 +3,38 @@ name = "bevy_mod_js_scripting" version = "0.1.0" edition = "2021" -[features] -default = ["typescript"] -typescript = [ - "deno_ast" -] - [dependencies] anyhow = "1.0.57" bevy = { version = "0.8.0", default-features = false, features = ["bevy_asset"] } bevy_reflect = "0.8.0-dev" fixedbitset = "0.4" -deno_core = "0.146.0" -deno_ast = { version = "0.17.0", features = ["transpiling"], optional = true } serde = "1.0" serde_json = "1.0" bevy_ecs_dynamic = { git = "https://github.com/jakobhellermann/bevy_ecs_dynamic" } bevy_reflect_fns = { git = "https://github.com/jakobhellermann/bevy_reflect_fns" } +swc_common = { version = "0.27.4" } +swc_ecma_codegen = { version = "0.121.2" } +swc_ecma_parser = { version = "0.117.2" } +swc_ecma_transforms_base = { version = "0.103.4" } +swc_ecma_transforms_typescript = { version = "0.145.1" } +swc_ecma_visit = { version = "0.76.3" } +swc_atoms = { version = "0.4.3" } + +[profile.dev] +opt-level = 1 + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +deno_core = "0.146.0" + +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm_mutex = "0.1.4" +js-sys = "0.3.59" +wasm-bindgen = { version = "0.2.82", features = ["enable-interning"] } +serde-wasm-bindgen = "0.4.3" +slotmap = { version = "1.0.6", features = ["serde"] } + [dev-dependencies] bevy = { version = "0.8.0", default-features = false, features = ["render", "bevy_winit", "x11", "filesystem_watcher"] } diff --git a/README.md b/README.md new file mode 100644 index 0000000..94adc6d --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# bevy_mod_js_scripting + +Work in progress javascript scripting integration into bevy. +Proper readme coming soon. + +## Web support + +`bevy_mod_js_scripting` can run in the browser using its native javascript execution environment. +To try it out, download and configure [wasm-server-runner](https://github.com/jakobhellermann/wasm-server-runner) and run +```sh +cargo run --example breakout --target wasm32-unknown-unknown +``` diff --git a/assets/scripts/breakout.ts b/assets/scripts/breakout.ts index 6a0eae7..3fff024 100644 --- a/assets/scripts/breakout.ts +++ b/assets/scripts/breakout.ts @@ -1,30 +1,35 @@ - -function filterComponentInfos(infos: ComponentInfo[], prefix: string): string[] { - return infos - .filter(info => info.name.startsWith(prefix)) - .map(info => info.name.replace(prefix, "")); +function filterComponentInfos( + infos: ComponentInfo[], + prefix: string +): string[] { + return infos + .filter((info) => info.name.startsWith(prefix)) + .map((info) => info.name.replace(prefix, "")); } - let firstIteration = true; let i = 0; type Scoreboard = { - score: number; + score: number; }; const Scoreboard: BevyType = { typeName: "breakout::Scoreboard" }; function run() { - i++; - if (i % 60 == 0) { - let time = world.resource(Scoreboard); - time.score += 1; - info(time.score); - } + i++; + if (i % 60 == 0) { + let score = world.resource(Scoreboard); + score.score += 1; + info(score.score); + } - if (firstIteration) { - firstIteration = false; - // info("Components: " + filterComponentInfos(world.components, "bevy_transform::")); - // info("Resources: " + filterComponentInfos(world.resources, "breakout::").join(", ")); - } + if (firstIteration) { + firstIteration = false; + // info("Components: " + filterComponentInfos(world.components, "bevy_transform::")); + // info("Resources: " + filterComponentInfos(world.resources, "breakout::").join(", ")); + } } + +export default { + update: run, +}; diff --git a/assets/scripts/headless.ts b/assets/scripts/headless.ts index a03a723..07105c7 100644 --- a/assets/scripts/headless.ts +++ b/assets/scripts/headless.ts @@ -1,39 +1,46 @@ -function filterComponentInfos(infos: ComponentInfo[], prefix: string): string[] { - return infos - .filter(info => info.name.startsWith(prefix)) - .map(info => info.name.replace(prefix, "")); +function filterComponentInfos( + infos: ComponentInfo[], + prefix: string +): string[] { + return infos + .filter((info) => info.name.startsWith(prefix)) + .map((info) => info.name.replace(prefix, "")); } function componentId(name) { - let id = world.components.find(info => info.name === name); - if (!id) throw new Error(`component id for ${name} not found`); - return id.id; - + let id = world.components.find((info) => info.name === name); + if (!id) throw new Error(`component id for ${name} not found`); + return id.id; } let firstIteration = true; function run() { - if (firstIteration) { - firstIteration = false; - - // info("Components: " + world.components.map(info => info.name).join(", ")); - // info("Resources:"); - // info(world.resources.map(info => info.name)); - // info("Resources (headless): " + filterComponentInfos(world.resources, "headless::").join(", ")); - // info("Entitites: " + (world.entities.map(entity => `Entity(${entity.id}v${entity.generation})`).join(", "))); - info("----------"); - - let transformId = componentId("bevy_transform::components::transform::Transform"); - - - let query = world.query({ - components: [transformId] - }); - let [transform1, transform2] = query.map(item => item.components[0]); - let [translation1, translation2] = [transform1.translation, transform2.translation]; - - for (const s of [0.0, 0.25, 0.5, 0.75, 1.0]) { - info(translation1.lerp(translation2, s).toString()); - } + if (firstIteration) { + firstIteration = false; + + // info("Components: " + world.components.map(info => info.name).join(", ")); + // info("Resources:"); + // info(world.resources.map(info => info.name)); + // info("Resources (headless): " + filterComponentInfos(world.resources, "headless::").join(", ")); + // info("Entitites: " + (world.entities.map(entity => `Entity(${entity.id}v${entity.generation})`).join(", "))); + info("----------"); + + let transformId = componentId( + "bevy_transform::components::transform::Transform" + ); + + let query = world.query({ + components: [transformId], + }); + let [transform1, transform2] = query.map((item) => item.components[0]); + let [translation1, translation2] = [transform1.translation, transform2.translation]; + + for (const s of [0.0, 0.25, 0.5, 0.75, 1.0]) { + info(translation1.lerp(translation2, s).toString()); } + } } + +export default { + update: run, +}; diff --git a/lib.bevy.d.ts b/lib.bevy.d.ts new file mode 100644 index 0000000..59e2fa4 --- /dev/null +++ b/lib.bevy.d.ts @@ -0,0 +1,66 @@ +declare namespace Deno { + namespace core { + function opSync(op: string, ...args: any[]): any; + } +} + +// log.s +declare function trace(val: any): void; +declare function debug(val: any): void; +declare function info(val: any): void; +declare function warn(val: any): void; +declare function error(val: any): void; + +// ecs.js +declare interface BevyScript { + first?: () => void; + pre_update?: () => void; + update?: () => void; + post_update?: () => void; + last?: () => void; +} + +declare class ComponentId { + index: number; +} +declare class Entity { + id: number; + generation: number; +} + +type ComponentInfo = { + id: ComponentId; + name: string; + size: number; +}; + +type QueryDescriptor = { + components: ComponentId[]; +}; + +type QueryItem = { + entity: Entity; + components: any[]; +}; + +type Primitive = number | string | boolean; +interface Value { + [path: string | number]: Value | Primitive | undefined; +} + +type BevyType = { + typeName: string; +}; + +declare class World { + get components(): ComponentInfo[]; + get resources(): ComponentInfo[]; + get entities(): Entity[]; + + resource(componentId: ComponentId): Value | null; + resource(type: BevyType): T | null; + + query(descriptor: QueryDescriptor): QueryItem[]; +} + +declare let world: World; diff --git a/src/asset.rs b/src/asset.rs index a2b3d76..7753d16 100644 --- a/src/asset.rs +++ b/src/asset.rs @@ -20,16 +20,7 @@ impl AssetLoader for JsScriptLoader { Box::pin(async move { let source = String::from_utf8(bytes.to_vec())?; - #[cfg(feature = "typescript")] - let source = if load_context - .path() - .extension() - .map_or(false, |ext| ext == "ts") - { - crate::ts_to_js::ts_to_js(load_context.path(), source)? - } else { - source - }; + let source = crate::transpile::transpile(load_context.path(), &source)?; load_context.set_default_asset(LoadedAsset::new(JsScript { source, @@ -40,10 +31,6 @@ impl AssetLoader for JsScriptLoader { } fn extensions(&self) -> &[&str] { - #[cfg(feature = "typescript")] - let exts = &["js", "ts"]; - #[cfg(not(feature = "typescript"))] - let exts = &["js", "ts"]; - exts + &["js", "ts"] } } diff --git a/src/lib.rs b/src/lib.rs index 4352157..c89ba5b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,43 +2,30 @@ mod asset; mod runtime; -#[cfg(feature = "typescript")] -mod ts_to_js; +mod transpile; use asset::{JsScript, JsScriptLoader}; -use bevy::utils::HashMap; use bevy::{asset::AssetStage, prelude::*}; -use deno_core::{v8, JsRuntime}; -use runtime::create_runtime; -pub struct JsScriptingPlugin; +use runtime::{JsRuntime, JsRuntimeApi}; -enum RuntimeStatus { - FailedToLoad, - Loaded, -} -struct LoadedRuntime { - runtime: JsRuntime, - status: RuntimeStatus, -} +pub struct JsScriptingPlugin; -#[derive(Default)] -pub struct ActiveScripts { - runtimes: HashMap, LoadedRuntime>, - by_stage: HashMap>>, -} +#[derive(Default, Deref, DerefMut)] +pub struct ActiveScripts(Vec>); impl Plugin for JsScriptingPlugin { fn build(&self, app: &mut App) { - app.insert_non_send_resource(ActiveScripts::default()); - app.add_asset::().add_asset_loader(JsScriptLoader); - - app.add_system_to_stage( - AssetStage::AssetEvents, - load_scripts.after(Assets::::asset_event_system), - ); + app.init_non_send_resource::() + .init_resource::() + .add_asset::() + .add_asset_loader(JsScriptLoader) + .add_system_to_stage( + AssetStage::AssetEvents, + load_scripts.after(Assets::::asset_event_system), + ); - for stage in [ + for stage in &[ CoreStage::First, CoreStage::PreUpdate, CoreStage::Update, @@ -48,24 +35,17 @@ impl Plugin for JsScriptingPlugin { app.add_system_to_stage( stage.clone(), (move |world: &mut World| { - let mut active = world.remove_non_send_resource::().unwrap(); - world.resource_scope(|world, assets: Mut>| { - let in_stage = active - .by_stage - .entry(stage.clone()) - .or_default() - .iter() - .filter(|&handle| assets.contains(handle)); - for handle in in_stage { - if let Some(runtime) = active.runtimes.get_mut(handle) { - if let RuntimeStatus::Loaded = runtime.status { - run_js_script(world, &mut runtime.runtime); - } - } + let active_scripts = world.remove_resource::().unwrap(); + let runtime = world.remove_non_send_resource::().unwrap(); + + for script in &*active_scripts { + if runtime.has_loaded(script) { + runtime.run_script(script, stage, world); } - }); + } - world.insert_non_send_resource(active); + world.insert_resource(active_scripts); + world.insert_non_send_resource(runtime); }) .exclusive_system() .at_start(), @@ -75,73 +55,65 @@ impl Plugin for JsScriptingPlugin { } pub trait AddJsSystem { - fn add_js_system_to_stage(&mut self, stage: CoreStage, path: &str) -> &mut Self; - fn add_js_system(&mut self, path: &str) -> &mut Self { - self.add_js_system_to_stage(CoreStage::Update, path) - } + fn add_js_system(&mut self, path: &str) -> &mut Self; } impl AddJsSystem for App { - fn add_js_system_to_stage(&mut self, stage: CoreStage, path: &str) -> &mut Self { + fn add_js_system(&mut self, path: &str) -> &mut Self { let asset_server = self.world.resource::(); let handle = asset_server.load(path); - let mut active = self.world.non_send_resource_mut::(); - active.by_stage.entry(stage).or_default().push(handle); + let mut active = self.world.resource_mut::(); + active.push(handle); self } } +/// Helper struct used in [`load_scripts`] +struct ScriptToLoad { + handle: Handle, + reload: bool, +} + +/// System to finish loading scripts that have had their source-code loaded by the asset server. fn load_scripts( + mut scripts_to_load: Local>, mut events: EventReader>, - mut active_scripts: NonSendMut, assets: Res>, + engine: NonSendMut, ) { for event in events.iter() { match event { - AssetEvent::Created { handle } | AssetEvent::Modified { handle } => { - let js_script = assets.get(handle).unwrap(); - - let mut runtime = create_runtime(js_script.path.clone()); - let name = js_script.path.display().to_string(); - - let status = match runtime.execute_script(&name, &js_script.source) { - Ok(_) => RuntimeStatus::Loaded, - Err(e) => { - warn!("failed to load {name}: {e}"); - RuntimeStatus::FailedToLoad - } - }; - - active_scripts - .runtimes - .insert(handle.clone_weak(), LoadedRuntime { runtime, status }); + AssetEvent::Created { handle } => { + scripts_to_load.push(ScriptToLoad { + handle: handle.clone_weak(), + reload: false, + }); } - AssetEvent::Removed { .. } => {} + AssetEvent::Modified { handle } => { + scripts_to_load.push(ScriptToLoad { + handle: handle.clone_weak(), + reload: true, + }); + } + _ => (), } } -} -fn run_js_script(world: &mut World, runtime: &mut JsRuntime) { - let res = runtime::with_world(world, runtime, |runtime| { - let context = runtime.global_context(); - let context = context.open(runtime.v8_isolate()); - let scope = &mut runtime.handle_scope(); - let global = context.global(scope); - let run_str = v8::String::new(scope, "run").unwrap(); - let run_fn = global - .get(scope, run_str.into()) - .ok_or_else(|| anyhow::anyhow!("script has no `run` function"))?; - let run_fn = v8::Local::::try_from(run_fn) - .map_err(|_| anyhow::anyhow!("`run` should be a function"))?; - - let undefined = v8::undefined(scope); - run_fn.call(scope, undefined.into(), &[undefined.into()]); - - Ok::<_, anyhow::Error>(()) - }); - - if let Err(e) = res { - warn!("script failed to run: {:?}", e); + // Get the list of scripts we need to try to load + let mut scripts = Vec::new(); + std::mem::swap(&mut *scripts_to_load, &mut scripts); + + for to_load in scripts { + // If the script asset has loaded + if let Some(script) = assets.get(&to_load.handle) { + // Have the engine load the script + engine.load_script(&to_load.handle, script, to_load.reload); + + // If the asset hasn't loaded yet + } else { + // Add it to the list of scripts to try to load later + scripts_to_load.push(to_load); + } } } diff --git a/src/runtime/js/ecs.js b/src/runtime/js/ecs.js index b438ba1..b468c37 100644 --- a/src/runtime/js/ecs.js +++ b/src/runtime/js/ecs.js @@ -1,7 +1,6 @@ "use strict"; ((window) => { - class ComponentId { index; } @@ -13,70 +12,109 @@ } class World { - get #rid() { return 0 } + get #rid() { + return 0; + } toString() { - return Deno.core.opSync("op_world_tostring", this.rid); + return bevyModJsScriptingOpSync("op_world_tostring", this.rid); } get components() { - return Deno.core.opSync("op_world_components", this.rid); + return bevyModJsScriptingOpSync("op_world_components", this.rid); } get resources() { - return Deno.core.opSync("op_world_resources", this.rid); + return bevyModJsScriptingOpSync("op_world_resources", this.rid); } get entities() { - return Deno.core.opSync("op_world_entities", this.rid); + return bevyModJsScriptingOpSync("op_world_entities", this.rid); } resource(componentId) { - let resource = Deno.core.opSync("op_world_get_resource", this.rid, componentId); + let resource = bevyModJsScriptingOpSync( + "op_world_get_resource", + this.rid, + componentId + ); return resource != null ? wrapValueRef(resource) : null; } query(descriptor) { - return Deno.core.opSync("op_world_query", this.rid, descriptor) - .map(({ entity, components }) => ({ - entity, - components: components.map(wrapValueRef), - })); + return bevyModJsScriptingOpSync( + "op_world_query", + this.rid, + descriptor + ).map(({ entity, components }) => ({ + entity, + components: components.map(wrapValueRef), + test: components, + })); } } - const VALUE_REF_GET_INNER = Symbol("value_ref_get_inner"); function wrapValueRef(valueRef) { // leaf primitives - if (typeof valueRef !== "object") { return valueRef }; - let target = () => { }; + if (typeof valueRef !== "object") { + return valueRef; + } + let target = () => {}; target.valueRef = valueRef; const proxy = new Proxy(target, { ownKeys: (target) => { - return [...Deno.core.opSync("op_value_ref_keys", world.rid, target.valueRef), VALUE_REF_GET_INNER]; + return [ + ...bevyModJsScriptingOpSync( + "op_value_ref_keys", + world.rid, + target.valueRef + ), + VALUE_REF_GET_INNER, + ]; }, get: (target, p, receiver) => { switch (p) { case VALUE_REF_GET_INNER: return target; case "toString": - return () => Deno.core.opSync("op_value_ref_to_string", world.rid, target.valueRef); + return () => + bevyModJsScriptingOpSync( + "op_value_ref_to_string", + world.rid, + target.valueRef + ); default: - let valueRef = Deno.core.opSync("op_value_ref_get", world.rid, target.valueRef, "." + p); + let valueRef = bevyModJsScriptingOpSync( + "op_value_ref_get", + world.rid, + target.valueRef, + "." + p + ); return wrapValueRef(valueRef); } }, set: (target, p, value) => { - Deno.core.opSync("op_value_ref_set", world.rid, target.valueRef, "." + p, value); + bevyModJsScriptingOpSync( + "op_value_ref_set", + world.rid, + target.valueRef, + "." + p, + value + ); }, apply: (target, thisArg, args) => { - let ret = Deno.core.opSync("op_value_ref_call", world.rid, target.valueRef, args.map(arg => { - let valueRef = arg[VALUE_REF_GET_INNER]?.valueRef; - return (valueRef !== undefined) ? valueRef : arg; - })); + let ret = bevyModJsScriptingOpSync( + "op_value_ref_call", + world.rid, + target.valueRef, + args.map((arg) => { + let valueRef = arg[VALUE_REF_GET_INNER]?.valueRef; + return valueRef !== undefined ? valueRef : arg; + }) + ); return wrapValueRef(ret); - } + }, }); return proxy; } diff --git a/src/runtime/js/index.d.ts b/src/runtime/js/index.d.ts deleted file mode 100644 index 889e7f4..0000000 --- a/src/runtime/js/index.d.ts +++ /dev/null @@ -1,59 +0,0 @@ -declare namespace Deno { - namespace core { - function opSync(op: string, ...args: any[]): any; - - } -} - -// log.s -declare function trace(val: any): void; -declare function debug(val: any): void; -declare function info(val: any): void; -declare function warn(val: any): void; -declare function error(val: any): void; - -// ecs.js -declare class ComponentId { - index: number; -} -declare class Entity { - id: number; - generation: number; -} - -type ComponentInfo = { - id: ComponentId, - name: string, - size: number; -}; - -type QueryDescriptor = { - components: ComponentId[], -}; - -type QueryItem = { - entity: Entity, - components: any[], -}; - -type Primitive = number | string | boolean; -interface Value { - [path: string | number]: Value | Primitive | undefined, -} - -type BevyType = { - typeName: string; -}; - -declare class World { - get components(): ComponentInfo[]; - get resources(): ComponentInfo[]; - get entities(): Entity[]; - - resource(componentId: ComponentId): Value | null; - resource(type: BevyType): T | null; - - query(descriptor: QueryDescriptor): QueryItem[]; -} - -declare let world: World; diff --git a/src/runtime/js/log.js b/src/runtime/js/log.js index 108519e..a1b78fb 100644 --- a/src/runtime/js/log.js +++ b/src/runtime/js/log.js @@ -2,18 +2,18 @@ ((window) => { window.trace = function (val) { - Deno.core.opSync("op_log", "trace", val); - } + bevyModJsScriptingOpSync("op_log", "trace", val); + }; window.debug = function (val) { - Deno.core.opSync("op_log", "debug", val) - } + bevyModJsScriptingOpSync("op_log", "debug", val); + }; window.info = function (val) { - Deno.core.opSync("op_log", "info", val) - } + bevyModJsScriptingOpSync("op_log", "info", val); + }; window.warn = function (val) { - Deno.core.opSync("op_log", "warn", val) - } + bevyModJsScriptingOpSync("op_log", "warn", val); + }; window.error = function (val) { - Deno.core.opSync("op_log", "error", val) - } + bevyModJsScriptingOpSync("op_log", "error", val); + }; })(globalThis); diff --git a/src/runtime/mod.rs b/src/runtime/mod.rs index 195d82d..8ceb2c8 100644 --- a/src/runtime/mod.rs +++ b/src/runtime/mod.rs @@ -1,62 +1,32 @@ -mod ecs; -mod log; - -use std::{cell::RefCell, path::PathBuf}; - use bevy::prelude::*; -use deno_core::{JsRuntime, ResourceId, RuntimeOptions}; -struct WorldResource { - world: RefCell, -} -impl deno_core::Resource for WorldResource {} +use crate::asset::JsScript; -const WORLD_RID: ResourceId = 0; - -struct ScriptInfo { - path: PathBuf, -} +mod types; -pub fn create_runtime(path: PathBuf) -> JsRuntime { - let mut runtime = JsRuntime::new(RuntimeOptions { - extensions: vec![ecs::extension(), log::extension()], - ..Default::default() - }); - - let state = runtime.op_state(); - let mut state = state.borrow_mut(); - state.put(ScriptInfo { path }); - - let rid = state.resource_table.add(WorldResource { - world: RefCell::new(World::default()), - }); - assert_eq!(rid, WORLD_RID); - - runtime -} +#[cfg(target_arch = "wasm32")] +mod wasm; +#[cfg(target_arch = "wasm32")] +pub use wasm::*; -pub fn with_world( - world: &mut World, - runtime: &mut JsRuntime, - f: impl Fn(&mut JsRuntime) -> T, -) -> T { - let resource = runtime - .op_state() - .borrow_mut() - .resource_table - .get::(WORLD_RID) - .unwrap(); - std::mem::swap(world, &mut *resource.world.borrow_mut()); +#[cfg(not(target_arch = "wasm32"))] +mod native; +#[cfg(not(target_arch = "wasm32"))] +pub use native::*; - let ret = f(runtime); +/// The API implemented by different script runtimes. +/// +/// Currently we have a native runtime built on [`deno_core`] and a web runtime utilizing +/// [`wasm_bindgen`]. +pub trait JsRuntimeApi: FromWorld { + /// Load a script + /// + /// This will not reload a script that has already been loaded unless `reload` is set to `true`. + fn load_script(&self, handle: &Handle, script: &JsScript, reload: bool); - let resource = runtime - .op_state() - .borrow_mut() - .resource_table - .get::(WORLD_RID) - .unwrap(); - std::mem::swap(world, &mut *resource.world.borrow_mut()); + /// Returns whether or not a script has been loaded yet + fn has_loaded(&self, handle: &Handle) -> bool; - ret + /// Run a script + fn run_script(&self, handle: &Handle, stage: &CoreStage, world: &mut World); } diff --git a/src/runtime/ecs/call.rs b/src/runtime/native/ecs/call.rs similarity index 71% rename from src/runtime/ecs/call.rs rename to src/runtime/native/ecs/call.rs index 4c298e1..02806d7 100644 --- a/src/runtime/ecs/call.rs +++ b/src/runtime/native/ecs/call.rs @@ -1,3 +1,5 @@ +use crate::runtime::types::{Primitive, ReflectArgIntermediate, ReflectArgIntermediateValue}; + use super::{ v8_utils::{ create_value_ref_object, reflect_value_ref_from_v8_value_transmit, @@ -5,70 +7,11 @@ use super::{ }, WorldResource, }; -use bevy::prelude::*; -use bevy_ecs_dynamic::reflect_value_ref::{ - ReflectValueRef, ReflectValueRefBorrow, ReflectValueRefBorrowMut, -}; +use bevy_ecs_dynamic::reflect_value_ref::ReflectValueRef; use bevy_reflect_fns::{PassMode, ReflectArg}; use deno_core::{error::AnyError, op, serde_v8, v8, OpState, ResourceId}; use std::{any::TypeId, cell::RefCell, rc::Rc}; -// Value, from which a `ReflectArg` can be borrowed -enum ReflectArgIntermediate<'a> { - Value(ReflectArgIntermediateValue<'a>), - Primitive(Primitive, PassMode), -} - -enum ReflectArgIntermediateValue<'a> { - Ref(ReflectValueRefBorrow<'a>), - #[allow(dead_code)] - RefMut(ReflectValueRefBorrowMut<'a>), - Owned(ReflectValueRefBorrow<'a>), -} - -impl<'a> ReflectArgIntermediateValue<'a> { - fn to_arg(&mut self) -> ReflectArg<'_> { - match self { - ReflectArgIntermediateValue::Ref(val) => ReflectArg::Ref(&**val), - ReflectArgIntermediateValue::RefMut(val) => ReflectArg::RefMut(&mut **val), - ReflectArgIntermediateValue::Owned(val) => ReflectArg::Owned(&**val), - } - } -} -impl<'a> ReflectArgIntermediate<'a> { - fn to_arg(&mut self) -> ReflectArg<'_> { - match self { - ReflectArgIntermediate::Value(val) => val.to_arg(), - ReflectArgIntermediate::Primitive(prim, pass_mode) => prim.to_arg(*pass_mode), - } - } -} - -#[allow(non_camel_case_types)] -enum Primitive { - f32(f32), - f64(f64), - i32(i32), - u32(u32), -} - -impl Primitive { - fn to_arg(&mut self, pass_mode: PassMode) -> ReflectArg<'_> { - let reflect: &mut dyn Reflect = match self { - Primitive::f32(val) => val, - Primitive::f64(val) => val, - Primitive::i32(val) => val, - Primitive::u32(val) => val, - }; - - match pass_mode { - PassMode::Ref => ReflectArg::Ref(reflect), - PassMode::RefMut => ReflectArg::RefMut(reflect), - PassMode::Owned => ReflectArg::Owned(reflect), - } - } -} - #[op(v8)] fn op_value_ref_call( state: &mut OpState, @@ -150,7 +93,7 @@ fn op_value_ref_call( .collect::, AnyError>>()?; let mut args: Vec = std::iter::once(&mut receiver_intermediate) .chain(arg_intermediates.iter_mut()) - .map(|intermediate| intermediate.to_arg()) + .map(|intermediate| intermediate.as_arg()) .collect(); let ret = method.call(args.as_mut_slice())?; diff --git a/src/runtime/ecs/info.rs b/src/runtime/native/ecs/info.rs similarity index 95% rename from src/runtime/ecs/info.rs rename to src/runtime/native/ecs/info.rs index e0fe8b8..247fa07 100644 --- a/src/runtime/ecs/info.rs +++ b/src/runtime/native/ecs/info.rs @@ -1,7 +1,5 @@ -use super::{ - types::{JsComponentInfo, JsEntity}, - WorldResource, -}; +use super::WorldResource; +use crate::runtime::types::{JsComponentInfo, JsEntity}; use bevy::{ecs::component::ComponentId, prelude::*, utils::HashSet}; use deno_core::{error::AnyError, op, OpState, ResourceId}; diff --git a/src/runtime/ecs/mod.rs b/src/runtime/native/ecs/mod.rs similarity index 91% rename from src/runtime/ecs/mod.rs rename to src/runtime/native/ecs/mod.rs index 36e5ff7..5b7259e 100644 --- a/src/runtime/ecs/mod.rs +++ b/src/runtime/native/ecs/mod.rs @@ -1,7 +1,6 @@ use super::WorldResource; use deno_core::{include_js_files, Extension}; -mod types; mod v8_utils; mod call; @@ -25,6 +24,6 @@ pub fn extension() -> Extension { call::op_value_ref_call::decl(), resource::op_world_get_resource::decl(), ]) - .js(include_js_files!(prefix "bevy", "../js/ecs.js",)) + .js(include_js_files!(prefix "bevy", "../../js/ecs.js",)) .build() } diff --git a/src/runtime/ecs/query.rs b/src/runtime/native/ecs/query.rs similarity index 88% rename from src/runtime/ecs/query.rs rename to src/runtime/native/ecs/query.rs index 97f6728..f79a8db 100644 --- a/src/runtime/ecs/query.rs +++ b/src/runtime/native/ecs/query.rs @@ -1,17 +1,12 @@ use super::{ - types::{JsComponentId, JsEntity}, v8_utils::{create_value_ref_object, ValueRefObject}, WorldResource, }; +use crate::runtime::types::{JsEntity, QueryDescriptor}; use bevy::ecs::component::ComponentId; use bevy_ecs_dynamic::reflect_value_ref::query::EcsValueRefQuery; use deno_core::{error::AnyError, op, v8, OpState, ResourceId}; -use serde::{Deserialize, Serialize}; - -#[derive(Deserialize)] -pub struct QueryDescriptor { - components: Vec, -} +use serde::Serialize; #[derive(Serialize)] pub struct JsQueryItem { diff --git a/src/runtime/ecs/resource.rs b/src/runtime/native/ecs/resource.rs similarity index 85% rename from src/runtime/ecs/resource.rs rename to src/runtime/native/ecs/resource.rs index 27dbb41..df6d3f6 100644 --- a/src/runtime/ecs/resource.rs +++ b/src/runtime/native/ecs/resource.rs @@ -1,12 +1,9 @@ use bevy_ecs_dynamic::reflect_value_ref::EcsValueRef; use deno_core::{error::AnyError, op, v8, OpState, ResourceId}; -use crate::runtime::WorldResource; +use crate::runtime::{native::WorldResource, types::ComponentIdOrBevyType}; -use super::{ - types::ComponentIdOrBevyType, - v8_utils::{create_value_ref_object, ValueRefObject}, -}; +use super::v8_utils::{create_value_ref_object, ValueRefObject}; #[op(v8)] pub fn op_world_get_resource( diff --git a/src/runtime/ecs/v8_utils.rs b/src/runtime/native/ecs/v8_utils.rs similarity index 100% rename from src/runtime/ecs/v8_utils.rs rename to src/runtime/native/ecs/v8_utils.rs diff --git a/src/runtime/ecs/value.rs b/src/runtime/native/ecs/value.rs similarity index 96% rename from src/runtime/ecs/value.rs rename to src/runtime/native/ecs/value.rs index 65d9b7c..674de62 100644 --- a/src/runtime/ecs/value.rs +++ b/src/runtime/native/ecs/value.rs @@ -36,8 +36,8 @@ pub fn op_value_ref_get( let reflect_methods = type_registry .get_type_data::(value_ref.get(&world)?.type_id()); if let Some(reflect_methods) = reflect_methods { - let method_name = path.trim_start_matches("."); - if let Some(reflect_function) = reflect_methods.get(method_name.trim_start_matches(".")) { + let method_name = path.trim_start_matches('.'); + if let Some(reflect_function) = reflect_methods.get(method_name.trim_start_matches('.')) { return Ok(unsafe { create_value_ref_object( scope, @@ -146,6 +146,6 @@ pub fn op_value_ref_to_string( let reflect = value.get(&world)?; Ok(format!("{reflect:?}")) } - ReflectValueRefTransmit::Method(_, method) => Ok(format!("{}", method.fn_name)), + ReflectValueRefTransmit::Method(_, method) => Ok(method.fn_name.to_string()), } } diff --git a/src/runtime/log.rs b/src/runtime/native/log.rs similarity index 95% rename from src/runtime/log.rs rename to src/runtime/native/log.rs index 7cad1c3..16554c4 100644 --- a/src/runtime/log.rs +++ b/src/runtime/native/log.rs @@ -35,6 +35,6 @@ fn op_log(state: &mut OpState, kind: String, text: serde_json::Value) -> Result< pub fn extension() -> Extension { Extension::builder() .ops(vec![op_log::decl()]) - .js(include_js_files!(prefix "bevy", "js/log.js",)) + .js(include_js_files!(prefix "bevy", "../js/log.js",)) .build() } diff --git a/src/runtime/native/mod.rs b/src/runtime/native/mod.rs new file mode 100644 index 0000000..db91707 --- /dev/null +++ b/src/runtime/native/mod.rs @@ -0,0 +1,222 @@ +mod ecs; +mod log; + +use std::{cell::RefCell, path::PathBuf}; + +use bevy::{prelude::*, utils::HashMap}; +use deno_core::{ + include_js_files, v8, Extension, JsRuntime as DenoJsRuntime, ResourceId, RuntimeOptions, +}; + +use super::JsRuntimeApi; +use crate::asset::JsScript; + +/// Resource stored in the Deno runtime to give access to the Bevy world +struct WorldResource { + world: RefCell, +} +impl deno_core::Resource for WorldResource {} +impl WorldResource { + const RID: ResourceId = 0; +} + +/// Info about the currently executing script, stored in the Deno op_state for use in ops such as +/// logging. +struct ScriptInfo { + path: PathBuf, +} + +/// The [`JsRuntimeApi`] implementation for native platforms. +#[derive(Deref, DerefMut)] +pub struct JsRuntime(RefCell); + +pub struct JsRuntimeInner { + scripts: HashMap, LoadedScriptData>, + runtime: deno_core::JsRuntime, +} + +struct LoadedScriptData { + output: v8::Global, + path: PathBuf, +} + +impl Default for JsRuntime { + fn default() -> Self { + let mut runtime = DenoJsRuntime::new(RuntimeOptions { + extensions: vec![ + ecs::extension(), + log::extension(), + Extension::builder() + .js(include_js_files!(prefix "bevy", "./native_setup.js",)) + .build(), + ], + ..Default::default() + }); + + let state = runtime.op_state(); + let rid = state.borrow_mut().resource_table.add(WorldResource { + world: RefCell::new(World::default()), + }); + assert_eq!(rid, WorldResource::RID); + + Self(RefCell::new(JsRuntimeInner { + scripts: Default::default(), + runtime, + })) + } +} + +impl JsRuntimeApi for JsRuntime { + fn load_script(&self, handle: &Handle, script: &JsScript, reload: bool) { + let mut this = self.borrow_mut(); + let already_loaded = this.scripts.contains_key(handle); + + // Skip if already loaded and we aren't intentionally reloading + if already_loaded && !reload { + return; + } + + // Helper to load script + let mut load_script = || { + // Get the script source code + let code = &script.source; + + // Wrap the script in a closure + let code = format!( + r#" + "strict_mode"; + + ((window) => {{ + {code} + }})(globalThis) + "#, + ); + + // Make script info available to the runtime + this.runtime.op_state().borrow_mut().put(ScriptInfo { + path: script.path.clone(), + }); + + // Run the script and get it's output + let output = this + .runtime + .execute_script(&script.path.to_string_lossy(), &code)?; + + debug!(?script.path, "Loaded script"); + + // Store the module's exported namespace in the script map + this.scripts.insert( + handle.clone_weak(), + LoadedScriptData { + output, + path: script.path.clone(), + }, + ); + + Ok::<_, anyhow::Error>(()) + }; + + // Load script or report errors + if let Err(e) = load_script() { + error!("Error running script: {}", e); + } + } + + fn has_loaded(&self, handle: &Handle) -> bool { + self.borrow().scripts.contains_key(handle) + } + + fn run_script(&self, handle: &Handle, stage: &CoreStage, world: &mut World) { + let mut this = self.borrow_mut(); + let JsRuntimeInner { scripts, runtime } = &mut *this; + + // Get the script output + let script = if let Some(script) = scripts.get(handle) { + script + } else { + return; + }; + + // Make script info available to the runtime + runtime.op_state().borrow_mut().put(ScriptInfo { + path: script.path.clone(), + }); + + with_world(world, runtime, |runtime| { + let scope = &mut runtime.handle_scope(); + let output = v8::Local::new(scope, &script.output); + + // Make sure that script output was an object + let output = if let Ok(value) = v8::Local::::try_from(output) { + value + } else { + warn!(?script.path, "Script init() did not return an object. Skipping."); + return; + }; + + // Figure out which function to call on the exported object + let fn_name_str = match stage { + CoreStage::First => "first", + CoreStage::PreUpdate => "pre_update", + CoreStage::Update => "update", + CoreStage::PostUpdate => "post_update", + CoreStage::Last => "last", + }; + + // Get a javascript value for the name of the function to call + let fn_name = v8::String::new_from_utf8( + scope, + fn_name_str.as_bytes(), + v8::NewStringType::Internalized, + ) + .unwrap(); + + // Get get the named function from the object + let script_fn = if let Some(script_fn) = output.get(scope, fn_name.into()) { + script_fn + } else { + warn!(?script.path, "Getting function named `{}` on script init() value failed. Skipping.", fn_name_str); + return; + }; + + // Make sure the value is a function + let script_fn = if let Ok(value) = v8::Local::::try_from(script_fn) { + value + } else { + // It is valid to not have a function for a script stage so we don't print a warning if + // the function isn't found. + return; + }; + + script_fn.call(scope, output.into(), &[]); + }); + } +} + +/// Helper to insert the Bevy world into into the deno resource map while a closure is executed, and +/// remove the world when the closure finishes. +pub fn with_world( + world: &mut World, + runtime: &mut DenoJsRuntime, + f: impl FnOnce(&mut DenoJsRuntime) -> T, +) -> T { + let resource = runtime + .op_state() + .borrow_mut() + .resource_table + .get::(WorldResource::RID) + .unwrap(); + std::mem::swap(world, &mut *resource.world.borrow_mut()); + + let ret = f(runtime); + + let resource = runtime + .op_state() + .borrow_mut() + .resource_table + .get::(WorldResource::RID) + .unwrap(); + std::mem::swap(world, &mut *resource.world.borrow_mut()); + + ret +} diff --git a/src/runtime/native/native_setup.js b/src/runtime/native/native_setup.js new file mode 100644 index 0000000..6b252b5 --- /dev/null +++ b/src/runtime/native/native_setup.js @@ -0,0 +1,6 @@ +"use_strict"; + +((window) => { + // Set the bevy scripting op function to Deno's opSync function + window.bevyModJsScriptingOpSync = Deno.core.opSync; +})(globalThis); diff --git a/src/runtime/ecs/types.rs b/src/runtime/types.rs similarity index 53% rename from src/runtime/ecs/types.rs rename to src/runtime/types.rs index bd71e4c..aa81a81 100644 --- a/src/runtime/ecs/types.rs +++ b/src/runtime/types.rs @@ -2,8 +2,9 @@ use bevy::{ ecs::component::{ComponentId, ComponentInfo}, prelude::{Entity, World}, }; -use bevy_reflect::TypeRegistryArc; -use deno_core::error::AnyError; +use bevy_ecs_dynamic::reflect_value_ref::{ReflectValueRefBorrow, ReflectValueRefBorrowMut}; +use bevy_reflect::{Reflect, TypeRegistryArc}; +use bevy_reflect_fns::{PassMode, ReflectArg}; use serde::{Deserialize, Serialize}; #[derive(Deserialize, Debug)] @@ -17,7 +18,7 @@ pub enum ComponentIdOrBevyType { } impl ComponentIdOrBevyType { - pub fn component_id(self, world: &World) -> Result { + pub fn component_id(self, world: &World) -> Result { match self { ComponentIdOrBevyType::ComponentId(id) => Ok(ComponentId::from(&id)), ComponentIdOrBevyType::Type { type_name } => { @@ -83,3 +84,66 @@ impl From for JsEntity { } } } + +#[derive(Deserialize)] +pub struct QueryDescriptor { + pub components: Vec, +} + +// Value, from which a `ReflectArg` can be borrowed +pub enum ReflectArgIntermediate<'a> { + Value(ReflectArgIntermediateValue<'a>), + Primitive(Primitive, PassMode), +} + +pub enum ReflectArgIntermediateValue<'a> { + Ref(ReflectValueRefBorrow<'a>), + #[allow(dead_code)] + RefMut(ReflectValueRefBorrowMut<'a>), + Owned(ReflectValueRefBorrow<'a>), +} + +impl<'a> ReflectArgIntermediateValue<'a> { + pub fn as_arg(&mut self) -> ReflectArg<'_> { + match self { + ReflectArgIntermediateValue::Ref(val) => ReflectArg::Ref(&**val), + ReflectArgIntermediateValue::RefMut(val) => ReflectArg::RefMut(&mut **val), + ReflectArgIntermediateValue::Owned(val) => ReflectArg::Owned(&**val), + } + } +} +impl<'a> ReflectArgIntermediate<'a> { + pub fn as_arg(&mut self) -> ReflectArg<'_> { + match self { + ReflectArgIntermediate::Value(val) => val.as_arg(), + ReflectArgIntermediate::Primitive(prim, pass_mode) => prim.as_arg(*pass_mode), + } + } +} + +#[allow(non_camel_case_types)] +#[derive(Deserialize, Debug)] +#[serde(untagged)] +pub enum Primitive { + f32(f32), + f64(f64), + i32(i32), + u32(u32), +} + +impl Primitive { + pub fn as_arg(&mut self, pass_mode: PassMode) -> ReflectArg<'_> { + let reflect: &mut dyn Reflect = match self { + Primitive::f32(val) => val, + Primitive::f64(val) => val, + Primitive::i32(val) => val, + Primitive::u32(val) => val, + }; + + match pass_mode { + PassMode::Ref => ReflectArg::Ref(reflect), + PassMode::RefMut => ReflectArg::RefMut(reflect), + PassMode::Owned => ReflectArg::Owned(reflect), + } + } +} diff --git a/src/runtime/wasm/ecs/info.rs b/src/runtime/wasm/ecs/info.rs new file mode 100644 index 0000000..ade0c68 --- /dev/null +++ b/src/runtime/wasm/ecs/info.rs @@ -0,0 +1,66 @@ +use bevy::{ecs::component::ComponentId, prelude::*, utils::HashSet}; +use wasm_bindgen::prelude::*; + +use crate::runtime::{ + types::{JsComponentInfo, JsEntity}, + wasm::{BevyModJsScripting, WORLD_RID}, +}; + +#[wasm_bindgen] +impl BevyModJsScripting { + pub fn op_world_tostring(&self, rid: u32) -> String { + assert_eq!(rid, WORLD_RID); + let state = self.state(); + let world = &state.world; + + format!("{world:?}") + } + + pub fn op_world_components(&self, rid: u32) -> Result { + assert_eq!(rid, WORLD_RID); + let state = self.state(); + let world = &state.world; + + let resource_components: HashSet = + world.archetypes().resource().components().collect(); + + let infos = world + .components() + .iter() + .filter(|info| !resource_components.contains(&info.id())) + .map(JsComponentInfo::from) + .collect::>(); + + Ok(serde_wasm_bindgen::to_value(&infos)?) + } + + pub fn op_world_resources(&self, rid: u32) -> Result { + assert_eq!(rid, WORLD_RID); + let state = self.state(); + let world = &state.world; + + let infos = world + .archetypes() + .resource() + .components() + .map(|id| world.components().get_info(id).unwrap()) + .map(JsComponentInfo::from) + .collect::>(); + + Ok(serde_wasm_bindgen::to_value(&infos)?) + } + + pub fn op_world_entities(&self, rid: u32) -> Result { + assert_eq!(rid, WORLD_RID); + let mut state = self.state(); + let world = &mut state.world; + + let entities = world + .query::() + .iter(world) + .map(JsEntity::from) + .collect::>(); + + Ok(serde_wasm_bindgen::to_value(&entities)?) + } +} diff --git a/src/runtime/wasm/ecs/mod.rs b/src/runtime/wasm/ecs/mod.rs new file mode 100644 index 0000000..a13a888 --- /dev/null +++ b/src/runtime/wasm/ecs/mod.rs @@ -0,0 +1,4 @@ +mod info; +mod query; +mod resource; +mod value; diff --git a/src/runtime/wasm/ecs/query.rs b/src/runtime/wasm/ecs/query.rs new file mode 100644 index 0000000..008f65d --- /dev/null +++ b/src/runtime/wasm/ecs/query.rs @@ -0,0 +1,49 @@ +use bevy::ecs::component::ComponentId; +use bevy_ecs_dynamic::reflect_value_ref::{query::EcsValueRefQuery, ReflectValueRef}; +use wasm_bindgen::prelude::*; + +use crate::runtime::{ + types::QueryDescriptor, + wasm::{BevyModJsScripting, JsQueryItem, JsRuntimeState, JsValueRef, WORLD_RID}, +}; + +#[wasm_bindgen] +impl BevyModJsScripting { + pub fn op_world_query(&self, rid: u32, query: JsValue) -> Result { + assert_eq!(rid, WORLD_RID); + let mut state = self.state(); + let JsRuntimeState { + world, value_refs, .. + } = &mut *state; + + let descriptor: QueryDescriptor = serde_wasm_bindgen::from_value(query)?; + + let components: Vec = descriptor + .components + .iter() + .map(ComponentId::from) + .collect(); + + let mut query = EcsValueRefQuery::new(world, &components); + let results = query + .iter(world) + .map(|item| { + let components = item + .items + .into_iter() + .map(|value| JsValueRef { + key: value_refs.insert(ReflectValueRef::ecs_ref(value)), + function: None, + }) + .collect(); + + JsQueryItem { + entity: item.entity.into(), + components, + } + }) + .collect::>(); + + Ok(serde_wasm_bindgen::to_value(&results)?) + } +} diff --git a/src/runtime/wasm/ecs/resource.rs b/src/runtime/wasm/ecs/resource.rs new file mode 100644 index 0000000..5fe1cb4 --- /dev/null +++ b/src/runtime/wasm/ecs/resource.rs @@ -0,0 +1,35 @@ +use bevy_ecs_dynamic::reflect_value_ref::{EcsValueRef, ReflectValueRef}; +use wasm_bindgen::prelude::*; + +use crate::runtime::{ + types::ComponentIdOrBevyType, + wasm::{BevyModJsScripting, JsRuntimeState, JsValueRef, WORLD_RID}, + ToJsErr, +}; + +#[wasm_bindgen] +impl BevyModJsScripting { + pub fn op_world_get_resource( + &self, + rid: u32, + component_id: JsValue, + ) -> Result { + assert_eq!(rid, WORLD_RID); + let mut state = self.state(); + let JsRuntimeState { + world, value_refs, .. + } = &mut *state; + + let component_id: ComponentIdOrBevyType = serde_wasm_bindgen::from_value(component_id)?; + let component_id = component_id.component_id(world).to_js_error()?; + + let value_ref = EcsValueRef::resource(world, component_id).to_js_error()?; + + let value_ref = JsValueRef { + key: value_refs.insert(ReflectValueRef::ecs_ref(value_ref)), + function: None, + }; + + Ok(serde_wasm_bindgen::to_value(&value_ref)?) + } +} diff --git a/src/runtime/wasm/ecs/value.rs b/src/runtime/wasm/ecs/value.rs new file mode 100644 index 0000000..52a478c --- /dev/null +++ b/src/runtime/wasm/ecs/value.rs @@ -0,0 +1,306 @@ +use std::{any::TypeId, cell::RefCell, rc::Rc}; + +use bevy_ecs_dynamic::reflect_value_ref::ReflectValueRef; +use bevy_reflect::{ReflectRef, TypeRegistryArc}; +use bevy_reflect_fns::{PassMode, ReflectArg, ReflectMethods}; +use wasm_bindgen::{prelude::*, JsCast}; + +use crate::runtime::{ + types::{Primitive, ReflectArgIntermediate, ReflectArgIntermediateValue}, + wasm::{ + BevyModJsScripting, GetReflectValueRef, JsRuntimeState, JsValueRef, REF_NOT_EXIST, + WORLD_RID, + }, + ToJsErr, +}; + +macro_rules! try_downcast_leaf_get { + ($value:ident for $($ty:ty $(,)?),*) => { + $(if let Some(value) = $value.downcast_ref::<$ty>() { + let value = serde_wasm_bindgen::to_value(value)?; + return Ok(value); + })* + }; +} + +macro_rules! try_downcast_leaf_set { + ($value:ident <- $new_value:ident for $($ty:ty $(,)?),*) => { + $(if let Some(value) = $value.downcast_mut::<$ty>() { + *value = serde_wasm_bindgen::from_value($new_value)?; + return Ok(()); + })* + }; +} + +#[wasm_bindgen] +impl BevyModJsScripting { + pub fn op_value_ref_get( + &self, + rid: u32, + value_ref: JsValue, + path: &str, + ) -> Result { + assert_eq!(rid, WORLD_RID); + let mut state = self.state(); + let JsRuntimeState { + world, + value_refs, + reflect_functions, + .. + } = &mut *state; + + // Get the reflect value ref from the JS argument + let value_ref = value_refs.get_reflect_value_ref(value_ref)?; + + // Load the type registry + let type_registry = world.resource::(); + let type_registry = type_registry.read(); + + // See if we can find any reflect methods for this type in the type registry + let reflect_methods = type_registry + .get_type_data::(value_ref.get(world).to_js_error()?.type_id()); + + // If we found methods for this type + if let Some(reflect_methods) = reflect_methods { + let method_name = path.trim_start_matches('.'); + // If the path we are accessing is a method on the type + if let Some(reflect_function) = reflect_methods.get(method_name.trim_start_matches('.')) + { + // Return a method reference + let value = JsValueRef { + key: value_refs.insert(value_ref.clone()), + function: Some(reflect_functions.insert(reflect_function.clone())), + }; + + return Ok(serde_wasm_bindgen::to_value(&value)?); + } + } + + // If we didn't find a method, add the path to our value ref + let value_ref = value_ref.append_path(path, world).to_js_error()?; + + // Try to downcast the value to a primitive + { + let value = value_ref.get(world).to_js_error()?; + + try_downcast_leaf_get!(value for + u8, u16, u32, u64, u128, usize, + i8, i16, i32, i64, i128, isize, + String, char, bool, f32, f64 + ); + } + + // If not a primitive, just return a new value ref + let object = JsValueRef { + key: value_refs.insert(value_ref), + function: None, + }; + + Ok(serde_wasm_bindgen::to_value(&object)?) + } + + pub fn op_value_ref_set( + &self, + rid: u32, + value_ref: JsValue, + path: &str, + new_value: JsValue, + ) -> Result<(), JsValue> { + assert_eq!(rid, WORLD_RID); + let mut state = self.state(); + let JsRuntimeState { + world, value_refs, .. + } = &mut *state; + + // Get the value ref from the JS arg + let value_ref = value_refs.get_reflect_value_ref(value_ref)?; + + // Access the provided path on the value ref + let mut value_ref = value_ref.append_path(path, world).unwrap(); + + // Get the reflect value + let mut reflect = value_ref.get_mut(world).unwrap(); + + // Try to store a primitive in the value + try_downcast_leaf_set!(reflect <- new_value for + u8, u16, u32, u64, u128, usize, + i8, i16, i32, i64, i128, isize, + String, char, bool, f32, f64 + ); + + Err(JsValue::from_str(&format!( + "could not set value reference: type `{}` is not a primitive type", + reflect.type_name(), + ))) + } + + pub fn op_value_ref_keys(&self, rid: u32, value_ref: JsValue) -> Result { + assert_eq!(rid, WORLD_RID); + let mut state = self.state(); + let JsRuntimeState { + world, value_refs, .. + } = &mut *state; + + // Get the reflect ref from the JS arg + let value_ref = value_refs.get_reflect_value_ref(value_ref)?; + let reflect = value_ref.get(world).unwrap(); + + // Enumerate the fields of the reflected object + let fields = match reflect.reflect_ref() { + ReflectRef::Struct(s) => (0..s.field_len()) + .map(|i| { + let name = s.name_at(i).ok_or_else(|| { + JsValue::from_str(&format!( + "misbehaving Reflect impl on `{}`", + s.type_name() + )) + })?; + Ok(name.to_owned()) + }) + .collect::>()?, + ReflectRef::Tuple(tuple) => (0..tuple.field_len()).map(|i| i.to_string()).collect(), + ReflectRef::TupleStruct(tuple_struct) => (0..tuple_struct.field_len()) + .map(|i| i.to_string()) + .collect(), + _ => Vec::new(), + }; + + Ok(serde_wasm_bindgen::to_value(&fields)?) + } + + pub fn op_value_ref_to_string(&self, rid: u32, value_ref: JsValue) -> Result { + assert_eq!(rid, WORLD_RID); + let mut state = self.state(); + let JsRuntimeState { + world, value_refs, .. + } = &mut *state; + + // Get the value ref from JS arg + let value_ref = value_refs.get_reflect_value_ref(value_ref)?; + let reflect = value_ref.get(world).unwrap(); + + // Return the debug formatted string for the reflected object + Ok(JsValue::from_str(&format!("{reflect:?}"))) + } + + pub fn op_value_ref_call( + &self, + rid: u32, + receiver: JsValue, + args: JsValue, + ) -> Result { + assert_eq!(rid, WORLD_RID); + let mut state = self.state(); + let JsRuntimeState { + world, + value_refs, + reflect_functions, + .. + } = &mut *state; + + // Deserialize the receiver + let receiver: JsValueRef = serde_wasm_bindgen::from_value(receiver)?; + + // Get the receiver's reflect_function + let method_key = receiver + .function + .ok_or("Cannot call non-function ref") + .to_js_error()?; + let method = reflect_functions + .get_mut(method_key) + .ok_or(REF_NOT_EXIST) + .to_js_error()?; + + // Get the receiver's reflect ref + let receiver = value_refs + .get(receiver.key) + .ok_or(REF_NOT_EXIST) + .to_js_error()?; + + // Cast the argumetn list to a JS array + let args: js_sys::Array = args + .dyn_into() + .map_err(|_| "Argument list not an array") + .to_js_error()?; + + // Collect the receiver intermediate value + let receiver_pass_mode = method.signature[0].0; + let receiver_intermediate = match receiver_pass_mode { + PassMode::Ref => ReflectArgIntermediateValue::Ref(receiver.get(world).unwrap()), + PassMode::RefMut => { + unimplemented!("values passed by mutable reference in reflect fn call") + } + PassMode::Owned => ReflectArgIntermediateValue::Owned(receiver.get(world).unwrap()), + }; + let mut receiver_intermediate = ReflectArgIntermediate::Value(receiver_intermediate); + + // Collect the intermediate values for the arguments + let mut arg_intermediates = args + .iter() + .zip(method.signature.iter().skip(1)) + .map(|(arg, &(pass_mode, type_id))| { + // Try to cast the arg as a primitive + let downcast_primitive = match type_id { + type_id if type_id == TypeId::of::() => { + Some(Primitive::f32(serde_wasm_bindgen::from_value(arg.clone())?)) + } + type_id if type_id == TypeId::of::() => { + Some(Primitive::f64(serde_wasm_bindgen::from_value(arg.clone())?)) + } + type_id if type_id == TypeId::of::() => { + Some(Primitive::i32(serde_wasm_bindgen::from_value(arg.clone())?)) + } + type_id if type_id == TypeId::of::() => { + Some(Primitive::u32(serde_wasm_bindgen::from_value(arg.clone())?)) + } + _ => None, + }; + // If the arg cast worked, return a primitive arg + if let Some(primitive) = downcast_primitive { + return Ok(ReflectArgIntermediate::Primitive(primitive, pass_mode)); + } + + // Otherwise, try get the arg as a value ref + let value_ref = value_refs.get_reflect_value_ref(arg)?; + let value_ref = match pass_mode { + PassMode::Ref => { + ReflectArgIntermediateValue::Ref(value_ref.get(world).unwrap()) + } + PassMode::RefMut => { + unimplemented!("values passed by mutable reference in reflect fn call") + } + PassMode::Owned => { + ReflectArgIntermediateValue::Owned(value_ref.get(world).unwrap()) + } + }; + + Ok(ReflectArgIntermediate::Value(value_ref)) + }) + .collect::, JsValue>>()?; + + // Collect references to our intermediates as [`ReflectArg`]s + let mut args: Vec = std::iter::once(&mut receiver_intermediate) + .chain(arg_intermediates.iter_mut()) + .map(|intermediate| intermediate.as_arg()) + .collect(); + + // Finally call the method + let ret = method.call(args.as_mut_slice()).unwrap(); + // And package it's return value as a standalone reflect ref + let ret = Rc::new(RefCell::new(ret)); + let ret = ReflectValueRef::free(ret); + + // Drop our intermediates and args so that we can use `value_refs` again, below. + drop(args); + drop(arg_intermediates); + drop(receiver_intermediate); + + // Return our resulting value ref + let ret = JsValueRef { + key: value_refs.insert(ret), + function: None, + }; + + Ok(serde_wasm_bindgen::to_value(&ret)?) + } +} diff --git a/src/runtime/wasm/log.rs b/src/runtime/wasm/log.rs new file mode 100644 index 0000000..93bbdd0 --- /dev/null +++ b/src/runtime/wasm/log.rs @@ -0,0 +1,31 @@ +use wasm_bindgen::prelude::*; + +use super::{BevyModJsScripting, LOCK_SHOULD_NOT_FAIL}; + +use bevy::utils::tracing::{event, span, Level}; + +#[wasm_bindgen] +impl BevyModJsScripting { + pub fn op_log(&self, level: &str, text: &str) { + let state = self.state.try_lock().expect(LOCK_SHOULD_NOT_FAIL); + let path = &state.current_script_path; + + let level: Level = level.parse().unwrap_or(Level::INFO); + if level == Level::TRACE { + let _span = span!(Level::TRACE, "script", ?path).entered(); + event!(target: "js_runtime", Level::TRACE, "{text}"); + } else if level == Level::DEBUG { + let _span = span!(Level::DEBUG, "script", ?path).entered(); + event!(target: "js_runtime", Level::DEBUG, "{text}"); + } else if level == Level::INFO { + let _span = span!(Level::INFO, "script", ?path).entered(); + event!(target: "js_runtime", Level::INFO, "{text}"); + } else if level == Level::WARN { + let _span = span!(Level::WARN, "script", ?path).entered(); + event!(target: "js_runtime", Level::WARN, "{text}"); + } else if level == Level::ERROR { + let _span = span!(Level::ERROR, "script", ?path).entered(); + event!(target: "js_runtime", Level::ERROR, "{text}"); + } + } +} diff --git a/src/runtime/wasm/mod.rs b/src/runtime/wasm/mod.rs new file mode 100644 index 0000000..32de948 --- /dev/null +++ b/src/runtime/wasm/mod.rs @@ -0,0 +1,231 @@ +mod ecs; +mod log; + +use std::path::PathBuf; +use std::rc::Rc; + +use bevy::prelude::*; +use bevy::utils::HashMap; +use bevy_ecs_dynamic::reflect_value_ref::ReflectValueRef; +use bevy_reflect_fns::ReflectFunction; +use serde::{Deserialize, Serialize}; +use slotmap::SlotMap; +use wasm_bindgen::{prelude::*, JsCast}; +use wasm_mutex::{Mutex, MutexRef}; + +use super::JsRuntimeApi; +use crate::asset::JsScript; +use crate::runtime::types::JsEntity; + +/// Panic message when a mutex lock fails +const LOCK_SHOULD_NOT_FAIL: &str = + "Mutex lock should not fail because there should be no concurrent access"; + +/// Error message thrown when a value ref refers to a value that doesn't exist. +const REF_NOT_EXIST: &str = + "Value referenced does not exist. Each value ref is only valid for the duration of the script \ + execution that it was created in. You may have attempted to use a value from a previous sciprt \ + run."; + +const WORLD_RID: u32 = 0; + +slotmap::new_key_type! { + struct JsValueRefKey; + struct ReflectFunctionKey; +} + +#[derive(Serialize, Deserialize, Debug)] +struct JsValueRef { + key: JsValueRefKey, + function: Option, +} + +#[derive(Serialize)] +struct JsQueryItem { + entity: JsEntity, + components: Vec, +} + +#[wasm_bindgen] +struct BevyModJsScripting { + state: Rc>, +} + +impl BevyModJsScripting { + /// Lock the state and panic if the lock cannot be obtained immediately. + fn state(&self) -> MutexRef { + self.state.try_lock().expect(LOCK_SHOULD_NOT_FAIL) + } +} + +#[derive(Default)] +struct JsRuntimeState { + current_script_path: PathBuf, + world: World, + value_refs: SlotMap, + reflect_functions: SlotMap, +} + +#[wasm_bindgen(module = "/src/runtime/wasm/wasm_setup.js")] +extern "C" { + fn setup_js_globals(bevy_mod_js_scripting: BevyModJsScripting); +} + +pub struct JsRuntime { + scripts: Mutex, ScriptData>>, + state: Rc>, +} + +struct ScriptData { + path: PathBuf, + output: wasm_bindgen::JsValue, +} + +impl FromWorld for JsRuntime { + fn from_world(_: &mut World) -> Self { + let state = Rc::new(Mutex::new(JsRuntimeState::default())); + + setup_js_globals(BevyModJsScripting { + state: state.clone(), + }); + + js_sys::eval(include_str!("../js/ecs.js")).expect("Eval Init JS"); + js_sys::eval(include_str!("../js/log.js")).expect("Eval Init JS"); + + Self { + scripts: Default::default(), + state, + } + } +} + +impl JsRuntimeApi for JsRuntime { + fn load_script(&self, handle: &Handle, script: &JsScript, _reload: bool) { + let function = js_sys::Function::new_no_args(&format!( + r#"return ((window) => {{ + {code} + }})(globalThis);"#, + code = script.source + )); + + let output = match function.call0(&JsValue::UNDEFINED) { + Ok(output) => output, + Err(e) => { + error!(?script.path, "Error executing script: {:?}", e); + return; + } + }; + + self.scripts.try_lock().expect(LOCK_SHOULD_NOT_FAIL).insert( + handle.clone_weak(), + ScriptData { + path: script.path.clone(), + output, + }, + ); + } + + fn has_loaded(&self, handle: &Handle) -> bool { + self.scripts + .try_lock() + .expect(LOCK_SHOULD_NOT_FAIL) + .contains_key(handle) + } + + fn run_script(&self, handle: &Handle, stage: &CoreStage, world: &mut World) { + { + let mut state = self.state.try_lock().expect(LOCK_SHOULD_NOT_FAIL); + std::mem::swap(&mut state.world, world); + } + + let try_run = || { + let scripts = self.scripts.try_lock().expect(LOCK_SHOULD_NOT_FAIL); + let script = scripts + .get(handle) + .ok_or_else(|| anyhow::format_err!("Script not loaded yet"))?; + let output = &script.output; + + { + let mut state = self.state.try_lock().expect(LOCK_SHOULD_NOT_FAIL); + state.value_refs.clear(); + state.reflect_functions.clear(); + state.current_script_path = script.path.clone(); + } + + let output: &js_sys::Object = output.dyn_ref().ok_or_else(|| { + anyhow::format_err!("Script must have a default export that returns an object") + })?; + + let fn_name = match stage { + CoreStage::First => "first", + CoreStage::PreUpdate => "pre_update", + CoreStage::Update => "update", + CoreStage::PostUpdate => "post_update", + CoreStage::Last => "last", + }; + let fn_name_str = wasm_bindgen::intern(fn_name); + let fn_name = wasm_bindgen::JsValue::from_str(fn_name_str); + + if let Ok(script_fn) = js_sys::Reflect::get(output, &fn_name) { + // If a handler isn't specified for this stage, just skip this script + if script_fn.is_undefined() { + return Ok(()); + } + + match script_fn.dyn_ref::() { + Some(script_fn) => { + script_fn.call0(output).map_err(|e| { + anyhow::format_err!("Error running script {fn_name_str} handler: {e:?}") + })?; + } + None => { + warn!( + "Script exported object with {fn_name_str} field, but it was not a \ + function. Ignoring." + ); + } + } + } + + Ok::<_, anyhow::Error>(()) + }; + + if let Err(e) = try_run() { + // TODO: add script path to error + error!("Error running script: {}", e); + } + + let mut state = self.state.try_lock().expect(LOCK_SHOULD_NOT_FAIL); + state.current_script_path = PathBuf::new(); + std::mem::swap(&mut state.world, world); + } +} + +/// Helper trait for mapping errors to [`JsValue`]s +pub trait ToJsErr { + /// Convert the error to a [`JsValue`] + fn to_js_error(self) -> Result; +} + +impl ToJsErr for Result { + fn to_js_error(self) -> Result { + match self { + Ok(ok) => Ok(ok), + Err(e) => Err(JsValue::from_str(&e.to_string())), + } + } +} + +/// Helper trait to get a reflect value ref from the `value_refs` slotmap on [`JsRuntimeState`]. +trait GetReflectValueRef { + /// Casts a [`JsValue`] to a [`JsValueRef`] and loads its [`ReflectValueRef`]. + fn get_reflect_value_ref(&self, value: JsValue) -> Result<&ReflectValueRef, JsValue>; +} + +impl GetReflectValueRef for SlotMap { + fn get_reflect_value_ref(&self, js_value: JsValue) -> Result<&ReflectValueRef, JsValue> { + let value_ref: JsValueRef = serde_wasm_bindgen::from_value(js_value)?; + + self.get(value_ref.key).ok_or(REF_NOT_EXIST).to_js_error() + } +} diff --git a/src/runtime/wasm/wasm_setup.js b/src/runtime/wasm/wasm_setup.js new file mode 100644 index 0000000..90da760 --- /dev/null +++ b/src/runtime/wasm/wasm_setup.js @@ -0,0 +1,40 @@ +export function setup_js_globals(bevyModJsScripting) { + window.bevyModJsScripting = bevyModJsScripting; + window.bevyModJsScriptingOpSync = bevyModJsScriptingOpSync; +} + +function bevyModJsScriptingOpSync(op_name, ...args) { + const WORLD_RID = 0; + switch (op_name) { + case "op_log": + return window.bevyModJsScripting.op_log( + args[0], + JSON.stringify(args[1]) + ); + case "op_world_tostring": + return window.bevyModJsScripting.op_world_tostring(WORLD_RID); + case "op_world_components": + return window.bevyModJsScripting.op_world_components(WORLD_RID); + case "op_world_resources": + return window.bevyModJsScripting.op_world_resources(WORLD_RID); + case "op_world_entities": + return window.bevyModJsScripting.op_world_entities(WORLD_RID); + case "op_world_query": + return window.bevyModJsScripting.op_world_query(WORLD_RID, args[1]); + case "op_world_get_resource": + return window.bevyModJsScripting.op_world_get_resource(WORLD_RID, args[1]); + case "op_value_ref_get": + return window.bevyModJsScripting.op_value_ref_get(WORLD_RID, args[1], args[2]); + case "op_value_ref_set": + return window.bevyModJsScripting.op_value_ref_set(WORLD_RID, args[1], args[2], args[3]); + case "op_value_ref_keys": + return window.bevyModJsScripting.op_value_ref_keys(WORLD_RID, args[1]); + case "op_value_ref_call": + return window.bevyModJsScripting.op_value_ref_call(WORLD_RID, args[1], args[2]); + case "op_value_ref_to_string": + return window.bevyModJsScripting.op_value_ref_to_string(WORLD_RID, args[1]); + default: + console.error(`Op not implemented for browser yet: ${op_name}`); + return; + } +} diff --git a/src/transpile.rs b/src/transpile.rs new file mode 100644 index 0000000..f77cf36 --- /dev/null +++ b/src/transpile.rs @@ -0,0 +1,148 @@ +use std::{ + io::{Cursor, Write}, + path::Path, + sync::{Arc, Mutex}, +}; + +use swc_common::{ + self, + comments::SingleThreadedComments, + errors::{EmitterWriter, Handler}, + sync::Lrc, + BytePos, Globals, Mark, SourceMap, Span, GLOBALS, +}; +use swc_ecma_codegen::{text_writer::JsWriter, Emitter}; +use swc_ecma_parser::{lexer::Lexer, Parser, StringInput, Syntax, TsConfig}; +use swc_ecma_transforms_base::{fixer::fixer, hygiene::hygiene, resolver}; +use swc_ecma_transforms_typescript::strip; +use swc_ecma_visit::{ + swc_ecma_ast::{ModuleDecl, ModuleItem, ReturnStmt, Stmt}, + FoldWith, +}; + +struct SharedWriter(Arc>); + +impl Write for SharedWriter { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + (*self.0.lock().unwrap()).write(buf) + } + + fn flush(&mut self) -> std::io::Result<()> { + (*self.0.lock().unwrap()).flush() + } +} + +pub fn transpile(path: &Path, js: &str) -> Result { + let cm: Lrc = Default::default(); + + cm.new_source_file(path.to_owned().into(), js.to_owned()); + + let error_output = Arc::new(Mutex::new(Cursor::new(Vec::new()))); + let emitter = EmitterWriter::new( + Box::new(SharedWriter(Arc::clone(&error_output))), + None, + true, + false, + ); + let handler = Handler::with_emitter(true, false, Box::new(emitter)); + + let comments = SingleThreadedComments::default(); + + let lexer = Lexer::new( + Syntax::Typescript(TsConfig { + ..Default::default() + }), + Default::default(), + StringInput::new(js, BytePos(0), BytePos(0)), + Some(&comments), + ); + + let mut parser = Parser::new_from(lexer); + + let mut had_error = false; + for e in parser.take_errors() { + had_error = true; + e.into_diagnostic(&handler).emit(); + } + + let module = parser + .parse_module() + .map_err(|e| e.into_diagnostic(&handler).emit()); + + let mut module = match module { + Ok(module) if !had_error => module, + _ => { + let error_msg = + String::from_utf8(std::mem::take(&mut *error_output.lock().unwrap()).into_inner())?; + return Err(anyhow::anyhow!( + "Failed to transpile js to ts:\n{error_msg}" + )); + } + }; + + // Rewrite module import/exports + + let globals = Globals::default(); + let ts = GLOBALS.set(&globals, || { + let unresolved_mark = Mark::new(); + let top_level_mark = Mark::new(); + + let mut body = Vec::new(); + std::mem::swap(&mut body, &mut module.body); + for item in body { + if let ModuleItem::ModuleDecl(decl) = item { + match decl { + ModuleDecl::ExportDefaultExpr(expr) => { + module.body.push(ModuleItem::Stmt(Stmt::Return(ReturnStmt { + span: Span::dummy_with_cmt(), + arg: Some(expr.expr), + }))) + } + ModuleDecl::ExportNamed(_) + | ModuleDecl::ExportDefaultDecl(_) + | ModuleDecl::ExportAll(_) + | ModuleDecl::TsNamespaceExport(_) + | ModuleDecl::ExportDecl(_) => { + anyhow::bail!("Only default expression exports are supported currently") + } + ModuleDecl::TsImportEquals(_) + | ModuleDecl::TsExportAssignment(_) + | ModuleDecl::Import(_) => anyhow::bail!("Imports are not yet supported"), + } + } else { + module.body.push(item); + } + } + + // Conduct identifier scope analysis + let module = module.fold_with(&mut resolver(unresolved_mark, top_level_mark, true)); + + // Remove typescript types + let module = module.fold_with(&mut strip(top_level_mark)); + + // Fix up any identifiers with the same name, but different contexts + let module = module.fold_with(&mut hygiene()); + + // Ensure that we have enough parenthesis. + let module = module.fold_with(&mut fixer(Some(&comments))); + + let mut buf = vec![]; + { + let mut emitter = Emitter { + cfg: swc_ecma_codegen::Config { + minify: false, + ..Default::default() + }, + cm: cm.clone(), + comments: Some(&comments), + wr: JsWriter::new(cm.clone(), "\n", &mut buf, None), + }; + + emitter.emit_module(&module).unwrap(); + } + + Ok(String::from_utf8(buf)) + })??; + + Ok(ts) +} diff --git a/src/ts_to_js.rs b/src/ts_to_js.rs deleted file mode 100644 index 6def0f6..0000000 --- a/src/ts_to_js.rs +++ /dev/null @@ -1,13 +0,0 @@ -use std::path::Path; - -pub fn ts_to_js(path: &Path, code: String) -> Result { - let parsed = deno_ast::parse_module(deno_ast::ParseParams { - specifier: path.display().to_string(), - text_info: deno_ast::SourceTextInfo::from_string(code), - media_type: deno_ast::MediaType::TypeScript, - capture_tokens: false, - scope_analysis: false, - maybe_syntax: None, - })?; - Ok(parsed.transpile(&Default::default())?.text) -} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..b41e0e0 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "rootDir": "./assets", + "target": "es6", + "lib": [ + "es2015" + ] + }, + "include": [ + "./assets/**/*", + "./lib.bevy.d.ts" + ], +} \ No newline at end of file