diff --git a/CHANGELOG.md b/CHANGELOG.md index a547d7ea18..d6da20340e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,30 @@ # Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.15.0](https://github.com/makspll/bevy_mod_scripting/compare/v0.14.0...v0.15.0) - 2025-08-14 + +### Added + +- Use the Handles, Luke! ([#427](https://github.com/makspll/bevy_mod_scripting/pull/427)) + +### Fixed + +- fix version +- fix version + +### Other + +- improve docs, fix issues +- update versions to currently released ones +- Merge remote-tracking branch 'origin/main' into staging +# Changelog + ## [0.13.0](https://github.com/makspll/bevy_mod_scripting/compare/v0.12.0...v0.13.0) - 2025-07-05 ### Added diff --git a/Cargo.toml b/Cargo.toml index 48e8b97359..2a30720cf5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_mod_scripting" -version = "0.14.0" +version = "0.15.0" authors = ["Maksymilian Mozolewski "] edition = "2021" license = "MIT OR Apache-2.0" @@ -74,20 +74,21 @@ bevy = { workspace = true } bevy_math = { workspace = true } bevy_reflect = { workspace = true } bevy_mod_scripting_core = { workspace = true } -bevy_mod_scripting_lua = { path = "crates/languages/bevy_mod_scripting_lua", version = "0.14.0", optional = true } -bevy_mod_scripting_rhai = { path = "crates/languages/bevy_mod_scripting_rhai", version = "0.14.0", optional = true } +bevy_mod_scripting_lua = { path = "crates/languages/bevy_mod_scripting_lua", version = "0.15.0", optional = true } +bevy_mod_scripting_rhai = { path = "crates/languages/bevy_mod_scripting_rhai", version = "0.15.0", optional = true } # bevy_mod_scripting_rune = { path = "crates/languages/bevy_mod_scripting_rune", version = "0.9.0-alpha.2", optional = true } bevy_mod_scripting_functions = { workspace = true } bevy_mod_scripting_derive = { workspace = true } [workspace.dependencies] profiling = { version = "1.0" } + bevy = { version = "0.16.0", default-features = false } bevy_math = { version = "0.16.0" } bevy_reflect = { version = "0.16.0" } -bevy_mod_scripting_core = { path = "crates/bevy_mod_scripting_core", version = "0.14.0" } -bevy_mod_scripting_functions = { path = "crates/bevy_mod_scripting_functions", version = "0.14.0", default-features = false } -bevy_mod_scripting_derive = { path = "crates/bevy_mod_scripting_derive", version = "0.14.0" } +bevy_mod_scripting_core = { path = "crates/bevy_mod_scripting_core", version = "0.15.0" } +bevy_mod_scripting_functions = { path = "crates/bevy_mod_scripting_functions", version = "0.15.0", default-features = false } +bevy_mod_scripting_derive = { path = "crates/bevy_mod_scripting_derive", version = "0.15.0" } # test utilities script_integration_test_harness = { path = "crates/testing_crates/script_integration_test_harness" } @@ -98,7 +99,7 @@ bevy = { workspace = true, default-features = true, features = ["std"] } clap = { version = "4.1", features = ["derive"] } rand = "0.9.1" criterion = { version = "0.5" } -ladfile_builder = { path = "crates/ladfile_builder", version = "0.4.0" } +ladfile_builder = { path = "crates/ladfile_builder", version = "0.4.1" } script_integration_test_harness = { workspace = true } test_utils = { workspace = true } libtest-mimic = "0.8" diff --git a/assets/reload.lua b/assets/reload.lua new file mode 100644 index 0000000000..0d3cbab694 --- /dev/null +++ b/assets/reload.lua @@ -0,0 +1,22 @@ +-- reload.lua +-- +-- An example of the script reload feature. Exercise with this command: +-- ```sh +-- cargo run --features lua54,bevy/file_watcher,bevy/multi_threaded --example run-script -- reload.lua +-- ``` +function on_script_loaded() + world.info("Hello world") +end + +function on_script_unloaded() + world.info("Goodbye world") + return "house" +end + +function on_script_reloaded(value) + if value then + world.info("I'm back. Thanks for the "..value.." keys!") + else + world.info('I have not saved any state before unloading') + end +end diff --git a/assets/tests/add_system/added_systems_run_in_parallel.lua b/assets/tests/add_system/added_systems_run_in_parallel.lua index bd7951c729..8c8afb7f15 100644 --- a/assets/tests/add_system/added_systems_run_in_parallel.lua +++ b/assets/tests/add_system/added_systems_run_in_parallel.lua @@ -1,34 +1,35 @@ - function on_test() - local post_update_schedule = world.get_schedule_by_name("PostUpdate") - - local test_system = post_update_schedule:get_system_by_name("on_test_post_update") - - local system_a = world.add_system( - post_update_schedule, - system_builder("custom_system_a", script_id) - :after(test_system) - ) - - local system_b = world.add_system( - post_update_schedule, - system_builder("custom_system_b", script_id) - :after(test_system) - ) + local post_update_schedule = world.get_schedule_by_name("PostUpdate") + + local test_system = post_update_schedule:get_system_by_name("on_test_post_update") + + local script_attachment = ScriptAttachment.new_entity_script(entity, script_asset) - -- generate a schedule graph and verify it's what we expect - local dot_graph = post_update_schedule:render_dot() + local system_a = world.add_system( + post_update_schedule, + system_builder("custom_system_a", script_attachment) + :after(test_system) + ) - local expected_dot_graph = [[ + local system_b = world.add_system( + post_update_schedule, + system_builder("custom_system_b", script_attachment) + :after(test_system) + ) + + -- generate a schedule graph and verify it's what we expect + local dot_graph = post_update_schedule:render_dot() + + local expected_dot_graph = [[ digraph { node_0 [label="bevy_asset::assets::Assets::asset_events"]; node_1 [label="bevy_asset::assets::Assets::asset_events"]; node_2 [label="bevy_asset::assets::Assets<()>::asset_events"]; node_3 [label="bevy_asset::assets::Assets::asset_events"]; node_4 [label="bevy_mod_scripting_core::bindings::allocator::garbage_collector"]; - node_5 [label="on_test_post_update"]; - node_6 [label="script_integration_test_harness::dummy_before_post_update_system"]; - node_7 [label="script_integration_test_harness::dummy_post_update_system"]; + node_5 [label="script_integration_test_harness::dummy_before_post_update_system"]; + node_6 [label="script_integration_test_harness::dummy_post_update_system"]; + node_7 [label="on_test_post_update"]; node_8 [label="custom_system_a"]; node_9 [label="custom_system_b"]; node_10 [label="SystemSet AssetEvents"]; @@ -42,11 +43,10 @@ digraph { node_4 -> node_11 [color=red, label="child of", arrowhead=diamond]; node_8 -> node_12 [color=red, label="child of", arrowhead=diamond]; node_9 -> node_13 [color=red, label="child of", arrowhead=diamond]; - node_5 -> node_8 [color=blue, label="runs before", arrowhead=normal]; - node_5 -> node_9 [color=blue, label="runs before", arrowhead=normal]; - node_6 -> node_7 [color=blue, label="runs before", arrowhead=normal]; + node_5 -> node_6 [color=blue, label="runs before", arrowhead=normal]; + node_7 -> node_8 [color=blue, label="runs before", arrowhead=normal]; + node_7 -> node_9 [color=blue, label="runs before", arrowhead=normal]; } ]] - - assert_str_eq(dot_graph, expected_dot_graph, "Expected the schedule graph to match the expected graph") + assert_str_eq(dot_graph, expected_dot_graph, "Expected the schedule graph to match the expected graph") end diff --git a/assets/tests/add_system/added_systems_run_in_parallel.rhai b/assets/tests/add_system/added_systems_run_in_parallel.rhai index b49d41b9b3..cc59990d73 100644 --- a/assets/tests/add_system/added_systems_run_in_parallel.rhai +++ b/assets/tests/add_system/added_systems_run_in_parallel.rhai @@ -1,17 +1,18 @@ fn on_test() { let post_update_schedule = world.get_schedule_by_name.call("PostUpdate"); + let script_attachment = ScriptAttachment.new_entity_script.call(entity, script_asset); let test_system = post_update_schedule.get_system_by_name.call("on_test_post_update"); let system_a = world.add_system.call( post_update_schedule, - system_builder.call("custom_system_a", script_id) + system_builder.call("custom_system_a", script_attachment) .after.call(test_system) ); let system_b = world.add_system.call( post_update_schedule, - system_builder.call("custom_system_b", script_id) + system_builder.call("custom_system_b", script_attachment) .after.call(test_system) ); @@ -25,9 +26,9 @@ digraph { node_2 [label="bevy_asset::assets::Assets<()>::asset_events"]; node_3 [label="bevy_asset::assets::Assets::asset_events"]; node_4 [label="bevy_mod_scripting_core::bindings::allocator::garbage_collector"]; - node_5 [label="on_test_post_update"]; - node_6 [label="script_integration_test_harness::dummy_before_post_update_system"]; - node_7 [label="script_integration_test_harness::dummy_post_update_system"]; + node_5 [label="script_integration_test_harness::dummy_before_post_update_system"]; + node_6 [label="script_integration_test_harness::dummy_post_update_system"]; + node_7 [label="on_test_post_update"]; node_8 [label="custom_system_a"]; node_9 [label="custom_system_b"]; node_10 [label="SystemSet AssetEvents"]; @@ -41,9 +42,9 @@ digraph { node_4 -> node_11 [color=red, label="child of", arrowhead=diamond]; node_8 -> node_12 [color=red, label="child of", arrowhead=diamond]; node_9 -> node_13 [color=red, label="child of", arrowhead=diamond]; - node_5 -> node_8 [color=blue, label="runs before", arrowhead=normal]; - node_5 -> node_9 [color=blue, label="runs before", arrowhead=normal]; - node_6 -> node_7 [color=blue, label="runs before", arrowhead=normal]; + node_5 -> node_6 [color=blue, label="runs before", arrowhead=normal]; + node_7 -> node_8 [color=blue, label="runs before", arrowhead=normal]; + node_7 -> node_9 [color=blue, label="runs before", arrowhead=normal]; }`; assert_str_eq.call(dot_graph, expected_dot_graph, "Expected the schedule graph to match the expected graph"); diff --git a/assets/tests/add_system/adds_system_in_correct_order.lua b/assets/tests/add_system/adds_system_in_correct_order.lua index 44a844d77b..ae51b6a476 100644 --- a/assets/tests/add_system/adds_system_in_correct_order.lua +++ b/assets/tests/add_system/adds_system_in_correct_order.lua @@ -1,41 +1,41 @@ -- add two systems, one before and one after the existing `on_test_post_update` callback, then assert all systems have run -- in the `on_test_last` callback -local runs = {} +local runs = {} -- runs on `Update` function on_test() local post_update_schedule = world.get_schedule_by_name("PostUpdate") - + local test_system = post_update_schedule:get_system_by_name("on_test_post_update") - + local script_attachment = ScriptAttachment.new_entity_script(entity, script_asset) + local system_after = world.add_system( post_update_schedule, - system_builder("custom_system_after", script_id) - :after(test_system) + system_builder("custom_system_after", script_attachment) + :after(test_system) ) - + local system_before = world.add_system( post_update_schedule, - system_builder("custom_system_before", script_id) - :before(test_system) - ) + system_builder("custom_system_before", script_attachment) + :before(test_system) + ) local script_system_between = world.add_system( post_update_schedule, - system_builder("custom_system_between", script_id) - :after(test_system) - :before(system_after) + system_builder("custom_system_between", script_attachment) + :after(test_system) + :before(system_after) ) end - function custom_system_before() print("custom_system_before") runs[#runs + 1] = "custom_system_before" end --- runs on post_update +-- runs on post_update function on_test_post_update() print("on_test_post_update") runs[#runs + 1] = "on_test_post_update" @@ -53,9 +53,10 @@ end -- runs in the `Last` bevy schedule function on_test_last() - assert(#runs == 4, "Expected 4 runs, got: " .. #runs) + local string_table = table.concat(runs, ", ") + assert(#runs == 4, "Expected 4 runs, got: " .. tostring(string_table)) assert(runs[1] == "custom_system_before", "Expected custom_system_before to run first, got: " .. runs[1]) assert(runs[2] == "on_test_post_update", "Expected on_test_post_update to run second, got: " .. runs[2]) assert(runs[3] == "custom_system_between", "Expected custom_system_between to run third, got: " .. runs[3]) assert(runs[4] == "custom_system_after", "Expected custom_system_after to run second, got: " .. runs[4]) -end \ No newline at end of file +end diff --git a/assets/tests/add_system/adds_system_in_correct_order.rhai b/assets/tests/add_system/adds_system_in_correct_order.rhai index 6bfc1a6e47..dd0c4e7e55 100644 --- a/assets/tests/add_system/adds_system_in_correct_order.rhai +++ b/assets/tests/add_system/adds_system_in_correct_order.rhai @@ -3,16 +3,17 @@ let runs = []; fn on_test() { let post_update_schedule = world.get_schedule_by_name.call("PostUpdate"); let test_system = post_update_schedule.get_system_by_name.call("on_test_post_update"); + let script_attachment = ScriptAttachment.new_entity_script.call(entity, script_asset); - let builder_after = system_builder.call("custom_system_after", script_id) + let builder_after = system_builder.call("custom_system_after", script_attachment) .after.call(test_system); let system_after = world.add_system.call(post_update_schedule, builder_after); - let builder_before = system_builder.call("custom_system_before", script_id) + let builder_before = system_builder.call("custom_system_before", script_attachment) .before.call(test_system); let system_before = world.add_system.call(post_update_schedule, builder_before); - let builder_between = system_builder.call("custom_system_between", script_id) + let builder_between = system_builder.call("custom_system_between", script_attachment) .after.call(test_system) .before.call(system_after); diff --git a/assets/tests/add_system/system_can_access_unspecified_resource_in_exclusive_system__RETURNS.lua b/assets/tests/add_system/system_can_access_unspecified_resource_in_exclusive_system.lua similarity index 78% rename from assets/tests/add_system/system_can_access_unspecified_resource_in_exclusive_system__RETURNS.lua rename to assets/tests/add_system/system_can_access_unspecified_resource_in_exclusive_system.lua index 544513648b..2ffdb57195 100644 --- a/assets/tests/add_system/system_can_access_unspecified_resource_in_exclusive_system__RETURNS.lua +++ b/assets/tests/add_system/system_can_access_unspecified_resource_in_exclusive_system.lua @@ -1,12 +1,11 @@ - runs = {} function on_test() local post_update_schedule = world.get_schedule_by_name("PostUpdate") - + local script_attachment = ScriptAttachment.new_entity_script(entity, script_asset) world.add_system( post_update_schedule, - system_builder("my_exclusive_system", script_id):exclusive() + system_builder("my_exclusive_system", script_attachment):exclusive() ) return true @@ -21,7 +20,6 @@ function my_exclusive_system() assert(res ~= nil, "Expected to get resource but got nil") end - function on_test_post_update() return true end @@ -29,4 +27,4 @@ end function on_test_last() assert(#runs == 1, "Expected 1 runs, got: " .. #runs) return true -end \ No newline at end of file +end diff --git a/assets/tests/add_system/system_can_access_unspecified_resource_in_exclusive_system__RETURNS.rhai b/assets/tests/add_system/system_can_access_unspecified_resource_in_exclusive_system.rhai similarity index 78% rename from assets/tests/add_system/system_can_access_unspecified_resource_in_exclusive_system__RETURNS.rhai rename to assets/tests/add_system/system_can_access_unspecified_resource_in_exclusive_system.rhai index 88413e440f..41bb1754d0 100644 --- a/assets/tests/add_system/system_can_access_unspecified_resource_in_exclusive_system__RETURNS.rhai +++ b/assets/tests/add_system/system_can_access_unspecified_resource_in_exclusive_system.rhai @@ -2,10 +2,11 @@ let runs = []; fn on_test() { let post_update_schedule = world.get_schedule_by_name.call("PostUpdate"); - + let script_attachment = ScriptAttachment.new_entity_script.call(entity, script_asset); + world.add_system.call( post_update_schedule, - system_builder.call("my_exclusive_system", script_id).exclusive.call() + system_builder.call("my_exclusive_system", script_attachment).exclusive.call() ); return true; diff --git a/assets/tests/add_system/system_cannot_access_unspecified_resource_in_non_exclusive_system__RETURNS.lua b/assets/tests/add_system/system_cannot_access_unspecified_resource_in_non_exclusive_system.lua similarity index 80% rename from assets/tests/add_system/system_cannot_access_unspecified_resource_in_non_exclusive_system__RETURNS.lua rename to assets/tests/add_system/system_cannot_access_unspecified_resource_in_non_exclusive_system.lua index 0d94f730a9..352caec683 100644 --- a/assets/tests/add_system/system_cannot_access_unspecified_resource_in_non_exclusive_system__RETURNS.lua +++ b/assets/tests/add_system/system_cannot_access_unspecified_resource_in_non_exclusive_system.lua @@ -1,12 +1,12 @@ - runs = {} function on_test() local post_update_schedule = world.get_schedule_by_name("PostUpdate") - + local script_attachment = ScriptAttachment.new_entity_script(entity, script_asset) + world.add_system( post_update_schedule, - system_builder("my_non_exclusive_system", script_id) + system_builder("my_non_exclusive_system", script_attachment) ) return true @@ -23,7 +23,6 @@ function my_non_exclusive_system() end, ".*annot claim access to.*") end - function on_test_post_update() return true end @@ -31,4 +30,4 @@ end function on_test_last() assert(#runs == 1, "Expected 1 runs, got: " .. #runs) return true -end \ No newline at end of file +end diff --git a/assets/tests/add_system/system_cannot_access_unspecified_resource_in_non_exclusive_system__RETURNS.rhai b/assets/tests/add_system/system_cannot_access_unspecified_resource_in_non_exclusive_system.rhai similarity index 82% rename from assets/tests/add_system/system_cannot_access_unspecified_resource_in_non_exclusive_system__RETURNS.rhai rename to assets/tests/add_system/system_cannot_access_unspecified_resource_in_non_exclusive_system.rhai index f82bb1378b..fb389dc654 100644 --- a/assets/tests/add_system/system_cannot_access_unspecified_resource_in_non_exclusive_system__RETURNS.rhai +++ b/assets/tests/add_system/system_cannot_access_unspecified_resource_in_non_exclusive_system.rhai @@ -2,7 +2,8 @@ let runs = []; fn on_test() { let post_update_schedule = world.get_schedule_by_name.call("PostUpdate"); - world.add_system.call(post_update_schedule, system_builder.call("my_non_exclusive_system", script_id)); + let script_attachment = ScriptAttachment.new_entity_script.call(entity, script_asset); + world.add_system.call(post_update_schedule, system_builder.call("my_non_exclusive_system", script_attachment)); return true; } diff --git a/assets/tests/add_system/system_with_parameters__RETURNS.lua b/assets/tests/add_system/system_with_parameters.lua similarity index 82% rename from assets/tests/add_system/system_with_parameters__RETURNS.lua rename to assets/tests/add_system/system_with_parameters.lua index 700658214a..3f00f9f9a8 100644 --- a/assets/tests/add_system/system_with_parameters__RETURNS.lua +++ b/assets/tests/add_system/system_with_parameters.lua @@ -1,4 +1,3 @@ - runs = {} local ResourceTypeA = world.get_type_by_name("TestResource") local ResourceTypeB = world.get_type_by_name("TestResourceWithVariousFields") @@ -7,7 +6,9 @@ local ComponentB = world.get_type_by_name("CompWithDefaultAndComponentData") function on_test() local post_update_schedule = world.get_schedule_by_name("PostUpdate") - + + + local script_attachment = ScriptAttachment.new_entity_script(entity, script_asset) local entity = world.spawn() local entity2 = world.spawn() @@ -17,18 +18,19 @@ function on_test() world.add_default_component(entity2, ComponentA) world.add_default_component(entity2, ComponentB) + world.add_system( post_update_schedule, - system_builder("my_parameterised_system", script_id) - :resource(ResourceTypeA) - :query(world.query():component(ComponentA):component(ComponentB)) - :resource(ResourceTypeB) + system_builder("my_parameterised_system", script_attachment) + :resource(ResourceTypeA) + :query(world.query():component(ComponentA):component(ComponentB)) + :resource(ResourceTypeB) ) return true end -function my_parameterised_system(resourceA,query,resourceB) +function my_parameterised_system(resourceA, query, resourceB) print("my_parameterised_system") runs[#runs + 1] = "my_non_exclusive_system" @@ -39,7 +41,7 @@ function my_parameterised_system(resourceA,query,resourceB) assert(#resourceA.bytes == 6, "Expected 6 bytes, got: " .. #resourceA.bytes) assert(resourceB.string == "Initial Value", "Expected 'Initial Value', got: " .. resourceB.string) assert(#query == 2, "Expected 3 results, got: " .. #query) - for i,result in pairs(query) do + for i, result in pairs(query) do components = result:components() assert(#components == 2, "Expected 2 components, got " .. #components) local componentA = components[1] @@ -49,7 +51,6 @@ function my_parameterised_system(resourceA,query,resourceB) end end - function on_test_post_update() return true end @@ -57,4 +58,4 @@ end function on_test_last() assert(#runs == 1, "Expected 1 runs, got: " .. #runs) return true -end \ No newline at end of file +end diff --git a/assets/tests/add_system/system_with_parameters__RETURNS.rhai b/assets/tests/add_system/system_with_parameters.rhai similarity index 94% rename from assets/tests/add_system/system_with_parameters__RETURNS.rhai rename to assets/tests/add_system/system_with_parameters.rhai index bbd01872c0..4cbd9ebe5e 100644 --- a/assets/tests/add_system/system_with_parameters__RETURNS.rhai +++ b/assets/tests/add_system/system_with_parameters.rhai @@ -8,15 +8,17 @@ let ComponentB = world.get_type_by_name.call("CompWithDefaultAndComponentData" fn on_test() { let post_update_schedule = world.get_schedule_by_name.call("PostUpdate"); + let script_attachment = ScriptAttachment.new_entity_script.call(entity, script_asset); let entity = world.spawn_.call(); let entity2 = world.spawn_.call(); - + world.add_default_component.call(entity, ComponentA); world.add_default_component.call(entity, ComponentB); world.add_default_component.call(entity2, ComponentA); world.add_default_component.call(entity2, ComponentB); - let built_system = system_builder.call("my_parameterised_system", script_id) + + let built_system = system_builder.call("my_parameterised_system", script_attachment) .resource.call(ResourceTypeA) .query.call(world.query.call().component.call(ComponentA).with_.call(ComponentB)) .resource.call(ResourceTypeB); @@ -47,10 +49,8 @@ fn my_parameterised_system(resourceA, query, resourceB) { } fn on_test_post_update() { - return true; } fn on_test_last() { assert(runs.len == 1, "Expected 1 runs, got: " + runs.len); - return true; } \ No newline at end of file diff --git a/assets/tests/api_availability/api_available_on_callback_and_on_load__RETURN.lua b/assets/tests/api_availability/api_available_on_callback_and_on_load.lua similarity index 100% rename from assets/tests/api_availability/api_available_on_callback_and_on_load__RETURN.lua rename to assets/tests/api_availability/api_available_on_callback_and_on_load.lua diff --git a/assets/tests/api_availability/api_available_on_callback_and_on_load__RETURN.rhai b/assets/tests/api_availability/api_available_on_callback_and_on_load.rhai similarity index 100% rename from assets/tests/api_availability/api_available_on_callback_and_on_load__RETURN.rhai rename to assets/tests/api_availability/api_available_on_callback_and_on_load.rhai diff --git a/assets/tests/handle/handle_asset_path_when_script_loaded.lua b/assets/tests/handle/handle_asset_path_when_script_loaded.lua new file mode 100644 index 0000000000..f0c5f301aa --- /dev/null +++ b/assets/tests/handle/handle_asset_path_when_script_loaded.lua @@ -0,0 +1,14 @@ +local expected_asset_path = "tests/handle/handle_asset_path_when_script_loaded.lua" +function normalize_path(path) + return string.gsub(tostring(path), "\\", "/") +end + +local normalized_gotten_asset_path = normalize_path(script_asset:asset_path()) +assert(normalized_gotten_asset_path == expected_asset_path, + "Expected script asset path to match, got :" .. normalized_gotten_asset_path) + +function on_test() + local normalized_gotten_asset_path = normalize_path(script_asset:asset_path()) + assert(normalized_gotten_asset_path == expected_asset_path, + "Expected script asset path to match, got: " .. normalized_gotten_asset_path) +end diff --git a/assets/tests/lifecycle/default/entity_script/callback/callback.lua b/assets/tests/lifecycle/default/entity_script/callback/callback.lua new file mode 100644 index 0000000000..c0dff08f63 --- /dev/null +++ b/assets/tests/lifecycle/default/entity_script/callback/callback.lua @@ -0,0 +1,3 @@ +function on_test(arg1) + return "got: " .. arg1 +end diff --git a/assets/tests/lifecycle/default/entity_script/callback/scenario.txt b/assets/tests/lifecycle/default/entity_script/callback/scenario.txt new file mode 100644 index 0000000000..ef1850490c --- /dev/null +++ b/assets/tests/lifecycle/default/entity_script/callback/scenario.txt @@ -0,0 +1,19 @@ +SetCurrentLanguage language="@this_script_language" +InstallPlugin +SetupHandler OnTest=null, Update=null +SetupHandler OnTestPostUpdate=null, PostUpdate=null +SetupHandler Last=null, OnTestLast=null +FinalizeApp + +LoadScriptAs as_name="@this_script", path="@this_script" +WaitForScriptLoaded name="@this_script" +SpawnEntityWithScript name="test_entity", script="@this_script" +RunUpdateOnce + +EmitScriptCallbackEvent emit_response=true, entity="test_entity", label="OnTest", language=null, recipients="EntityScript", script="@this_script", string_value="arg1" +RunUpdateOnce +AssertCallbackSuccess attachment="EntityScript", entity="test_entity", label="OnTest", script="@this_script", expect_string_value="got: arg1" + +EmitScriptCallbackEvent entity="test_entity", label="OnTest", language=null, recipients="EntityScript", script="@this_script", string_value="arg1" +RunUpdateOnce +AssertNoCallbackResponsesEmitted \ No newline at end of file diff --git a/assets/tests/lifecycle/default/entity_script/loading/lifecycle.lua b/assets/tests/lifecycle/default/entity_script/loading/lifecycle.lua new file mode 100644 index 0000000000..9dff611cc1 --- /dev/null +++ b/assets/tests/lifecycle/default/entity_script/loading/lifecycle.lua @@ -0,0 +1,11 @@ +function on_script_loaded() + return "loaded!" +end + +function on_script_unloaded() + return "unloaded!" +end + +function on_script_reloaded(val) + return "reloaded with: " .. val +end diff --git a/assets/tests/lifecycle/default/entity_script/loading/scenario.txt b/assets/tests/lifecycle/default/entity_script/loading/scenario.txt new file mode 100644 index 0000000000..96fe1a2bbe --- /dev/null +++ b/assets/tests/lifecycle/default/entity_script/loading/scenario.txt @@ -0,0 +1,31 @@ +SetCurrentLanguage language="@this_script_language" +InstallPlugin emit_responses=true +FinalizeApp + +// load script a +LoadScriptAs as_name="script_a", path="lifecycle.lua" +WaitForScriptLoaded name="script_a" +SpawnEntityWithScript name="test_entity_a", script="script_a" +RunUpdateOnce +AssertCallbackSuccess attachment="EntityScript", entity="test_entity_a", label="OnScriptLoaded", script="script_a", expect_string_value="loaded!" +AssertNoCallbackResponsesEmitted + +// reload script_a, with the same content +ReloadScriptFrom script="script_a", path="lifecycle.lua" +RunUpdateOnce +AssertCallbackSuccess attachment="EntityScript", entity="test_entity_a", label="OnScriptUnloaded", script="script_a", expect_string_value="unloaded!" +AssertCallbackSuccess attachment="EntityScript", entity="test_entity_a", label="OnScriptLoaded", script="script_a", expect_string_value="loaded!" +AssertCallbackSuccess attachment="EntityScript", entity="test_entity_a", label="OnScriptReloaded", script="script_a", expect_string_value="reloaded with: unloaded!" +AssertNoCallbackResponsesEmitted + +// now first drop the script asset, assert that does nothing yet +DropScriptAsset script="script_a" +RunUpdateOnce +AssertNoCallbackResponsesEmitted + +// now despawn the entity, expect unloading +DespawnEntity entity="test_entity_a" +RunUpdateOnce +AssertCallbackSuccess attachment="EntityScript", entity="test_entity_a", label="OnScriptUnloaded", script="script_a", expect_string_value="unloaded!" +AssertNoCallbackResponsesEmitted +AssertContextResidents attachment="EntityScript", script="script_a", entity="test_entity_a", residents_num=0 \ No newline at end of file diff --git a/assets/tests/lifecycle/default/multi_language/multi_lang.lua b/assets/tests/lifecycle/default/multi_language/multi_lang.lua new file mode 100644 index 0000000000..7da785e583 --- /dev/null +++ b/assets/tests/lifecycle/default/multi_language/multi_lang.lua @@ -0,0 +1,3 @@ +function on_test() + return "Hi from Lua!" +end diff --git a/assets/tests/lifecycle/default/multi_language/multi_lang.rhai b/assets/tests/lifecycle/default/multi_language/multi_lang.rhai new file mode 100644 index 0000000000..cc0d4a5171 --- /dev/null +++ b/assets/tests/lifecycle/default/multi_language/multi_lang.rhai @@ -0,0 +1,3 @@ +fn on_test(){ + return "Hi from Rhai!"; +} diff --git a/assets/tests/lifecycle/default/multi_language/scenario.txt b/assets/tests/lifecycle/default/multi_language/scenario.txt new file mode 100644 index 0000000000..607a4d212a --- /dev/null +++ b/assets/tests/lifecycle/default/multi_language/scenario.txt @@ -0,0 +1,41 @@ +// #main_script multi_lang.lua +SetCurrentLanguage language="Lua" +InstallPlugin emit_responses=false +SetupHandler OnTest=null, Update=null + +SetCurrentLanguage language="Rhai" +InstallPlugin emit_responses=false +SetupHandler OnTest=null, Update=null +FinalizeApp + +// load lua script +SetCurrentLanguage language="Lua" +LoadScriptAs as_name="script_lua", path="multi_lang.lua" +WaitForScriptLoaded name="script_lua" +AttachStaticScript script="script_lua" + +// load rhai script +SetCurrentLanguage language="Rhai" +LoadScriptAs as_name="script_rhai", path="multi_lang.rhai" +WaitForScriptLoaded name="script_rhai" +AttachStaticScript script="script_rhai" + +// expect no responses +RunUpdateOnce +AssertNoCallbackResponsesEmitted + +// emit callbacks +EmitScriptCallbackEvent emit_response=true, label="OnTest", language="Lua", recipients="AllScripts", script="script_lua" +RunUpdateOnce +AssertCallbackSuccess attachment="StaticScript", label="OnTest", language="Lua", script="script_lua", expect_string_value="Hi from Lua!" +AssertNoCallbackResponsesEmitted + +EmitScriptCallbackEvent emit_response=true, label="OnTest", language="Rhai", recipients="AllScripts", script="script_rhai" +RunUpdateOnce +AssertCallbackSuccess attachment="StaticScript", label="OnTest", language="Rhai", script="script_rhai", expect_string_value="Hi from Rhai!" +AssertNoCallbackResponsesEmitted + +EmitScriptCallbackEvent emit_response=true, label="OnTest", recipients="AllScripts", script="script_lua", expect_string_value="Hi from Lua!" +RunUpdateOnce +AssertCallbackSuccess attachment="StaticScript", label="OnTest", language="Lua", script="script_lua" +AssertCallbackSuccess attachment="StaticScript", label="OnTest", language="Rhai", script="script_rhai" \ No newline at end of file diff --git a/assets/tests/lifecycle/default/static_script/callback/callback.lua b/assets/tests/lifecycle/default/static_script/callback/callback.lua new file mode 100644 index 0000000000..c0dff08f63 --- /dev/null +++ b/assets/tests/lifecycle/default/static_script/callback/callback.lua @@ -0,0 +1,3 @@ +function on_test(arg1) + return "got: " .. arg1 +end diff --git a/assets/tests/lifecycle/default/static_script/callback/scenario.txt b/assets/tests/lifecycle/default/static_script/callback/scenario.txt new file mode 100644 index 0000000000..39431b5d8f --- /dev/null +++ b/assets/tests/lifecycle/default/static_script/callback/scenario.txt @@ -0,0 +1,19 @@ +SetCurrentLanguage language="@this_script_language" +InstallPlugin +SetupHandler OnTest=null, Update=null +SetupHandler OnTestPostUpdate=null, PostUpdate=null +SetupHandler Last=null, OnTestLast=null +FinalizeApp + +LoadScriptAs as_name="@this_script", path="@this_script" +WaitForScriptLoaded name="@this_script" +AttachStaticScript script="@this_script" +RunUpdateOnce + +EmitScriptCallbackEvent emit_response=true, label="OnTest", language=null, recipients="StaticScript", script="@this_script", string_value="arg1" +RunUpdateOnce +AssertCallbackSuccess attachment="StaticScript", label="OnTest", script="@this_script", expect_string_value="got: arg1" + +EmitScriptCallbackEvent label="OnTest", language=null, recipients="StaticScript", script="@this_script", string_value="arg1" +RunUpdateOnce +AssertNoCallbackResponsesEmitted \ No newline at end of file diff --git a/assets/tests/lifecycle/default/static_script/loading/lifecycle.lua b/assets/tests/lifecycle/default/static_script/loading/lifecycle.lua new file mode 100644 index 0000000000..9dff611cc1 --- /dev/null +++ b/assets/tests/lifecycle/default/static_script/loading/lifecycle.lua @@ -0,0 +1,11 @@ +function on_script_loaded() + return "loaded!" +end + +function on_script_unloaded() + return "unloaded!" +end + +function on_script_reloaded(val) + return "reloaded with: " .. val +end diff --git a/assets/tests/lifecycle/default/static_script/loading/scenario.txt b/assets/tests/lifecycle/default/static_script/loading/scenario.txt new file mode 100644 index 0000000000..2e408f32f3 --- /dev/null +++ b/assets/tests/lifecycle/default/static_script/loading/scenario.txt @@ -0,0 +1,31 @@ +SetCurrentLanguage language="@this_script_language" +InstallPlugin emit_responses=true +FinalizeApp + +// load script a +LoadScriptAs as_name="script_a", path="lifecycle.lua" +WaitForScriptLoaded name="script_a" +AttachStaticScript script="script_a" +RunUpdateOnce +AssertCallbackSuccess attachment="StaticScript", label="OnScriptLoaded", script="script_a", expect_string_value="loaded!" +AssertNoCallbackResponsesEmitted + +// reload script_a, with the same content +ReloadScriptFrom script="script_a", path="lifecycle.lua" +RunUpdateOnce +AssertCallbackSuccess attachment="StaticScript", label="OnScriptUnloaded", script="script_a", expect_string_value="unloaded!" +AssertCallbackSuccess attachment="StaticScript", label="OnScriptLoaded", script="script_a", expect_string_value="loaded!" +AssertCallbackSuccess attachment="StaticScript", label="OnScriptReloaded", script="script_a", expect_string_value="reloaded with: unloaded!" +AssertNoCallbackResponsesEmitted + +// now first drop the script asset, assert that does nothing yet +DropScriptAsset script="script_a" +RunUpdateOnce +AssertNoCallbackResponsesEmitted + +// now detach the script, expect unloading +DetachStaticScript script="script_a" +RunUpdateOnce +AssertCallbackSuccess attachment="StaticScript", label="OnScriptUnloaded", script="script_a", expect_string_value="unloaded!" +AssertNoCallbackResponsesEmitted +AssertContextResidents attachment="StaticScript", script="script_a", residents_num=0 \ No newline at end of file diff --git a/assets/tests/lifecycle/shared/entity_script/lifecycle.lua b/assets/tests/lifecycle/shared/entity_script/lifecycle.lua new file mode 100644 index 0000000000..9dff611cc1 --- /dev/null +++ b/assets/tests/lifecycle/shared/entity_script/lifecycle.lua @@ -0,0 +1,11 @@ +function on_script_loaded() + return "loaded!" +end + +function on_script_unloaded() + return "unloaded!" +end + +function on_script_reloaded(val) + return "reloaded with: " .. val +end diff --git a/assets/tests/lifecycle/shared/entity_script/lifecycle_b.lua b/assets/tests/lifecycle/shared/entity_script/lifecycle_b.lua new file mode 100644 index 0000000000..34373a9fdb --- /dev/null +++ b/assets/tests/lifecycle/shared/entity_script/lifecycle_b.lua @@ -0,0 +1,11 @@ +function on_script_loaded() + return "B: loaded!" +end + +function on_script_unloaded() + return "B: unloaded!" +end + +function on_script_reloaded(val) + return "B: reloaded with: " .. val +end diff --git a/assets/tests/lifecycle/shared/entity_script/scenario.txt b/assets/tests/lifecycle/shared/entity_script/scenario.txt new file mode 100644 index 0000000000..7d6c892c8a --- /dev/null +++ b/assets/tests/lifecycle/shared/entity_script/scenario.txt @@ -0,0 +1,65 @@ +// #main_script lifecycle.lua +SetCurrentLanguage language="@this_script_language" +InstallPlugin emit_responses=true, context_policy="Global" +FinalizeApp + +// load script a +LoadScriptAs as_name="script_a", path="lifecycle.lua" +WaitForScriptLoaded name="script_a" +SpawnEntityWithScript name="test_entity_a", script="script_a" +RunUpdateOnce +AssertCallbackSuccess attachment="EntityScript", label="OnScriptLoaded", script="script_a", entity="test_entity_a", expect_string_value="loaded!" +AssertNoCallbackResponsesEmitted + +// reload script_a, with the same content +ReloadScriptFrom script="script_a", path="lifecycle.lua" +RunUpdateOnce +AssertCallbackSuccess attachment="EntityScript", label="OnScriptUnloaded", script="script_a", entity="test_entity_a", expect_string_value="unloaded!" +AssertCallbackSuccess attachment="EntityScript", label="OnScriptLoaded", script="script_a", entity="test_entity_a", expect_string_value="loaded!" +AssertCallbackSuccess attachment="EntityScript", label="OnScriptReloaded", script="script_a", entity="test_entity_a", expect_string_value="reloaded with: unloaded!" +AssertNoCallbackResponsesEmitted + +// load licecycle_b.lua, which prefixes all outputs with "B" +LoadScriptAs as_name="script_b", path="lifecycle_b.lua" +WaitForScriptLoaded name="script_b" +SpawnEntityWithScript name="test_entity_b", script="script_b" +RunUpdateOnce +AssertCallbackSuccess attachment="EntityScript", label="OnScriptLoaded", script="script_b", entity="test_entity_b", expect_string_value="B: loaded!" +AssertNoCallbackResponsesEmitted + +// reload script_b, with the same content +ReloadScriptFrom script="script_b", path="lifecycle_b.lua" +RunUpdateOnce +AssertCallbackSuccess attachment="EntityScript", label="OnScriptUnloaded", script="script_b", entity="test_entity_b", expect_string_value="B: unloaded!" +AssertCallbackSuccess attachment="EntityScript", label="OnScriptLoaded", script="script_b", entity="test_entity_b", expect_string_value="B: loaded!" +AssertCallbackSuccess attachment="EntityScript", label="OnScriptReloaded", script="script_b", entity="test_entity_b", expect_string_value="B: reloaded with: B: unloaded!" +AssertNoCallbackResponsesEmitted + +// assert context residents +AssertContextResidents attachment="EntityScript", script="script_a", entity="test_entity_a", residents_num=2 +AssertContextResidents attachment="EntityScript", script="script_b", entity="test_entity_b", residents_num=2 + + +// now first drop the script asset, assert that does nothing yet +DropScriptAsset script="script_a" +RunUpdateOnce +AssertNoCallbackResponsesEmitted + +// now detach the second script, expect unloading, but with the callback from script_b +DespawnEntity entity="test_entity_a" +RunUpdateOnce +AssertCallbackSuccess attachment="EntityScript", label="OnScriptUnloaded", script="script_a", entity="test_entity_a", expect_string_value="B: unloaded!" +AssertNoCallbackResponsesEmitted +AssertContextResidents attachment="EntityScript", script="script_a", entity="test_entity_a", residents_num=1 + +// drop the second script asset, expect unloading, with callback from script_b +DropScriptAsset script="script_b" +RunUpdateOnce +AssertNoCallbackResponsesEmitted + +// detach the second script, expect unloading +DespawnEntity entity="test_entity_b" +RunUpdateOnce +AssertCallbackSuccess attachment="EntityScript", label="OnScriptUnloaded", script="script_b", entity="test_entity_b", expect_string_value="B: unloaded!" +AssertNoCallbackResponsesEmitted +AssertContextResidents attachment="EntityScript", script="script_b", entity="test_entity_b", residents_num=0 \ No newline at end of file diff --git a/assets/tests/lifecycle/shared/static_script/lifecycle.lua b/assets/tests/lifecycle/shared/static_script/lifecycle.lua new file mode 100644 index 0000000000..9dff611cc1 --- /dev/null +++ b/assets/tests/lifecycle/shared/static_script/lifecycle.lua @@ -0,0 +1,11 @@ +function on_script_loaded() + return "loaded!" +end + +function on_script_unloaded() + return "unloaded!" +end + +function on_script_reloaded(val) + return "reloaded with: " .. val +end diff --git a/assets/tests/lifecycle/shared/static_script/lifecycle_b.lua b/assets/tests/lifecycle/shared/static_script/lifecycle_b.lua new file mode 100644 index 0000000000..34373a9fdb --- /dev/null +++ b/assets/tests/lifecycle/shared/static_script/lifecycle_b.lua @@ -0,0 +1,11 @@ +function on_script_loaded() + return "B: loaded!" +end + +function on_script_unloaded() + return "B: unloaded!" +end + +function on_script_reloaded(val) + return "B: reloaded with: " .. val +end diff --git a/assets/tests/lifecycle/shared/static_script/scenario.txt b/assets/tests/lifecycle/shared/static_script/scenario.txt new file mode 100644 index 0000000000..7fdc758c0f --- /dev/null +++ b/assets/tests/lifecycle/shared/static_script/scenario.txt @@ -0,0 +1,65 @@ +// #main_script lifecycle.lua +SetCurrentLanguage language="@this_script_language" +InstallPlugin emit_responses=true, context_policy="Global" +FinalizeApp + +// load script a +LoadScriptAs as_name="script_a", path="lifecycle.lua" +WaitForScriptLoaded name="script_a" +AttachStaticScript script="script_a" +RunUpdateOnce +AssertCallbackSuccess attachment="StaticScript", label="OnScriptLoaded", script="script_a", expect_string_value="loaded!" +AssertNoCallbackResponsesEmitted + +// reload script_a, with the same content +ReloadScriptFrom script="script_a", path="lifecycle.lua" +RunUpdateOnce +AssertCallbackSuccess attachment="StaticScript", label="OnScriptUnloaded", script="script_a", expect_string_value="unloaded!" +AssertCallbackSuccess attachment="StaticScript", label="OnScriptLoaded", script="script_a", expect_string_value="loaded!" +AssertCallbackSuccess attachment="StaticScript", label="OnScriptReloaded", script="script_a", expect_string_value="reloaded with: unloaded!" +AssertNoCallbackResponsesEmitted + +// load licecycle_b.lua, which prefixes all outputs with "B" +LoadScriptAs as_name="script_b", path="lifecycle_b.lua" +WaitForScriptLoaded name="script_b" +AttachStaticScript script="script_b" +RunUpdateOnce +AssertCallbackSuccess attachment="StaticScript", label="OnScriptLoaded", script="script_b", expect_string_value="B: loaded!" +AssertNoCallbackResponsesEmitted + +// reload script_b, with the same content +ReloadScriptFrom script="script_b", path="lifecycle_b.lua" +RunUpdateOnce +AssertCallbackSuccess attachment="StaticScript", label="OnScriptUnloaded", script="script_b", expect_string_value="B: unloaded!" +AssertCallbackSuccess attachment="StaticScript", label="OnScriptLoaded", script="script_b", expect_string_value="B: loaded!" +AssertCallbackSuccess attachment="StaticScript", label="OnScriptReloaded", script="script_b", expect_string_value="B: reloaded with: B: unloaded!" +AssertNoCallbackResponsesEmitted + +// assert context residents +AssertContextResidents attachment="StaticScript", script="script_a", residents_num=2 +AssertContextResidents attachment="StaticScript", script="script_b", residents_num=2 + + +// now first drop the script asset, assert that does nothing yet +DropScriptAsset script="script_a" +RunUpdateOnce +AssertNoCallbackResponsesEmitted + +// now detach the second script, expect unloading, but with the callback from script_b +DetachStaticScript script="script_a" +RunUpdateOnce +AssertCallbackSuccess attachment="StaticScript", label="OnScriptUnloaded", script="script_a", expect_string_value="B: unloaded!" +AssertNoCallbackResponsesEmitted +AssertContextResidents attachment="StaticScript", script="script_a", residents_num=1 + +// drop the second script asset, expect unloading, with callback from script_b +DropScriptAsset script="script_b" +RunUpdateOnce +AssertNoCallbackResponsesEmitted + +// detach the second script, expect unloading +DetachStaticScript script="script_b" +RunUpdateOnce +AssertCallbackSuccess attachment="StaticScript", label="OnScriptUnloaded", script="script_b", expect_string_value="B: unloaded!" +AssertNoCallbackResponsesEmitted +AssertContextResidents attachment="StaticScript", script="script_b", residents_num=0 \ No newline at end of file diff --git a/assets/tests/scenario.txt b/assets/tests/scenario.txt new file mode 100644 index 0000000000..cff7bcb08e --- /dev/null +++ b/assets/tests/scenario.txt @@ -0,0 +1,18 @@ +SetCurrentLanguage language="@this_script_language" +InstallPlugin +SetupHandler OnTest=null, Update=null +SetupHandler OnTestPostUpdate=null, PostUpdate=null +SetupHandler Last=null, OnTestLast=null +FinalizeApp + +LoadScriptAs as_name="@this_script", path="@this_script" +WaitForScriptLoaded name="@this_script" +SpawnEntityWithScript name="test_entity", script="@this_script" +RunUpdateOnce +EmitScriptCallbackEvent emit_response=true, entity="test_entity", label="OnTest", language=null, recipients="EntityScript", script="@this_script" +EmitScriptCallbackEvent emit_response=true, entity="test_entity", label="OnTestPostUpdate", language=null, recipients="EntityScript", script="@this_script" +EmitScriptCallbackEvent emit_response=true, entity="test_entity", label="OnTestLast", language=null, recipients="EntityScript", script="@this_script" +RunUpdateOnce +AssertCallbackSuccess attachment="EntityScript", entity="test_entity", label="OnTest", script="@this_script" +AssertCallbackSuccess attachment="EntityScript", entity="test_entity", label="OnTestPostUpdate", script="@this_script" +AssertCallbackSuccess attachment="EntityScript", entity="test_entity", label="OnTestLast", script="@this_script" \ No newline at end of file diff --git a/benches/benchmarks.rs b/benches/benchmarks.rs index 087538ae90..356441381c 100644 --- a/benches/benchmarks.rs +++ b/benches/benchmarks.rs @@ -38,7 +38,7 @@ impl BenchmarkExecutor for Test { // use the file path from `benchmarks` onwards using folders as groupings // replace file separators with `/` // replace _ with spaces - let path = self.path.to_string_lossy(); + let path = self.script_asset_path.to_string_lossy(); let path = path.split("benchmarks").collect::>()[1] .replace(std::path::MAIN_SEPARATOR, "/"); let first_folder = path.split("/").collect::>()[1]; @@ -48,7 +48,7 @@ impl BenchmarkExecutor for Test { fn benchmark_name(&self) -> String { // use just the file stem let name = self - .path + .script_asset_path .file_stem() .unwrap() .to_string_lossy() @@ -63,13 +63,13 @@ impl BenchmarkExecutor for Test { fn execute(&self, criterion: &mut BenchmarkGroup) { match self.kind { test_utils::TestKind::Lua => run_lua_benchmark( - &self.path.to_string_lossy(), + &self.script_asset_path.to_string_lossy(), &self.benchmark_name(), criterion, ) .expect("Benchmark failed"), test_utils::TestKind::Rhai => run_rhai_benchmark( - &self.path.to_string_lossy(), + &self.script_asset_path.to_string_lossy(), &self.benchmark_name(), criterion, ) @@ -82,7 +82,7 @@ fn script_benchmarks(criterion: &mut Criterion, filter: Option) { // find manifest dir let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); let tests = discover_all_tests(manifest_dir, |p| { - p.path.starts_with("benchmarks") + p.script_asset_path.starts_with("benchmarks") && if let Some(filter) = &filter { let matching = filter.is_match(&p.benchmark_name()); if !matching { @@ -109,6 +109,24 @@ fn script_benchmarks(criterion: &mut Criterion, filter: Option) { tests.sort_by_key(|a| a.benchmark_name()); } + // debug + println!( + "{}", + grouped + .iter() + .map(|(k, v)| { + format!( + "Group: {k}, Tests: {}", + v.iter() + .map(|t| t.benchmark_name()) + .collect::>() + .join(", ") + ) + }) + .collect::>() + .join("\n"), + ); + for (group, tests) in grouped { println!("Running benchmarks for group: {group}"); let mut benchmark_group = criterion.benchmark_group(group); @@ -249,14 +267,7 @@ fn script_load_benchmarks(criterion: &mut Criterion) { // lua let plugin = make_test_lua_plugin(); let content = include_str!("../assets/macro_benchmarks/loading/empty.lua"); - run_plugin_script_load_benchmark( - plugin, - "empty Lua", - content, - &mut group, - |rand| format!("{rand}.lua"), - reload_probability, - ); + run_plugin_script_load_benchmark(plugin, "empty Lua", content, &mut group, reload_probability); // rhai let plugin = make_test_rhai_plugin(); @@ -266,7 +277,6 @@ fn script_load_benchmarks(criterion: &mut Criterion) { "empty Rhai", content, &mut group, - |rand| format!("{rand}.rhai"), reload_probability, ); } diff --git a/build_scripts.sh b/build_scripts.sh old mode 100644 new mode 100755 diff --git a/crates/bevy_mod_scripting_core/CHANGELOG.md b/crates/bevy_mod_scripting_core/CHANGELOG.md index 0d9c177529..317a734bc8 100644 --- a/crates/bevy_mod_scripting_core/CHANGELOG.md +++ b/crates/bevy_mod_scripting_core/CHANGELOG.md @@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.15.0](https://github.com/makspll/bevy_mod_scripting/compare/bevy_mod_scripting_core-v0.14.0...bevy_mod_scripting_core-v0.15.0) - 2025-08-14 + +### Added + +- Use the Handles, Luke! ([#427](https://github.com/makspll/bevy_mod_scripting/pull/427)) + +### Fixed + +- fix version + +### Other + +- update versions to currently released ones + ## [0.13.0](https://github.com/makspll/bevy_mod_scripting/compare/bevy_mod_scripting_core-v0.12.0...bevy_mod_scripting_core-v0.13.0) - 2025-07-05 ### Added diff --git a/crates/bevy_mod_scripting_core/Cargo.toml b/crates/bevy_mod_scripting_core/Cargo.toml index 2466f32967..ca5460cf34 100644 --- a/crates/bevy_mod_scripting_core/Cargo.toml +++ b/crates/bevy_mod_scripting_core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_mod_scripting_core" -version = "0.14.0" +version = "0.15.0" authors = ["Maksymilian Mozolewski "] edition = "2021" license = "MIT OR Apache-2.0" @@ -40,6 +40,8 @@ profiling = { workspace = true } bevy_mod_scripting_derive = { workspace = true } fixedbitset = "0.5" bevy_system_reflection = { path = "../bevy_system_reflection", version = "0.2.0" } +serde = { version = "1.0", features = ["derive"] } +uuid = "1.11" variadics_please = "1.1.0" [dev-dependencies] diff --git a/crates/bevy_mod_scripting_core/src/asset.rs b/crates/bevy_mod_scripting_core/src/asset.rs index 6a19209277..4e5f25f074 100644 --- a/crates/bevy_mod_scripting_core/src/asset.rs +++ b/crates/bevy_mod_scripting_core/src/asset.rs @@ -2,26 +2,29 @@ use std::borrow::Cow; -use bevy::{ - app::{App, PreUpdate}, - asset::{Asset, AssetEvent, AssetId, AssetLoader, AssetPath, Assets}, - log::{debug, info, trace, warn}, - platform::collections::HashMap, - prelude::{ - Commands, Event, EventReader, EventWriter, IntoScheduleConfigs, Res, ResMut, Resource, - }, - reflect::TypePath, -}; - use crate::{ commands::{CreateOrUpdateScript, DeleteScript}, + context::ContextLoadingSettings, error::ScriptError, - script::ScriptId, - IntoScriptPluginParams, ScriptingSystemSet, + event::ScriptEvent, + script::{ContextKey, DisplayProxy, ScriptAttachment}, + IntoScriptPluginParams, LanguageExtensions, ScriptComponent, ScriptingSystemSet, StaticScripts, }; +use bevy::{ + app::{App, Last}, + asset::{Asset, AssetEvent, AssetLoader, AssetPath, Assets, LoadState}, + log::{error, trace, warn, warn_once}, + prelude::{ + AssetServer, Commands, Entity, EventReader, EventWriter, IntoScheduleConfigs, Local, Query, + Res, + }, + reflect::Reflect, +}; +use serde::{Deserialize, Serialize}; +use std::collections::VecDeque; /// Represents a scripting language. Languages which compile into another language should use the target language as their language. -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Default)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Default, Serialize, Deserialize)] pub enum Language { /// The Rhai scripting language Rhai, @@ -49,42 +52,87 @@ impl std::fmt::Display for Language { } /// Represents a script loaded into memory as an asset -#[derive(Asset, TypePath, Clone)] +#[derive(Asset, Clone, Reflect)] +#[reflect(opaque)] pub struct ScriptAsset { /// The body of the script - pub content: Box<[u8]>, - /// The virtual filesystem path of the asset, used to map to the script Id for asset backed scripts + pub content: Box<[u8]>, // Any chance a Cow<'static, ?> could work here? + /// The language of the script + pub language: Language, + /// The asset path of the script. pub asset_path: AssetPath<'static>, } -#[derive(Event, Debug, Clone)] -pub(crate) enum ScriptAssetEvent { - Added(ScriptMetadata), - Removed(ScriptMetadata), - Modified(ScriptMetadata), +impl From for ScriptAsset { + fn from(s: String) -> ScriptAsset { + ScriptAsset { + content: s.into_bytes().into_boxed_slice(), + language: Language::default(), + asset_path: AssetPath::default(), + } + } +} + +impl ScriptAsset { + /// Create a new script asset with an unknown language. + pub fn new(s: impl Into) -> Self { + s.into().into() + } +} + +/// The queue that evaluates scripts. +type ScriptQueue = VecDeque; +/// Script settings +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct ScriptSettings { + /// Define the language for a script or use the extension if None. + pub language: Option, } #[derive(Default)] /// A loader for script assets pub struct ScriptAssetLoader { /// The file extensions this loader should handle - pub extensions: &'static [&'static str], + language_extensions: LanguageExtensions, + extensions: &'static [&'static str], /// preprocessor to run on the script before saving the content to an asset pub preprocessor: Option Result<(), ScriptError> + Send + Sync>>, } +impl ScriptAssetLoader { + /// Create a new script asset loader for the given extensions. + pub fn new(language_extensions: LanguageExtensions) -> Self { + let extensions: Vec<&'static str> = language_extensions.keys().copied().collect(); + let new_arr_static = Vec::leak(extensions); + Self { + language_extensions, + extensions: new_arr_static, + preprocessor: None, + } + } + + /// Add a preprocessor + pub fn with_preprocessor( + mut self, + preprocessor: Box Result<(), ScriptError> + Send + Sync>, + ) -> Self { + self.preprocessor = Some(preprocessor); + self + } +} + #[profiling::all_functions] impl AssetLoader for ScriptAssetLoader { type Asset = ScriptAsset; - type Settings = (); + type Settings = ScriptSettings; type Error = ScriptError; async fn load( &self, reader: &mut dyn bevy::asset::io::Reader, - _settings: &Self::Settings, + settings: &Self::Settings, load_context: &mut bevy::asset::LoadContext<'_>, ) -> Result { let mut content = Vec::new(); @@ -95,9 +143,32 @@ impl AssetLoader for ScriptAssetLoader { if let Some(processor) = &self.preprocessor { processor(&mut content)?; } + let language = settings.language.clone().unwrap_or_else(|| { + let ext = load_context + .path() + .extension() + .and_then(|e| e.to_str()) + .unwrap_or_default(); + self.language_extensions + .get(ext) + .cloned() + .unwrap_or_else(|| { + warn!("Unknown language for {:?}", load_context.path().display()); + Language::Unknown + }) + }); + if language == Language::Lua && cfg!(not(feature = "mlua")) { + warn_once!("Script {:?} is a Lua script but the {:?} feature is not enabled; the script will not be evaluated.", + load_context.path().display(), "mlua"); + } + if language == Language::Rhai && cfg!(not(feature = "rhai")) { + warn_once!("Script {:?} is a Rhai script but the {:?} feature is not enabled; the script will not be evaluated.", + load_context.path().display(), "rhai"); + } let asset = ScriptAsset { content: content.into_boxed_slice(), - asset_path: load_context.asset_path().to_owned(), + language, + asset_path: load_context.asset_path().clone(), }; Ok(asset) } @@ -107,280 +178,182 @@ impl AssetLoader for ScriptAssetLoader { } } -#[derive(Clone, Resource)] -/// Settings to do with script assets and how they are handled -pub struct ScriptAssetSettings { - /// Strategy for mapping asset paths to script ids, by default this is the identity function - pub script_id_mapper: AssetPathToScriptIdMapper, - /// Mapping from extension to script language - pub extension_to_language_map: HashMap<&'static str, Language>, - - /// The currently supported asset extensions - /// Should be updated by each scripting plugin to include the extensions it supports. - /// - /// Will be used to populate the script asset loader with the supported extensions - pub supported_extensions: &'static [&'static str], -} - -#[profiling::all_functions] -impl ScriptAssetSettings { - /// Selects the language for a given asset path - pub fn select_script_language(&self, path: &AssetPath) -> Language { - let extension = path - .path() - .extension() - .and_then(|ext| ext.to_str()) - .unwrap_or_default(); - self.extension_to_language_map - .get(extension) - .cloned() - .unwrap_or_default() - } -} - -impl Default for ScriptAssetSettings { - fn default() -> Self { - Self { - script_id_mapper: AssetPathToScriptIdMapper { - map: (|path: &AssetPath| path.path().to_string_lossy().into_owned().into()), - }, - extension_to_language_map: HashMap::from_iter(vec![ - ("lua", Language::Lua), - ("luau", Language::Lua), - ("rhai", Language::Rhai), - ("rn", Language::Rune), - ]), - supported_extensions: &["lua", "luau", "rhai", "rn"], - } - } -} - -/// Strategy for mapping asset paths to script ids, by default this is the identity function -#[derive(Clone, Copy)] -pub struct AssetPathToScriptIdMapper { - /// The mapping function - pub map: fn(&AssetPath) -> ScriptId, -} - -/// A cache of asset id's to their script id's. Necessary since when we drop an asset we won't have the ability to get the path from the asset. -#[derive(Default, Debug, Resource)] -pub struct ScriptMetadataStore { - /// The map of asset id's to their metadata - pub map: HashMap, ScriptMetadata>, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -/// Metadata for a script asset -pub struct ScriptMetadata { - /// The asset id of the script - pub asset_id: AssetId, - /// The script id of the script - pub script_id: ScriptId, - /// The language of the script - pub language: Language, -} - -#[profiling::all_functions] -impl ScriptMetadataStore { - /// Inserts a new metadata entry - pub fn insert(&mut self, id: AssetId, meta: ScriptMetadata) { - // TODO: new generations of assets are not going to have the same ID as the old one - self.map.insert(id, meta); - } - - /// Gets a metadata entry - pub fn get(&self, id: AssetId) -> Option<&ScriptMetadata> { - self.map.get(&id) - } - - /// Removes a metadata entry - pub fn remove(&mut self, id: AssetId) -> Option { - self.map.remove(&id) - } - - /// Checks if the store contains a metadata entry - pub fn contains(&self, id: AssetId) -> bool { - self.map.contains_key(&id) - } -} - -/// Converts incoming asset events, into internal script asset events, also loads and inserts metadata for newly added scripts -#[profiling::function] -pub(crate) fn dispatch_script_asset_events( +fn sync_assets( mut events: EventReader>, - mut script_asset_events: EventWriter, - assets: Res>, - mut metadata_store: ResMut, - settings: Res, + mut script_events: EventWriter, ) { for event in events.read() { match event { - AssetEvent::LoadedWithDependencies { id } | AssetEvent::Added { id } => { - // these can occur multiple times, we only send one added event though - if !metadata_store.contains(*id) { - let asset = assets.get(*id); - if let Some(asset) = asset { - let path = &asset.asset_path; - let converter = settings.script_id_mapper.map; - let script_id = converter(path); - - let language = settings.select_script_language(path); - if language == Language::Unknown { - let extension = path - .path() - .extension() - .and_then(|ext| ext.to_str()) - .unwrap_or_default(); - warn!("A script {:?} was added but its language is unknown. Consider adding the {:?} extension to the `ScriptAssetSettings`.", &script_id, extension); - } - let metadata = ScriptMetadata { - asset_id: *id, - script_id, - language, - }; - debug!("Script loaded, populating metadata: {:?}:", metadata); - script_asset_events.write(ScriptAssetEvent::Added(metadata.clone())); - metadata_store.insert(*id, metadata); - } else { - warn!("A script was added but it's asset was not found, failed to compute metadata. This script will not be loaded. Did you forget to store `Handle` somewhere?. {}", id); - } - } - } - AssetEvent::Removed { id } => { - if let Some(metadata) = metadata_store.get(*id) { - debug!("Script removed: {:?}", metadata); - script_asset_events.write(ScriptAssetEvent::Removed(metadata.clone())); - } else { - warn!("Script metadata not found for removed script asset: {}. Cannot properly clean up script", id); - } - } AssetEvent::Modified { id } => { - if let Some(metadata) = metadata_store.get(*id) { - debug!("Script modified: {:?}", metadata); - script_asset_events.write(ScriptAssetEvent::Modified(metadata.clone())); - } else { - warn!("Script metadata not found for modified script asset: {}. Cannot properly update script", id); - } + script_events.write(ScriptEvent::Modified { script: *id }); } - _ => {} - } - } -} - -/// Listens to [`ScriptAssetEvent::Removed`] events and removes the corresponding script metadata. -#[profiling::function] -pub(crate) fn remove_script_metadata( - mut events: EventReader, - mut asset_path_map: ResMut, -) { - for event in events.read() { - if let ScriptAssetEvent::Removed(metadata) = event { - let previous = asset_path_map.remove(metadata.asset_id); - if let Some(previous) = previous { - debug!("Removed script metadata: {:?}", previous); + AssetEvent::Added { id } => { + script_events.write(ScriptEvent::Added { script: *id }); + } + AssetEvent::Removed { id } => { + script_events.write(ScriptEvent::Removed { script: *id }); } + _ => (), } } } -/// Listens to [`ScriptAssetEvent`] events and dispatches [`CreateOrUpdateScript`] and [`DeleteScript`] commands accordingly. +/// Listens to [`ScriptEvent`] events and dispatches [`CreateOrUpdateScript`] and [`DeleteScript`] commands accordingly. /// /// Allows for hot-reloading of scripts. #[profiling::function] -pub(crate) fn sync_script_data( - mut events: EventReader, +fn handle_script_events( + mut events: EventReader, script_assets: Res>, + static_scripts: Res, + scripts: Query<(Entity, &ScriptComponent)>, + asset_server: Res, + mut script_queue: Local, mut commands: Commands, + context_loading_settings: Res>, ) { for event in events.read() { - let metadata = match event { - ScriptAssetEvent::Added(script_metadata) - | ScriptAssetEvent::Removed(script_metadata) - | ScriptAssetEvent::Modified(script_metadata) => script_metadata, - }; - - if metadata.language != P::LANGUAGE { - continue; - } - - trace!("{}: Received script asset event: {:?}", P::LANGUAGE, event); + trace!("{}: Received script event: {:?}", P::LANGUAGE, event); match event { - // emitted when a new script asset is loaded for the first time - ScriptAssetEvent::Added(_) | ScriptAssetEvent::Modified(_) => { - if metadata.language != P::LANGUAGE { - trace!( - "{}: Script asset with id: {} is for a different langauge than this sync system. Skipping.", - P::LANGUAGE, - metadata.script_id - ); - continue; + ScriptEvent::Modified { script: id } => { + if let Some(asset) = script_assets.get(*id) { + if asset.language != P::LANGUAGE { + continue; + } + // We need to reload the script for any context it's + // associated with. That could be static scripts, script + // components. + for (entity, script_component) in &scripts { + if let Some(handle) = + script_component.0.iter().find(|handle| handle.id() == *id) + { + commands.queue( + CreateOrUpdateScript::

::new(ScriptAttachment::EntityScript( + entity, + handle.clone(), + )) + .with_responses(context_loading_settings.emit_responses), + ); + } + } + + if let Some(handle) = static_scripts.scripts.iter().find(|s| s.id() == *id) { + commands.queue( + CreateOrUpdateScript::

::new(ScriptAttachment::StaticScript( + handle.clone(), + )) + .with_responses(context_loading_settings.emit_responses), + ); + } } + } + ScriptEvent::Detached { key } => { + commands.queue( + DeleteScript::

::new(key.clone()) + .with_responses(context_loading_settings.emit_responses), + ); + } + ScriptEvent::Attached { key } => { + script_queue.push_back(key.clone()); + } + _ => (), + } + } - info!("{}: Loading Script: {:?}", P::LANGUAGE, metadata.script_id,); + // Evalute the scripts in the order they were attached. + // + // If a script is not loaded yet, we stop evaluation and try again on the + // next call. + while !script_queue.is_empty() { + let mut script_failed = false; + // NOTE: Maybe using pop_front_if once stabalized. + let script_ready = script_queue + .front() + .map(|context_key| context_key.clone().into()) + .map(|context_key: ContextKey| { + // If there is a script, wait for the script to load. + context_key + .script + .as_ref() + .map(|script| { + script_assets.contains(script.id()) + || match asset_server.load_state(script) { + LoadState::NotLoaded => false, + LoadState::Loading => false, + LoadState::Loaded => true, + LoadState::Failed(e) => { + script_failed = true; + error!( + "Failed to load script {} for eval: {e}.", + script.display() + ); + true + } + } + }) + .unwrap_or(true) + }) + .unwrap_or(false); + if !script_ready { + // We can't evaluate it yet. It's still loading. + break; + } - if let Some(asset) = script_assets.get(metadata.asset_id) { - commands.queue(CreateOrUpdateScript::

::new( - metadata.script_id.clone(), - asset.content.clone(), - Some(script_assets.reserve_handle().clone_weak()), - )); - } + if let Some(context_key) = script_queue.pop_front() { + if script_failed { + continue; } - ScriptAssetEvent::Removed(_) => { - info!("{}: Deleting Script: {:?}", P::LANGUAGE, metadata.script_id,); - commands.queue(DeleteScript::

::new(metadata.script_id.clone())); + + let language = script_assets + .get(&context_key.script()) + .map(|asset| asset.language.clone()) + .unwrap_or_default(); + + if language == P::LANGUAGE { + commands.queue( + CreateOrUpdateScript::

::new(context_key) + .with_responses(context_loading_settings.emit_responses), + ); } - }; + } } } /// Setup all the asset systems for the scripting plugin and the dependencies #[profiling::function] -pub(crate) fn configure_asset_systems(app: &mut App) -> &mut App { +pub(crate) fn configure_asset_systems(app: &mut App) { // these should be in the same set as bevy's asset systems // currently this is in the PreUpdate set app.add_systems( - PreUpdate, - ( - dispatch_script_asset_events.in_set(ScriptingSystemSet::ScriptAssetDispatch), - remove_script_metadata.in_set(ScriptingSystemSet::ScriptMetadataRemoval), - ), + Last, + (sync_assets).in_set(ScriptingSystemSet::ScriptAssetDispatch), ) .configure_sets( - PreUpdate, + Last, ( - ScriptingSystemSet::ScriptAssetDispatch.after(bevy::asset::TrackAssets), + ScriptingSystemSet::ScriptAssetDispatch.after(bevy::asset::AssetEvents), ScriptingSystemSet::ScriptCommandDispatch - .after(ScriptingSystemSet::ScriptAssetDispatch) - .before(ScriptingSystemSet::ScriptMetadataRemoval), + .after(ScriptingSystemSet::ScriptAssetDispatch), ), - ) - .init_resource::() - .init_resource::() - .add_event::(); - - app + ); } /// Setup all the asset systems for the scripting plugin and the dependencies #[profiling::function] -pub(crate) fn configure_asset_systems_for_plugin( - app: &mut App, -) -> &mut App { +pub(crate) fn configure_asset_systems_for_plugin(app: &mut App) { app.add_systems( - PreUpdate, - sync_script_data::

.in_set(ScriptingSystemSet::ScriptCommandDispatch), + Last, + handle_script_events::

.in_set(ScriptingSystemSet::ScriptCommandDispatch), ); - app } #[cfg(test)] mod tests { - use std::path::{Path, PathBuf}; + use std::path::PathBuf; use bevy::{ - app::{App, Update}, - asset::{AssetApp, AssetPlugin, AssetServer, Assets, Handle, LoadState}, + app::App, + asset::{AssetApp, AssetPath, AssetPlugin, AssetServer, Assets, Handle, LoadState}, MinimalPlugins, }; @@ -394,17 +367,10 @@ mod tests { app } - fn make_test_settings() -> ScriptAssetSettings { - ScriptAssetSettings { - supported_extensions: &[], - script_id_mapper: AssetPathToScriptIdMapper { - map: |path| path.path().to_string_lossy().into_owned().into(), - }, - extension_to_language_map: HashMap::from_iter(vec![ - ("lua", Language::Lua), - ("rhai", Language::Rhai), - ]), - } + fn for_extension(extension: &'static str) -> ScriptAssetLoader { + let mut language_extensions = LanguageExtensions::default(); + language_extensions.insert(extension, Language::Unknown); + ScriptAssetLoader::new(language_extensions) } fn load_asset(app: &mut App, path: &str) -> Handle { @@ -441,10 +407,7 @@ mod tests { #[test] fn test_asset_loader_loads() { - let loader = ScriptAssetLoader { - extensions: &["script"], - preprocessor: None, - }; + let loader = for_extension("script"); let mut app = init_loader_test(loader); let handle = load_asset(&mut app, "test_assets/test_script.script"); @@ -455,11 +418,6 @@ mod tests { .get(&handle) .unwrap(); - assert_eq!( - asset.asset_path, - AssetPath::from_path(&PathBuf::from("test_assets/test_script.script")) - ); - assert_eq!( String::from_utf8(asset.content.clone().to_vec()).unwrap(), "test script".to_string() @@ -468,13 +426,10 @@ mod tests { #[test] fn test_asset_loader_applies_preprocessor() { - let loader = ScriptAssetLoader { - extensions: &["script"], - preprocessor: Some(Box::new(|content| { - content[0] = b'p'; - Ok(()) - })), - }; + let loader = for_extension("script").with_preprocessor(Box::new(|content| { + content[0] = b'p'; + Ok(()) + })); let mut app = init_loader_test(loader); let handle = load_asset(&mut app, "test_assets/test_script.script"); @@ -486,164 +441,12 @@ mod tests { .unwrap(); assert_eq!( - asset.asset_path, - AssetPath::from(PathBuf::from("test_assets/test_script.script")) + handle.path().unwrap(), + &AssetPath::from(PathBuf::from("test_assets/test_script.script")) ); assert_eq!( String::from_utf8(asset.content.clone().to_vec()).unwrap(), "pest script".to_string() ); } - - #[test] - fn test_metadata_store() { - let mut store = ScriptMetadataStore::default(); - let id = AssetId::invalid(); - let meta = ScriptMetadata { - asset_id: AssetId::invalid(), - script_id: "test".into(), - language: Language::Lua, - }; - - store.insert(id, meta.clone()); - assert_eq!(store.get(id), Some(&meta)); - - assert_eq!(store.remove(id), Some(meta)); - } - - #[test] - fn test_script_asset_settings_select_language() { - let settings = make_test_settings(); - - let path = AssetPath::from(Path::new("test.lua")); - assert_eq!(settings.select_script_language(&path), Language::Lua); - assert_eq!( - settings.select_script_language(&AssetPath::from(Path::new("test.rhai"))), - Language::Rhai - ); - assert_eq!( - settings.select_script_language(&AssetPath::from(Path::new("test.blob"))), - Language::Unknown - ); - } - - fn run_app_untill_asset_event(app: &mut App, event_kind: AssetEvent) { - let checker_system = |mut reader: EventReader>, - mut event_target: ResMut| { - println!("Reading asset events this frame"); - for event in reader.read() { - println!("{event:?}"); - if matches!( - (event_target.event, event), - (AssetEvent::Added { .. }, AssetEvent::Added { .. }) - | (AssetEvent::Modified { .. }, AssetEvent::Modified { .. }) - | (AssetEvent::Removed { .. }, AssetEvent::Removed { .. }) - | (AssetEvent::Unused { .. }, AssetEvent::Unused { .. }) - | ( - AssetEvent::LoadedWithDependencies { .. }, - AssetEvent::LoadedWithDependencies { .. }, - ) - ) { - println!("Event matched"); - event_target.happened = true; - } - } - }; - - if !app.world().contains_resource::() { - // for when we run this multiple times in a test - app.add_systems(Update, checker_system); - } - - #[derive(Resource)] - struct EventTarget { - event: AssetEvent, - happened: bool, - } - app.world_mut().insert_resource(EventTarget { - event: event_kind, - happened: false, - }); - - loop { - println!("Checking if asset event was dispatched"); - if app.world().get_resource::().unwrap().happened { - println!("Stopping loop"); - break; - } - println!("Running app"); - - app.update(); - } - } - - struct DummyPlugin; - - impl IntoScriptPluginParams for DummyPlugin { - const LANGUAGE: Language = Language::Lua; - type C = (); - type R = (); - - fn build_runtime() -> Self::R {} - } - - #[test] - fn test_asset_metadata_systems() { - // test metadata flow - let mut app = init_loader_test(ScriptAssetLoader { - extensions: &[], - preprocessor: None, - }); - app.world_mut().insert_resource(make_test_settings()); - configure_asset_systems(&mut app); - - // update untill the asset event gets dispatched - let asset_server: &AssetServer = app.world().resource::(); - let handle = asset_server.load("test_assets/test_script.lua"); - run_app_untill_asset_event( - &mut app, - AssetEvent::LoadedWithDependencies { - id: AssetId::invalid(), - }, - ); - let asset_id = handle.id(); - - // we expect the metadata to be inserted now, in the same frame as the asset is loaded - let metadata = app - .world() - .get_resource::() - .unwrap() - .get(asset_id) - .expect("Metadata not found"); - - assert_eq!(metadata.script_id, "test_assets/test_script.lua"); - assert_eq!(metadata.language, Language::Lua); - - // ----------------- REMOVING ----------------- - - // we drop the handle and wait untill the first asset event is dispatched - drop(handle); - - run_app_untill_asset_event( - &mut app, - AssetEvent::Removed { - id: AssetId::invalid(), - }, - ); - - // we expect the metadata to be removed now, in the same frame as the asset is removed - let metadata_len = app - .world() - .get_resource::() - .unwrap() - .map - .len(); - - assert_eq!(metadata_len, 0); - } - - // #[test] - // fn test_syncing_assets() { - // todo!() - // } } diff --git a/crates/bevy_mod_scripting_core/src/bindings/allocator.rs b/crates/bevy_mod_scripting_core/src/bindings/allocator.rs index 9feb3c2454..7914b0694b 100644 --- a/crates/bevy_mod_scripting_core/src/bindings/allocator.rs +++ b/crates/bevy_mod_scripting_core/src/bindings/allocator.rs @@ -62,7 +62,7 @@ impl Eq for ReflectAllocationId {} impl PartialOrd for ReflectAllocationId { fn partial_cmp(&self, other: &Self) -> Option { - Some(self.id().cmp(&other.id())) + Some(self.cmp(other)) } } @@ -147,12 +147,12 @@ impl Default for AppReflectAllocator { impl AppReflectAllocator { /// claim a read lock on the allocator - pub fn read(&self) -> RwLockReadGuard { + pub fn read(&self) -> RwLockReadGuard<'_, ReflectAllocator> { self.allocator.read() } /// claim a write lock on the allocator - pub fn write(&self) -> RwLockWriteGuard { + pub fn write(&self) -> RwLockWriteGuard<'_, ReflectAllocator> { self.allocator.write() } } diff --git a/crates/bevy_mod_scripting_core/src/bindings/function/script_function.rs b/crates/bevy_mod_scripting_core/src/bindings/function/script_function.rs index dbcfad6640..6105ad3d4b 100644 --- a/crates/bevy_mod_scripting_core/src/bindings/function/script_function.rs +++ b/crates/bevy_mod_scripting_core/src/bindings/function/script_function.rs @@ -253,12 +253,12 @@ pub struct ScriptFunctionRegistryArc(pub Arc>); #[profiling::all_functions] impl ScriptFunctionRegistryArc { /// claim a read lock on the registry - pub fn read(&self) -> RwLockReadGuard { + pub fn read(&self) -> RwLockReadGuard<'_, ScriptFunctionRegistry> { self.0.read() } /// claim a write lock on the registry - pub fn write(&mut self) -> RwLockWriteGuard { + pub fn write(&mut self) -> RwLockWriteGuard<'_, ScriptFunctionRegistry> { self.0.write() } } diff --git a/crates/bevy_mod_scripting_core/src/bindings/globals/core.rs b/crates/bevy_mod_scripting_core/src/bindings/globals/core.rs index 44e85bd38d..23de84eb48 100644 --- a/crates/bevy_mod_scripting_core/src/bindings/globals/core.rs +++ b/crates/bevy_mod_scripting_core/src/bindings/globals/core.rs @@ -4,12 +4,14 @@ use std::{cell::RefCell, collections::HashMap, sync::Arc}; use bevy::{ app::Plugin, + asset::Handle, ecs::{entity::Entity, reflect::AppTypeRegistry, world::World}, reflect::TypeRegistration, }; use bevy_mod_scripting_derive::script_globals; use crate::{ + asset::ScriptAsset, bindings::{ function::from::{Union, Val}, ScriptComponentRegistration, ScriptResourceRegistration, ScriptTypeRegistration, @@ -53,7 +55,7 @@ impl Plugin for CoreScriptGlobalsPlugin { app.init_resource::(); } fn finish(&self, app: &mut bevy::app::App) { - profiling::function_scope!("app finish"); + // profiling::function_scope!("app finish"); if self.register_static_references { register_static_core_globals(app.world_mut(), self.filter); @@ -99,7 +101,7 @@ fn register_static_core_globals( global_registry.register_dummy::("world", "The current ECS world."); global_registry .register_dummy::("entity", "The entity this script is attached to if any."); - global_registry.register_dummy::("script_id", "the name/id of this script"); + global_registry.register_dummy_typed::>>("script_asset", "the asset handle for this script. If the asset is ever unloaded, the handle will be less useful."); } #[script_globals(bms_core_path = "crate", name = "core_globals")] @@ -121,7 +123,7 @@ impl CoreGlobals { >, InteropError, > { - profiling::function_scope!("registering core globals"); + // profiling::function_scope!("registering core globals"); let type_registry = guard.type_registry(); let type_registry = type_registry.read(); let mut type_cache = HashMap::::default(); diff --git a/crates/bevy_mod_scripting_core/src/bindings/globals/mod.rs b/crates/bevy_mod_scripting_core/src/bindings/globals/mod.rs index 49e329e5ba..8fd06673b3 100644 --- a/crates/bevy_mod_scripting_core/src/bindings/globals/mod.rs +++ b/crates/bevy_mod_scripting_core/src/bindings/globals/mod.rs @@ -1,13 +1,13 @@ //! Contains abstractions for exposing "globals" to scripts, in a language-agnostic way. use super::{ - function::arg_meta::{ScriptReturn, TypedScriptReturn}, - script_value::ScriptValue, - WorldGuard, + function::arg_meta::{ScriptReturn, TypedScriptReturn}, + script_value::ScriptValue, + WorldGuard, }; use crate::{ - docgen::{into_through_type_info, typed_through::ThroughTypeInfo}, - error::InteropError, + docgen::{into_through_type_info, typed_through::ThroughTypeInfo, TypedThrough}, + error::InteropError, }; use bevy::{platform::collections::HashMap, prelude::Resource, reflect::Typed}; use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard}; @@ -24,12 +24,12 @@ pub struct AppScriptGlobalsRegistry(Arc>); #[profiling::all_functions] impl AppScriptGlobalsRegistry { /// Returns a reference to the inner [`ScriptGlobalsRegistry`]. - pub fn read(&self) -> RwLockReadGuard { + pub fn read(&self) -> RwLockReadGuard<'_, ScriptGlobalsRegistry> { self.0.read() } /// Returns a mutable reference to the inner [`ScriptGlobalsRegistry`]. - pub fn write(&self) -> RwLockWriteGuard { + pub fn write(&self) -> RwLockWriteGuard<'_, ScriptGlobalsRegistry> { self.0.write() } } @@ -156,6 +156,22 @@ impl ScriptGlobalsRegistry { ); } + /// Typed equivalent to [`Self::register_dummy`]. + pub fn register_dummy_typed( + &mut self, + name: impl Into>, + documentation: impl Into>, + ) { + self.dummies.insert( + name.into(), + ScriptGlobalDummy { + documentation: Some(documentation.into()), + type_id: TypeId::of::(), + type_information: Some(T::through_type_info()), + }, + ); + } + /// Inserts a global into the registry, returns the previous value if it existed. /// /// This is a version of [`Self::register`] which stores type information regarding the global. @@ -235,11 +251,11 @@ impl ScriptGlobalsRegistry { #[cfg(test)] mod test { - use bevy::ecs::world::World; + use bevy::ecs::world::World; - use super::*; + use super::*; - #[test] + #[test] fn test_script_globals_registry() { let mut registry = ScriptGlobalsRegistry::default(); diff --git a/crates/bevy_mod_scripting_core/src/bindings/pretty_print.rs b/crates/bevy_mod_scripting_core/src/bindings/pretty_print.rs index 385c4a61eb..80b666039c 100644 --- a/crates/bevy_mod_scripting_core/src/bindings/pretty_print.rs +++ b/crates/bevy_mod_scripting_core/src/bindings/pretty_print.rs @@ -354,7 +354,7 @@ pub trait AsAny: 'static { } #[doc(hidden)] -impl AsAny for T { +impl AsAny for T { fn as_any(&self) -> &dyn Any { self } diff --git a/crates/bevy_mod_scripting_core/src/bindings/schedule.rs b/crates/bevy_mod_scripting_core/src/bindings/schedule.rs index 8feff1cc98..41a15353fd 100644 --- a/crates/bevy_mod_scripting_core/src/bindings/schedule.rs +++ b/crates/bevy_mod_scripting_core/src/bindings/schedule.rs @@ -23,12 +23,12 @@ pub struct AppScheduleRegistry(Arc>); impl AppScheduleRegistry { /// Reads the schedule registry. - pub fn read(&self) -> parking_lot::RwLockReadGuard { + pub fn read(&self) -> parking_lot::RwLockReadGuard<'_, ScheduleRegistry> { self.0.read() } /// Writes to the schedule registry. - pub fn write(&self) -> parking_lot::RwLockWriteGuard { + pub fn write(&self) -> parking_lot::RwLockWriteGuard<'_, ScheduleRegistry> { self.0.write() } @@ -176,7 +176,7 @@ impl WorldAccessGuard<'_> { bevy::log::debug!( "Adding script system '{}' for script '{}' to schedule '{}'", builder.name, - builder.script_id, + builder.attachment, schedule.identifier() ); diff --git a/crates/bevy_mod_scripting_core/src/bindings/script_component.rs b/crates/bevy_mod_scripting_core/src/bindings/script_component.rs index b1a0c650f0..b97ad88d46 100644 --- a/crates/bevy_mod_scripting_core/src/bindings/script_component.rs +++ b/crates/bevy_mod_scripting_core/src/bindings/script_component.rs @@ -39,12 +39,12 @@ pub struct AppScriptComponentRegistry(pub Arc>); #[profiling::all_functions] impl AppScriptComponentRegistry { /// Reads the underlying registry - pub fn read(&self) -> parking_lot::RwLockReadGuard { + pub fn read(&self) -> parking_lot::RwLockReadGuard<'_, ScriptComponentRegistry> { self.0.read() } /// Writes to the underlying registry - pub fn write(&self) -> parking_lot::RwLockWriteGuard { + pub fn write(&self) -> parking_lot::RwLockWriteGuard<'_, ScriptComponentRegistry> { self.0.write() } } diff --git a/crates/bevy_mod_scripting_core/src/bindings/script_system.rs b/crates/bevy_mod_scripting_core/src/bindings/script_system.rs index c11332172b..1b4e4e1509 100644 --- a/crates/bevy_mod_scripting_core/src/bindings/script_system.rs +++ b/crates/bevy_mod_scripting_core/src/bindings/script_system.rs @@ -17,7 +17,7 @@ use crate::{ extractors::get_all_access_ids, handler::CallbackSettings, runtime::RuntimeContainer, - script::{ScriptId, Scripts}, + script::{ScriptAttachment, ScriptContext}, IntoScriptPluginParams, }; use bevy::{ @@ -36,8 +36,8 @@ use bevy::{ reflect::{OffsetAccess, ParsedPath, Reflect}, }; use bevy_system_reflection::{ReflectSchedule, ReflectSystem}; -use std::{any::TypeId, borrow::Cow, hash::Hash, marker::PhantomData, ops::Deref}; - +use parking_lot::Mutex; +use std::{any::TypeId, borrow::Cow, hash::Hash, marker::PhantomData, ops::Deref, sync::Arc}; #[derive(Clone, Hash, PartialEq, Eq)] /// a system set for script systems. pub struct ScriptSystemSet(Cow<'static, str>); @@ -85,7 +85,7 @@ enum ScriptSystemParamDescriptor { #[reflect(opaque)] pub struct ScriptSystemBuilder { pub(crate) name: CallbackLabel, - pub(crate) script_id: ScriptId, + pub(crate) attachment: ScriptAttachment, before: Vec, after: Vec, system_params: Vec, @@ -95,12 +95,12 @@ pub struct ScriptSystemBuilder { #[profiling::all_functions] impl ScriptSystemBuilder { /// Creates a new script system builder - pub fn new(name: CallbackLabel, script_id: ScriptId) -> Self { + pub fn new(name: CallbackLabel, attachment: ScriptAttachment) -> Self { Self { before: vec![], after: vec![], name, - script_id, + attachment, system_params: vec![], is_exclusive: false, } @@ -199,7 +199,7 @@ impl ScriptSystemBuilder { } struct DynamicHandlerContext<'w, P: IntoScriptPluginParams> { - scripts: &'w Scripts

, + script_context: &'w ScriptContext

, callback_settings: &'w CallbackSettings

, context_loading_settings: &'w ContextLoadingSettings

, runtime_container: &'w RuntimeContainer

, @@ -213,9 +213,8 @@ impl<'w, P: IntoScriptPluginParams> DynamicHandlerContext<'w, P> { )] pub fn init_param(world: &mut World, system: &mut FilteredAccessSet) { let mut access = FilteredAccess::::matches_nothing(); - let scripts_res_id = world - .resource_id::>() - .expect("Scripts resource not found"); + // let scripts_res_id = world + // .query::<&Script

>(); let callback_settings_res_id = world .resource_id::>() .expect("CallbackSettings resource not found"); @@ -226,7 +225,6 @@ impl<'w, P: IntoScriptPluginParams> DynamicHandlerContext<'w, P> { .resource_id::>() .expect("RuntimeContainer resource not found"); - access.add_resource_read(scripts_res_id); access.add_resource_read(callback_settings_res_id); access.add_resource_read(context_loading_settings_res_id); access.add_resource_read(runtime_container_res_id); @@ -241,7 +239,7 @@ impl<'w, P: IntoScriptPluginParams> DynamicHandlerContext<'w, P> { pub fn get_param(system: &UnsafeWorldCell<'w>) -> Self { unsafe { Self { - scripts: system.get_resource().expect("Scripts resource not found"), + script_context: system.get_resource().expect("Scripts resource not found"), callback_settings: system .get_resource() .expect("CallbackSettings resource not found"), @@ -259,15 +257,14 @@ impl<'w, P: IntoScriptPluginParams> DynamicHandlerContext<'w, P> { pub fn call_dynamic_label( &self, label: &CallbackLabel, - script_id: &ScriptId, - entity: Entity, + context_key: &ScriptAttachment, + context: Option>>, payload: Vec, guard: WorldGuard<'_>, ) -> Result { // find script - let script = match self.scripts.scripts.get(script_id) { - Some(script) => script, - None => return Err(InteropError::missing_script(script_id.clone()).into()), + let Some(context) = context.or_else(|| self.script_context.get(context_key)) else { + return Err(InteropError::missing_context(context_key.clone()).into()); }; // call the script @@ -277,13 +274,12 @@ impl<'w, P: IntoScriptPluginParams> DynamicHandlerContext<'w, P> { .context_pre_handling_initializers; let runtime = &self.runtime_container.runtime; - let mut context = script.context.lock(); + let mut context = context.lock(); CallbackSettings::

::call( handler, payload, - entity, - script_id, + context_key, label, &mut context, pre_handling_initializers, @@ -344,7 +340,7 @@ pub struct DynamicScriptSystem { /// cause a conflict pub(crate) archetype_component_access: Access, pub(crate) last_run: Tick, - target_script: ScriptId, + target_attachment: ScriptAttachment, archetype_generation: ArchetypeGeneration, system_param_descriptors: Vec, state: Option, @@ -367,7 +363,7 @@ impl IntoSystem<(), (), IsDynamicScriptSystem

> archetype_generation: ArchetypeGeneration::initial(), system_param_descriptors: builder.system_params, last_run: Default::default(), - target_script: builder.script_id, + target_attachment: builder.attachment, state: None, component_access_set: Default::default(), archetype_component_access: Default::default(), @@ -425,6 +421,7 @@ impl System for DynamicScriptSystem

{ }; let mut payload = Vec::with_capacity(state.system_params.len()); + let guard = if self.exclusive { // safety: we are an exclusive system, therefore the cell allows us to do this let world = unsafe { world.world_mut() }; @@ -487,29 +484,38 @@ impl System for DynamicScriptSystem

{ } } - // now that we have everything ready, we need to run the callback on the targetted scripts - // let's start with just calling the one targetted script + // Now that we have everything ready, we need to run the callback on the + // targetted scripts. Let's start with just calling the one targetted + // script. let handler_ctxt = DynamicHandlerContext::

::get_param(&world); - let result = handler_ctxt.call_dynamic_label( - &state.callback_label, - &self.target_script, - Entity::from_raw(0), - payload, - guard.clone(), - ); - - // TODO: emit error events via commands, maybe accumulate in state instead and use apply - match result { - Ok(_) => {} - Err(err) => { - bevy::log::error!( - "Error in dynamic script system `{}`: {}", - self.name, - err.display_with_world(guard) - ) + if let Some(context) = handler_ctxt.script_context.get(&self.target_attachment) { + let result = handler_ctxt.call_dynamic_label( + &state.callback_label, + &self.target_attachment, + Some(context), + payload, + guard.clone(), + ); + // TODO: Emit error events via commands, maybe accumulate in state + // instead and use apply. + match result { + Ok(_) => {} + Err(err) => { + bevy::log::error!( + "Error in dynamic script system `{}`: {}", + self.name, + err.display_with_world(guard) + ) + } } + } else { + bevy::log::warn_once!( + "Dynamic script system `{}` could not find script for attachment: {}. It will not run until it's loaded.", + self.name, + self.target_attachment + ); } } @@ -674,7 +680,7 @@ impl System for DynamicScriptSystem

{ mod test { use bevy::{ app::{App, MainScheduleOrder, Update}, - asset::AssetPlugin, + asset::{AssetId, AssetPlugin, Handle}, diagnostic::DiagnosticsPlugin, ecs::schedule::{ScheduleLabel, Schedules}, }; @@ -726,8 +732,11 @@ mod test { ReflectSystem::from_system(system.as_ref(), node_id) }); - // now dynamically add script system via builder - let mut builder = ScriptSystemBuilder::new("test".into(), "empty_script".into()); + // now dynamically add script system via builder, without a matching script + let mut builder = ScriptSystemBuilder::new( + "test".into(), + ScriptAttachment::StaticScript(Handle::Weak(AssetId::invalid())), + ); builder.before_system(test_system); let _ = builder diff --git a/crates/bevy_mod_scripting_core/src/bindings/world.rs b/crates/bevy_mod_scripting_core/src/bindings/world.rs index 40a38351c7..33958eb3b9 100644 --- a/crates/bevy_mod_scripting_core/src/bindings/world.rs +++ b/crates/bevy_mod_scripting_core/src/bindings/world.rs @@ -22,17 +22,21 @@ use super::{ ScriptTypeRegistration, Union, }; use crate::{ + asset::ScriptAsset, bindings::{ function::{from::FromScript, from_ref::FromScriptRef}, with_access_read, with_access_write, }, + commands::AddStaticScript, error::InteropError, reflection_extensions::PartialReflectExt, + script::{ScriptAttachment, ScriptComponent}, }; -use bevy::ecs::component::Mutable; +use bevy::ecs::{component::Mutable, system::Command}; use bevy::prelude::{ChildOf, Children}; use bevy::{ app::AppExit, + asset::{AssetServer, Handle, LoadState}, ecs::{ component::{Component, ComponentId}, entity::Entity, @@ -230,6 +234,13 @@ impl<'w> WorldAccessGuard<'w> { } } + /// Queues a command to the world, which will be executed later. + pub(crate) fn queue(&self, command: impl Command) -> Result<(), InteropError> { + self.with_global_access(|w| { + w.commands().queue(command); + }) + } + /// Runs a closure within an isolated access scope, releasing leftover accesses, should only be used in a single-threaded context. /// /// Safety: @@ -825,6 +836,36 @@ impl WorldAccessGuard<'_> { ::from_reflect_or_clone(dynamic.as_ref(), self.clone()) } + /// Loads a script from the given asset path with default settings. + pub fn load_script_asset(&self, asset_path: &str) -> Result, InteropError> { + self.with_resource(|r: &AssetServer| r.load(asset_path)) + } + + /// Checks the load state of a script asset. + pub fn get_script_asset_load_state( + &self, + script: Handle, + ) -> Result { + self.with_resource(|r: &AssetServer| r.load_state(script.id())) + } + + /// Attaches a script + pub fn attach_script(&self, attachment: ScriptAttachment) -> Result<(), InteropError> { + match attachment { + ScriptAttachment::EntityScript(entity, handle) => { + // find existing script components on the entity + self.with_or_insert_component_mut(entity, |c: &mut ScriptComponent| { + c.0.push(handle.clone()) + })?; + } + ScriptAttachment::StaticScript(handle) => { + self.queue(AddStaticScript::new(handle))?; + } + }; + + Ok(()) + } + /// Spawns a new entity in the world pub fn spawn(&self) -> Result { self.with_global_access(|world| { @@ -1270,7 +1311,7 @@ impl WorldContainer for ThreadWorldContainer { #[cfg(test)] mod test { use super::*; - use bevy::reflect::{GetTypeRegistration, Reflect, ReflectFromReflect}; + use bevy::reflect::{GetTypeRegistration, ReflectFromReflect}; use test_utils::test_data::{setup_world, SimpleEnum, SimpleStruct, SimpleTupleStruct}; #[test] @@ -1324,11 +1365,6 @@ mod test { pretty_assertions::assert_str_eq!(format!("{result:#?}"), format!("{expected:#?}")); } - #[derive(Reflect)] - struct Test { - pub hello: (usize, usize), - } - #[test] fn test_construct_tuple() { let mut world = setup_world(|_, registry| { diff --git a/crates/bevy_mod_scripting_core/src/commands.rs b/crates/bevy_mod_scripting_core/src/commands.rs index b3c7cd069d..f7f187d3d7 100644 --- a/crates/bevy_mod_scripting_core/src/commands.rs +++ b/crates/bevy_mod_scripting_core/src/commands.rs @@ -7,56 +7,100 @@ use crate::{ error::{InteropError, ScriptError}, event::{ CallbackLabel, IntoCallbackLabel, OnScriptLoaded, OnScriptReloaded, OnScriptUnloaded, - ScriptCallbackResponseEvent, + ScriptCallbackResponseEvent, ScriptEvent, }, extractors::{with_handler_system_state, HandlerContext}, handler::{handle_script_errors, send_callback_response}, - script::{Script, ScriptId, Scripts, StaticScripts}, - IntoScriptPluginParams, + script::{DisplayProxy, ScriptAttachment, StaticScripts}, + IntoScriptPluginParams, ScriptContext, }; -use bevy::{asset::Handle, ecs::entity::Entity, log::debug, prelude::Command}; -use parking_lot::Mutex; -use std::{marker::PhantomData, sync::Arc}; +use bevy::{ + asset::{Assets, Handle}, + ecs::event::Events, + log::{debug, warn}, + prelude::Command, +}; +use std::marker::PhantomData; -/// Deletes a script with the given ID +/// Detaches a script, invoking the `on_script_unloaded` callback if it exists, and removes the script from the static scripts collection. pub struct DeleteScript { - /// The ID of the script to delete - pub id: ScriptId, + /// The context key + pub context_key: ScriptAttachment, + /// Whether to emit responses for core callbacks, like `on_script_loaded`, `on_script_reloaded`, etc. + pub emit_responses: bool, /// hack to make this Send, C does not need to be Send since it is not stored in the command pub _ph: PhantomData, } impl DeleteScript

{ /// Creates a new DeleteScript command with the given ID - pub fn new(id: ScriptId) -> Self { + pub fn new(context_key: ScriptAttachment) -> Self { Self { - id, + context_key, + emit_responses: false, _ph: PhantomData, } } + + /// If set to true, will emit responses for core callbacks, like `on_script_loaded`, `on_script_reloaded`, etc. + pub fn with_responses(mut self, emit_responses: bool) -> Self { + self.emit_responses = emit_responses; + self + } } impl Command for DeleteScript

{ - fn apply(self, world: &mut bevy::prelude::World) { + fn apply(mut self, world: &mut bevy::prelude::World) { + // we demote to weak from here on out, so as not to hold the asset hostage + self.context_key = self.context_key.into_weak(); + // first apply unload callback - RunScriptCallback::

::new( - self.id.clone(), - Entity::from_raw(0), - OnScriptUnloaded::into_callback_label(), - vec![], - false, - ) - .apply(world); + Command::apply( + RunScriptCallback::

::new( + self.context_key.clone(), + OnScriptUnloaded::into_callback_label(), + vec![], + self.emit_responses, + ), + world, + ); + match &self.context_key { + ScriptAttachment::EntityScript(_, _) => { + // nothing special needs to be done, just the context removal + } + ScriptAttachment::StaticScript(script) => { + // remove the static script + let mut scripts = world.get_resource_or_init::(); + if scripts.remove(script.id()) { + debug!("Deleted static script {}", script.display()); + } else { + warn!( + "Attempted to delete static script {}, but it was not found", + script.display() + ); + } + } + } - let mut scripts = world.get_resource_or_init::>(); - if scripts.remove(self.id.clone()) { - debug!("Deleted script with id: {}", self.id); + let mut script_contexts = world.get_resource_or_init::>(); + let residents_count = script_contexts.residents_len(&self.context_key); + let delete_context = residents_count == 1; + let script_id = self.context_key.script(); + if delete_context && script_contexts.remove(&self.context_key).is_some() { + bevy::log::info!( + "{}: Deleted context for script {:?}", + P::LANGUAGE, + script_id.display() + ); } else { - bevy::log::error!( - "Attempted to delete script with id: {} but it does not exist, doing nothing!", - self.id + bevy::log::info!( + "{}: Context for script {:?} was not deleted, as it still has {} residents", + P::LANGUAGE, + script_id.display(), + residents_count ); } + script_contexts.remove_resident(&self.context_key); } } @@ -64,70 +108,74 @@ impl Command for DeleteScript

{ /// /// If script comes from an asset, expects it to be loaded, otherwise this command will fail to process the script. pub struct CreateOrUpdateScript { - id: ScriptId, - content: Box<[u8]>, - asset: Option>, + attachment: ScriptAttachment, + // It feels like we're using a Box, which requires a clone merely to satisfy the Command trait. + content: Option>, // Hack to make this Send, C does not need to be Send since it is not stored in the command _ph: std::marker::PhantomData, + + // if set to true will emit responses for core callbacks, like `on_script_loaded`, `on_script_reloaded`, etc. + emit_responses: bool, } #[profiling::all_functions] impl CreateOrUpdateScript

{ - /// Creates a new CreateOrUpdateScript command with the given ID, content and asset - pub fn new(id: ScriptId, content: Box<[u8]>, asset: Option>) -> Self { + /// Creates a new CreateOrUpdateScript command with the given ID, content + pub fn new(attachment: ScriptAttachment) -> Self { Self { - id, - content, - asset, + attachment, + content: None, _ph: std::marker::PhantomData, + emit_responses: false, } } + /// Add content to be evaluated. + pub fn with_content(mut self, content: impl Into) -> Self { + let content = content.into(); + self.content = Some(content.into_bytes().into_boxed_slice()); + self + } + + /// If set to true, will emit responses for core callbacks, like `on_script_loaded`, `on_script_reloaded`, etc. + pub fn with_responses(mut self, emit_responses: bool) -> Self { + self.emit_responses = emit_responses; + self + } fn reload_context( - &self, + attachment: &ScriptAttachment, + content: &[u8], + context: &mut P::C, guard: WorldGuard, handler_ctxt: &HandlerContext

, ) -> Result<(), ScriptError> { - bevy::log::debug!("{}: reloading script with id: {}", P::LANGUAGE, self.id); - let existing_script = match handler_ctxt.scripts.scripts.get(&self.id) { - Some(script) => script, - None => { - return Err( - InteropError::invariant("Tried to reload script which doesn't exist").into(), - ) - } - }; - + bevy::log::debug!("{}: reloading context {}", P::LANGUAGE, attachment); // reload context - let mut context = existing_script.context.lock(); - (ContextBuilder::

::reload)( handler_ctxt.context_loading_settings.loader.reload, - &self.id, - &self.content, - &mut context, + attachment, + content, + context, &handler_ctxt.context_loading_settings.context_initializers, &handler_ctxt .context_loading_settings .context_pre_handling_initializers, guard.clone(), &handler_ctxt.runtime_container.runtime, - )?; - - Ok(()) + ) } fn load_context( - &self, + attachment: &ScriptAttachment, + content: &[u8], guard: WorldGuard, - handler_ctxt: &mut HandlerContext

, - ) -> Result<(), ScriptError> { - bevy::log::debug!("{}: loading script with id: {}", P::LANGUAGE, self.id); - + handler_ctxt: &HandlerContext

, + ) -> Result { + bevy::log::debug!("{}: loading context {}", P::LANGUAGE, attachment); let context = (ContextBuilder::

::load)( handler_ctxt.context_loading_settings.loader.load, - &self.id, - &self.content, + attachment, + content, &handler_ctxt.context_loading_settings.context_initializers, &handler_ctxt .context_loading_settings @@ -135,71 +183,53 @@ impl CreateOrUpdateScript

{ guard.clone(), &handler_ctxt.runtime_container.runtime, )?; - - let context = Arc::new(Mutex::new(context)); - - handler_ctxt.scripts.scripts.insert( - self.id.clone(), - Script { - id: self.id.clone(), - asset: self.asset.clone(), - context, - }, - ); - Ok(()) + Ok(context) } - fn before_load( - &self, + fn before_reload( + attachment: ScriptAttachment, world: WorldGuard, - handler_ctxt: &mut HandlerContext

, - is_reload: bool, + handler_ctxt: &HandlerContext

, + emit_responses: bool, ) -> Option { - if is_reload { - // if something goes wrong, the error will be handled in the command - // but we will not pass the script state to the after_load - return RunScriptCallback::

::new( - self.id.clone(), - Entity::from_raw(0), - OnScriptUnloaded::into_callback_label(), - vec![], - false, - ) - .with_context(P::LANGUAGE) - .with_context("saving reload state") - .run_with_handler(world, handler_ctxt) - .ok(); - } - - None + // if something goes wrong, the error will be handled in the command + // but we will not pass the script state to the after_load + RunScriptCallback::

::new( + attachment.clone(), + OnScriptUnloaded::into_callback_label(), + vec![], + emit_responses, + ) + .with_context(P::LANGUAGE) + .with_context("saving reload state") + .run_with_handler(world, handler_ctxt) + .ok() } fn after_load( - &self, + attachment: ScriptAttachment, world: WorldGuard, - handler_ctxt: &mut HandlerContext

, + handler_ctxt: &HandlerContext

, script_state: Option, + emit_responses: bool, is_reload: bool, ) { let _ = RunScriptCallback::

::new( - self.id.clone(), - Entity::from_raw(0), + attachment.clone(), OnScriptLoaded::into_callback_label(), vec![], - false, + emit_responses, ) .with_context(P::LANGUAGE) .with_context("on loaded callback") .run_with_handler(world.clone(), handler_ctxt); if is_reload { - let state = script_state.unwrap_or(ScriptValue::Unit); let _ = RunScriptCallback::

::new( - self.id.clone(), - Entity::from_raw(0), + attachment.clone(), OnScriptReloaded::into_callback_label(), - vec![state], - false, + vec![script_state.unwrap_or(ScriptValue::Unit)], + emit_responses, ) .with_context(P::LANGUAGE) .with_context("on reloaded callback") @@ -207,124 +237,156 @@ impl CreateOrUpdateScript

{ } } - fn handle_global_context( - &self, + pub(crate) fn create_or_update_script( + attachment: &ScriptAttachment, + content: &[u8], guard: WorldGuard, handler_ctxt: &mut HandlerContext

, - ) -> (Result<(), ScriptError>, Option, bool) { - let existing_context = handler_ctxt - .scripts - .scripts - .values() - .next() - .map(|s| s.context.clone()); - - debug!( - "{}: CreateOrUpdateScript command applying to global context (script_id: {}, new context?: {}, new script?: {})", - P::LANGUAGE, - self.id, - existing_context.is_none(), - !handler_ctxt.scripts.scripts.contains_key(&self.id) - ); - - let is_reload = existing_context.is_some(); - - if let Some(context) = existing_context { - // point all new scripts to the shared context - handler_ctxt.scripts.scripts.insert( - self.id.clone(), - Script { - id: self.id.clone(), - asset: self.asset.clone(), - context, - }, - ); + emit_responses: bool, + ) -> Result<(), ScriptError> { + // we demote to weak from here on out, so as not to hold the asset hostage + let attachment = attachment.clone().into_weak(); + if let ScriptAttachment::StaticScript(id) = &attachment { + // add to static scripts + handler_ctxt.static_scripts.insert(id.clone()); } - let script_state = self.before_load(guard.clone(), handler_ctxt, is_reload); + let script_id = attachment.script(); - let result = if is_reload { - self.reload_context(guard, handler_ctxt) + let phrase; + let success; + let mut script_state = None; + // what callbacks we invoke depends whether or not this attachment + // was already present in the context or not + let is_reload = handler_ctxt.script_context.contains(&attachment); + if is_reload { + phrase = "reloading"; + success = "reloaded"; + + script_state = Self::before_reload( + attachment.clone(), + guard.clone(), + handler_ctxt, + emit_responses, + ); } else { - self.load_context(guard, handler_ctxt) + phrase = "loading"; + success = "loaded"; }; - (result, script_state, is_reload) - } - - fn handle_individual_context( - &self, - guard: WorldGuard, - handler_ctxt: &mut HandlerContext

, - ) -> (Result<(), ScriptError>, Option, bool) { - let is_new_script = !handler_ctxt.scripts.scripts.contains_key(&self.id); - let is_reload = !is_new_script; - - debug!( - "{}: CreateOrUpdateScript command applying (script_id: {}, new context?: {}, new script?: {})", - P::LANGUAGE, - self.id, - is_new_script, - !handler_ctxt.scripts.scripts.contains_key(&self.id) - ); - - let script_state = self.before_load(guard.clone(), handler_ctxt, is_reload); - let result = if is_new_script { - self.load_context(guard, handler_ctxt) - } else { - self.reload_context(guard, handler_ctxt) + // whether or not we actually load vs reload the context (i.e. scrap the old one and create a new one) + // depends on whether the context is already present in the script context + let context = handler_ctxt.script_context.get(&attachment); + let result_context_to_insert = match context { + Some(context) => { + let mut context = context.lock(); + + Self::reload_context( + &attachment, + content, + &mut context, + guard.clone(), + handler_ctxt, + ) + .map(|_| None) + } + None => Self::load_context(&attachment, content, guard.clone(), handler_ctxt).map(Some), }; - (result, script_state, is_reload) - } -} -#[profiling::all_functions] -impl Command for CreateOrUpdateScript

{ - fn apply(self, world: &mut bevy::prelude::World) { - with_handler_system_state(world, |guard, handler_ctxt: &mut HandlerContext

| { - let (result, script_state, is_reload) = - match handler_ctxt.context_loading_settings.assignment_strategy { - crate::context::ContextAssignmentStrategy::Global => { - self.handle_global_context(guard.clone(), handler_ctxt) - } - crate::context::ContextAssignmentStrategy::Individual => { - self.handle_individual_context(guard.clone(), handler_ctxt) + match result_context_to_insert { + Ok(maybe_context) => { + if let Some(context) = maybe_context { + if handler_ctxt + .script_context + .insert(&attachment, context) + .is_err() + { + warn!("Unable to insert script context for {}.", attachment); } - }; + } + + // mark as resident in the context + handler_ctxt + .script_context + .insert_resident(attachment.clone()) + .map_err(|err| { + ScriptError::new(InteropError::invariant(format!( + "expected context to be present, could not mark attachment as resident in context, {err:?}" + ))) + })?; + + bevy::log::debug!( + "{}: script {} successfully {}", + P::LANGUAGE, + attachment, + success, + ); - if let Err(err) = result { + Self::after_load( + attachment, + guard, + handler_ctxt, + script_state, + emit_responses, + is_reload, + ); + + Ok(()) + } + Err(err) => { handle_script_errors( guard, vec![err - .with_script(self.id.clone()) + .clone() + .with_script(script_id.display()) .with_context(P::LANGUAGE) - .with_context(if is_reload { - "reloading an existing script or context" - } else { - "loading a new script or context" - })] + .with_context(phrase)] .into_iter(), ); - return; // don't run after_load if there was an error + Err(err) } + } + } +} - bevy::log::debug!( - "{}: script with id: {} successfully created or updated", - P::LANGUAGE, - self.id - ); +#[profiling::all_functions] +impl Command for CreateOrUpdateScript

{ + fn apply(self, world: &mut bevy::prelude::World) { + let content = match self.content { + Some(content) => content, + None => match world + .get_resource::>() + .and_then(|assets| assets.get(&self.attachment.script())) + .map(|a| a.content.clone()) + { + Some(content) => content, + None => { + bevy::log::error!( + "{}: No content provided for script attachment {}. Cannot attach script.", + P::LANGUAGE, + self.attachment.script().display() + ); + return; + } + }, + }; - self.after_load(guard, handler_ctxt, script_state, is_reload); + with_handler_system_state(world, |guard, handler_ctxt: &mut HandlerContext

| { + let _ = Self::create_or_update_script( + &self.attachment, + &content, + guard.clone(), + handler_ctxt, + self.emit_responses, + ); }); } } /// Runs a callback on the script with the given ID if it exists pub struct RunScriptCallback { - /// The ID of the script to run the callback on - pub id: ScriptId, - /// The entity to use for the callback - pub entity: Entity, + /// The context key + pub attachment: ScriptAttachment, /// The callback to run pub callback: CallbackLabel, /// optional context passed down to errors @@ -340,16 +402,14 @@ pub struct RunScriptCallback { impl RunScriptCallback

{ /// Creates a new RunCallbackCommand with the given ID, callback and arguments pub fn new( - id: ScriptId, - entity: Entity, + attachment: ScriptAttachment, callback: CallbackLabel, args: Vec, trigger_response: bool, ) -> Self { Self { - id, - entity, - context: Default::default(), + attachment, + context: vec![], callback, args, trigger_response, @@ -367,49 +427,45 @@ impl RunScriptCallback

{ pub fn run_with_handler( self, guard: WorldGuard, - handler_ctxt: &mut HandlerContext

, + handler_ctxt: &HandlerContext

, ) -> Result { - if !handler_ctxt.is_script_fully_loaded(self.id.clone()) { - bevy::log::error!( - "{}: Cannot apply callback {} command, as script does not exist: {}. Ignoring.", - P::LANGUAGE, - self.callback, - self.id - ); - return Err(ScriptError::new(InteropError::missing_script( - self.id.clone(), - ))); - } - let result = handler_ctxt.call_dynamic_label( &self.callback, - &self.id, - self.entity, + &self.attachment, + None, self.args, guard.clone(), ); if self.trigger_response { + bevy::log::trace!( + "{}: Sending callback response for callback: {}, attachment: {}", + P::LANGUAGE, + self.callback, + self.attachment, + ); send_callback_response( guard.clone(), ScriptCallbackResponseEvent::new( - self.entity, self.callback, - self.id.clone(), + self.attachment.clone(), result.clone(), + P::LANGUAGE, ), ); } - if let Err(err) = &result { - let mut error_with_context = err.clone().with_script(self.id).with_context(P::LANGUAGE); - for ctxt in &self.context { + if let Err(ref err) = result { + let mut error_with_context = err + .clone() + .with_script(self.attachment.script().display()) + .with_context(P::LANGUAGE); + for ctxt in self.context { error_with_context = error_with_context.with_context(ctxt); } handle_script_errors(guard, vec![error_with_context].into_iter()); } - result } @@ -425,409 +481,62 @@ impl RunScriptCallback

{ impl Command for RunScriptCallback

{ fn apply(self, world: &mut bevy::prelude::World) { - // internals handle this + // Internals handle this. let _ = self.run(world); } } -/// Adds a static script to the collection of static scripts +/// Attaches a static script, by initializing the appropriate script event, which is handled by the BMS systems. pub struct AddStaticScript { /// The ID of the script to add - id: ScriptId, + pub(crate) id: Handle, } impl AddStaticScript { /// Creates a new AddStaticScript command with the given ID - pub fn new(id: impl Into) -> Self { + pub fn new(id: impl Into>) -> Self { Self { id: id.into() } } + + /// Runs the command emitting the appropriate script event + pub fn run(self, events: &mut bevy::prelude::Events) { + events.send(ScriptEvent::Attached { + key: ScriptAttachment::StaticScript(self.id.clone()), + }); + } } impl Command for AddStaticScript { fn apply(self, world: &mut bevy::prelude::World) { - let mut static_scripts = world.get_resource_or_init::(); - static_scripts.insert(self.id); + let mut events = world.get_resource_or_init::>(); + self.run(&mut events); } } -/// Removes a static script from the collection of static scripts +/// Detaches a static script, by initializing the appropriate script event, which is handled by the BMS systems. pub struct RemoveStaticScript { /// The ID of the script to remove - id: ScriptId, + id: Handle, } impl RemoveStaticScript { /// Creates a new RemoveStaticScript command with the given ID - pub fn new(id: ScriptId) -> Self { + pub fn new(id: Handle) -> Self { Self { id } } + + /// Runs the command emitting the appropriate script event + pub fn run(self, events: &mut Events) { + events.send(ScriptEvent::Detached { + key: ScriptAttachment::StaticScript(self.id.clone()), + }); + } } #[profiling::all_functions] impl Command for RemoveStaticScript { fn apply(self, world: &mut bevy::prelude::World) { - let mut static_scripts = world.get_resource_or_init::(); - static_scripts.remove(self.id); - } -} - -#[cfg(test)] -mod test { - use bevy::{ - app::App, - ecs::event::Events, - log::{Level, LogPlugin}, - prelude::{Entity, World}, - }; - - use crate::{ - asset::Language, - bindings::script_value::ScriptValue, - context::{ContextBuilder, ContextLoadingSettings}, - handler::CallbackSettings, - runtime::RuntimeContainer, - script::Scripts, - }; - - use super::*; - - fn setup_app() -> App { - // setup all the resources necessary - let mut app = App::new(); - - app.add_event::(); - app.add_plugins(LogPlugin { - filter: "bevy_mod_scripting_core=debug,info".to_owned(), - level: Level::TRACE, - ..Default::default() - }); - - app.insert_resource(ContextLoadingSettings:: { - loader: ContextBuilder { - load: |name, c, init, pre_run_init, _| { - let mut context = String::from_utf8_lossy(c).into(); - for init in init { - init(name, &mut context)?; - } - for init in pre_run_init { - init(name, Entity::from_raw(0), &mut context)?; - } - Ok(context) - }, - reload: |name, new, existing, init, pre_run_init, _| { - let mut new = String::from_utf8_lossy(new).to_string(); - for init in init { - init(name, &mut new)?; - } - for init in pre_run_init { - init(name, Entity::from_raw(0), &mut new)?; - } - existing.push_str(" | "); - existing.push_str(&new); - Ok(()) - }, - }, - assignment_strategy: Default::default(), - context_initializers: vec![|_, c| { - c.push_str(" initialized"); - Ok(()) - }], - context_pre_handling_initializers: vec![|_, _, c| { - c.push_str(" pre-handling-initialized"); - Ok(()) - }], - }) - .insert_resource(RuntimeContainer:: { - runtime: "Runtime".to_string(), - }) - .init_resource::() - .insert_resource(CallbackSettings:: { - callback_handler: |_, _, _, callback, c, _, _| { - c.push_str(format!(" callback-ran-{callback}").as_str()); - Ok(ScriptValue::Unit) - }, - }) - .insert_resource(Scripts:: { - scripts: Default::default(), - }); - - app - } - - struct DummyPlugin; - - impl IntoScriptPluginParams for DummyPlugin { - type R = String; - type C = String; - const LANGUAGE: Language = Language::Unknown; - - fn build_runtime() -> Self::R { - "Runtime".to_string() - } - } - - fn assert_context_and_script(world: &World, id: &str, context: &str, message: &str) { - let scripts = world.get_resource::>().unwrap(); - - let script = scripts - .scripts - .get(id) - .unwrap_or_else(|| panic!("Script not found {message}")); - - assert_eq!(id, script.id); - let found_context = script.context.lock(); - pretty_assertions::assert_eq!( - *context, - *found_context, - "expected context != actual context. {message}" - ); - } - - fn assert_response_events( - app: &mut World, - expected: impl Iterator, - context: &'static str, - ) { - let mut events = app - .get_resource_mut::>() - .unwrap(); - let responses = events.drain().collect::>(); - let expected: Vec<_> = expected.collect(); - assert_eq!( - responses.len(), - expected.len(), - "Incorrect amount of events received {context}" - ); - for (a, b) in responses.iter().zip(expected.iter()) { - assert_eq!(a.label, b.label, "{context}"); - assert_eq!(a.script, b.script, "{context}"); - assert_eq!(a.response, b.response, "{context}"); - } - } - - #[test] - fn test_commands_with_default_assigner() { - let mut app = setup_app(); - - let content = "content".as_bytes().to_vec().into_boxed_slice(); - let command = CreateOrUpdateScript::::new("script".into(), content, None); - command.apply(app.world_mut()); - - // check script - let loaded_script_expected_content = - "content initialized pre-handling-initialized callback-ran-on_script_loaded"; - assert_context_and_script( - app.world_mut(), - "script", - loaded_script_expected_content, - "Initial script creation failed", - ); - - // update the script - let content = "new content".as_bytes().to_vec().into_boxed_slice(); - let command = CreateOrUpdateScript::::new("script".into(), content, None); - command.apply(app.world_mut()); - - // check script - let reloaded_script_expected_content = format!("{loaded_script_expected_content} callback-ran-on_script_unloaded \ - | new content initialized pre-handling-initialized callback-ran-on_script_loaded callback-ran-on_script_reloaded"); - - assert_context_and_script( - app.world_mut(), - "script", - &reloaded_script_expected_content, - "Script update failed", - ); - - // create second script - let content = "content2".as_bytes().to_vec().into_boxed_slice(); - let command = CreateOrUpdateScript::::new("script2".into(), content, None); - - command.apply(app.world_mut()); - - // check second script - assert_context_and_script( - app.world_mut(), - "script2", - "content2 initialized pre-handling-initialized callback-ran-on_script_loaded", - "Second script creation failed", - ); - - // run a specific callback on the first script - RunScriptCallback::::new( - "script".into(), - Entity::from_raw(0), - OnScriptLoaded::into_callback_label(), - vec![], - true, - ) - .apply(app.world_mut()); - - // check this has applied - assert_context_and_script( - app.world_mut(), - "script", - &format!("{reloaded_script_expected_content} callback-ran-on_script_loaded"), - "Script callback failed", - ); - // assert events sent - assert_response_events( - app.world_mut(), - vec![ScriptCallbackResponseEvent::new( - Entity::from_raw(0), - OnScriptLoaded::into_callback_label(), - "script".into(), - Ok(ScriptValue::Unit), - )] - .into_iter(), - "script callback failed", - ); - - // delete both scripts - let command = DeleteScript::::new("script".into()); - command.apply(app.world_mut()); - let command = DeleteScript::::new("script2".into()); - command.apply(app.world_mut()); - - // check that the scripts are gone - let scripts = app - .world_mut() - .get_resource::>() - .unwrap(); - assert!(scripts.scripts.is_empty()); - - assert_response_events( - app.world_mut(), - vec![].into_iter(), - "did not expect response events", - ); - } - - #[test] - fn test_commands_with_global_assigner() { - // setup all the resources necessary - let mut app = setup_app(); - - let mut settings = app - .world_mut() - .get_resource_mut::>() - .unwrap(); - - settings.assignment_strategy = crate::context::ContextAssignmentStrategy::Global; - - // create a script - let content = "content".as_bytes().to_vec().into_boxed_slice(); - let command = CreateOrUpdateScript::::new("script".into(), content, None); - - command.apply(app.world_mut()); - - // check script - let loaded_script_expected_content = - "content initialized pre-handling-initialized callback-ran-on_script_loaded"; - assert_context_and_script( - app.world(), - "script", - loaded_script_expected_content, - "Initial script creation failed", - ); - - // update the script - - let content = "new content".as_bytes().to_vec().into_boxed_slice(); - let command = CreateOrUpdateScript::::new("script".into(), content, None); - - command.apply(app.world_mut()); - - // check script - - let second_loaded_script_expected_content = - format!("{loaded_script_expected_content} callback-ran-on_script_unloaded \ - | new content initialized pre-handling-initialized callback-ran-on_script_loaded callback-ran-on_script_reloaded"); - assert_context_and_script( - app.world(), - "script", - &second_loaded_script_expected_content, - "Script update failed", - ); - - // create second script - - let content = "content2".as_bytes().to_vec().into_boxed_slice(); - let command = CreateOrUpdateScript::::new("script2".into(), content, None); - - command.apply(app.world_mut()); - - // check both scripts have the new context - let third_loaded_script_expected_content = format!( - "{second_loaded_script_expected_content} callback-ran-on_script_unloaded \ - | content2 initialized pre-handling-initialized callback-ran-on_script_loaded callback-ran-on_script_reloaded", - ); - assert_context_and_script( - app.world(), - "script2", - &third_loaded_script_expected_content, - "second script context was not created correctly", - ); - assert_context_and_script( - app.world(), - "script", - &third_loaded_script_expected_content, - "First script context was not updated on second script insert", - ); - - let scripts = app.world().get_resource::>().unwrap(); - assert!(scripts.scripts.len() == 2); - - // delete first script - let command = DeleteScript::::new("script".into()); - - command.apply(app.world_mut()); - - // check second script still has the context, and on unload was called - assert_context_and_script( - app.world(), - "script2", - &format!("{third_loaded_script_expected_content} callback-ran-on_script_unloaded"), - "first script unload didn't have the desired effect", - ); - - // delete second script - - let command = DeleteScript::::new("script2".into()); - - command.apply(app.world_mut()); - - // check that the scripts are gone, and so is the context - - let scripts = app.world().get_resource::>().unwrap(); - assert!(scripts.scripts.is_empty()); - - let scripts = app.world().get_resource::>().unwrap(); - - assert_eq!(scripts.scripts.len(), 0, "scripts weren't removed"); - assert_response_events( - app.world_mut(), - vec![].into_iter(), - "did not expect any response events", - ); - } - - #[test] - fn test_static_scripts() { - let mut app = setup_app(); - - let world = app.world_mut(); - - let command = AddStaticScript::new("script"); - command.apply(world); - - let static_scripts = world.get_resource::().unwrap(); - assert!(static_scripts.contains("script")); - - let command = RemoveStaticScript::new("script".into()); - command.apply(world); - - let static_scripts = world.get_resource::().unwrap(); - assert!(!static_scripts.contains("script")); + let mut events = world.get_resource_or_init::>(); + self.run(&mut events); } } diff --git a/crates/bevy_mod_scripting_core/src/context.rs b/crates/bevy_mod_scripting_core/src/context.rs index b626b759de..9bf6649c6d 100644 --- a/crates/bevy_mod_scripting_core/src/context.rs +++ b/crates/bevy_mod_scripting_core/src/context.rs @@ -3,10 +3,10 @@ use crate::{ bindings::{ThreadWorldContainer, WorldContainer, WorldGuard}, error::{InteropError, ScriptError}, - script::ScriptId, + script::ScriptAttachment, IntoScriptPluginParams, }; -use bevy::{ecs::entity::Entity, prelude::Resource}; +use bevy::prelude::Resource; /// A trait that all script contexts must implement. /// @@ -17,19 +17,20 @@ impl Context for T {} /// Initializer run once after creating a context but before executing it for the first time as well as after re-loading the script pub type ContextInitializer

= - fn(&str, &mut

::C) -> Result<(), ScriptError>; + fn(&ScriptAttachment, &mut

::C) -> Result<(), ScriptError>; /// Initializer run every time before executing or loading/re-loading a script pub type ContextPreHandlingInitializer

= - fn(&str, Entity, &mut

::C) -> Result<(), ScriptError>; + fn(&ScriptAttachment, &mut

::C) -> Result<(), ScriptError>; /// Settings concerning the creation and assignment of script contexts as well as their initialization. #[derive(Resource)] pub struct ContextLoadingSettings { + /// Whether to emit responses from core script_callbacks like `on_script_loaded` or `on_script_unloaded`. + /// By default, this is `false` and responses are not emitted. + pub emit_responses: bool, /// Defines the strategy used to load and reload contexts pub loader: ContextBuilder

, - /// Defines the strategy used to assign contexts to scripts - pub assignment_strategy: ContextAssignmentStrategy, /// Initializers run once after creating a context but before executing it for the first time pub context_initializers: Vec>, /// Initializers run every time before executing or loading a script @@ -39,8 +40,8 @@ pub struct ContextLoadingSettings { impl Default for ContextLoadingSettings

{ fn default() -> Self { Self { + emit_responses: false, loader: ContextBuilder::default(), - assignment_strategy: Default::default(), context_initializers: Default::default(), context_pre_handling_initializers: Default::default(), } @@ -50,8 +51,8 @@ impl Default for ContextLoadingSettings

{ impl Clone for ContextLoadingSettings { fn clone(&self) -> Self { Self { + emit_responses: self.emit_responses, loader: self.loader.clone(), - assignment_strategy: self.assignment_strategy, context_initializers: self.context_initializers.clone(), context_pre_handling_initializers: self.context_pre_handling_initializers.clone(), } @@ -59,7 +60,7 @@ impl Clone for ContextLoadingSettings { } /// A strategy for loading contexts pub type ContextLoadFn

= fn( - script_id: &ScriptId, + attachment: &ScriptAttachment, content: &[u8], context_initializers: &[ContextInitializer

], pre_handling_initializers: &[ContextPreHandlingInitializer

], @@ -68,7 +69,7 @@ pub type ContextLoadFn

= fn( /// A strategy for reloading contexts pub type ContextReloadFn

= fn( - script_id: &ScriptId, + attachment: &ScriptAttachment, content: &[u8], previous_context: &mut

::C, context_initializers: &[ContextInitializer

], @@ -99,7 +100,7 @@ impl ContextBuilder

{ /// load a context pub fn load( loader: ContextLoadFn

, - script: &ScriptId, + attachment: &ScriptAttachment, content: &[u8], context_initializers: &[ContextInitializer

], pre_handling_initializers: &[ContextPreHandlingInitializer

], @@ -109,7 +110,7 @@ impl ContextBuilder

{ WorldGuard::with_existing_static_guard(world.clone(), |world| { ThreadWorldContainer.set_world(world)?; (loader)( - script, + attachment, content, context_initializers, pre_handling_initializers, @@ -121,7 +122,7 @@ impl ContextBuilder

{ /// reload a context pub fn reload( reloader: ContextReloadFn

, - script: &ScriptId, + attachment: &ScriptAttachment, content: &[u8], previous_context: &mut P::C, context_initializers: &[ContextInitializer

], @@ -132,7 +133,7 @@ impl ContextBuilder

{ WorldGuard::with_existing_static_guard(world, |world| { ThreadWorldContainer.set_world(world)?; (reloader)( - script, + attachment, content, previous_context, context_initializers, @@ -151,13 +152,3 @@ impl Clone for ContextBuilder

{ } } } - -/// The strategy used in assigning contexts to scripts -#[derive(Default, Clone, Copy)] -pub enum ContextAssignmentStrategy { - /// Assign a new context to each script - #[default] - Individual, - /// Share contexts with all other scripts - Global, -} diff --git a/crates/bevy_mod_scripting_core/src/error.rs b/crates/bevy_mod_scripting_core/src/error.rs index 8b2bd2a967..a5548654b0 100644 --- a/crates/bevy_mod_scripting_core/src/error.rs +++ b/crates/bevy_mod_scripting_core/src/error.rs @@ -1,5 +1,6 @@ //! Errors that can occur when interacting with the scripting system +use crate::script::DisplayProxy; use crate::{ bindings::{ access_map::{DisplayCodeLocation, ReflectAccessId}, @@ -8,9 +9,11 @@ use crate::{ script_value::ScriptValue, ReflectBaseType, ReflectReference, }, - script::ScriptId, + script::ContextKey, + ScriptAsset, }; use bevy::{ + asset::{AssetPath, Handle}, ecs::{ component::ComponentId, schedule::{ScheduleBuildError, ScheduleNotInitialized}, @@ -592,16 +595,25 @@ impl InteropError { } /// Thrown if a script could not be found when trying to call a synchronous callback or otherwise - pub fn missing_script(script_id: impl Into) -> Self { + pub fn missing_script(script_id: impl Into>) -> Self { Self(Arc::new(InteropErrorInner::MissingScript { - script_id: script_id.into(), + script_id: Some(script_id.into()), + script_path: None, + })) + } + + /// Thrown if a script could not be found when trying to call a synchronous callback or otherwise + pub fn missing_script_by_path<'a>(script_id: impl Into>) -> Self { + Self(Arc::new(InteropErrorInner::MissingScript { + script_path: Some(script_id.into().to_string()), + script_id: None, })) } /// Thrown if the required context for an operation is missing. - pub fn missing_context(script_id: impl Into) -> Self { + pub fn missing_context(context_key: impl Into) -> Self { Self(Arc::new(InteropErrorInner::MissingContext { - script_id: script_id.into(), + context_key: context_key.into(), })) } @@ -626,9 +638,12 @@ pub enum InteropErrorInner { /// Thrown if a callback requires world access, but is unable to do so due MissingWorld, /// Thrown if a script could not be found when trying to call a synchronous callback. + /// The path or id is used depending on which stage the script was in when the error occurred. MissingScript { + /// The script path that was not found. + script_path: Option, /// The script id that was not found. - script_id: ScriptId, + script_id: Option>, }, /// Thrown if a base type is not registered with the reflection system UnregisteredBase { @@ -812,7 +827,7 @@ pub enum InteropErrorInner { /// Thrown if the required context for an operation is missing. MissingContext { /// The script that was attempting to access the context - script_id: ScriptId, + context_key: ContextKey, }, /// Thrown when a schedule is missing from the registry. MissingSchedule { @@ -826,9 +841,15 @@ impl PartialEq for InteropErrorInner { fn eq(&self, _other: &Self) -> bool { match (self, _other) { ( - InteropErrorInner::MissingScript { script_id: a }, - InteropErrorInner::MissingScript { script_id: b }, - ) => a == b, + InteropErrorInner::MissingScript { + script_id: a, + script_path: b, + }, + InteropErrorInner::MissingScript { + script_id: c, + script_path: d, + }, + ) => a == c && b == d, ( InteropErrorInner::InvalidAccessCount { count: a, @@ -1050,8 +1071,8 @@ impl PartialEq for InteropErrorInner { }, ) => a == c && b == d, ( - InteropErrorInner::MissingContext { script_id: b }, - InteropErrorInner::MissingContext { script_id: d }, + InteropErrorInner::MissingContext { context_key: b }, + InteropErrorInner::MissingContext { context_key: d }, ) => b == d, ( InteropErrorInner::MissingSchedule { schedule_name: a }, @@ -1252,10 +1273,13 @@ macro_rules! unregistered_component_or_resource_type { } macro_rules! missing_script_for_callback { - ($script_id:expr) => { + ($script_id:expr, $script_path:expr) => { format!( - "Could not find script with id: {}. Is the script loaded?", - $script_id + "Could not find script {}. Is the script loaded?", + $script_id.map_or_else( + || $script_path.unwrap_or_default(), + |id| id.display().to_string() + ) ) }; } @@ -1281,10 +1305,10 @@ macro_rules! argument_count_mismatch_msg { } macro_rules! missing_context_for_callback { - ($script_id:expr) => { + ($context_key:expr) => { format!( - "Missing context for script with id: {}. Was the script loaded?.", - $script_id + "Missing context for {}. Was the script loaded?.", + $context_key ) }; } @@ -1427,12 +1451,12 @@ impl DisplayWithWorld for InteropErrorInner { InteropErrorInner::ArgumentCountMismatch { expected, got } => { argument_count_mismatch_msg!(expected, got) }, - InteropErrorInner::MissingScript { script_id } => { - missing_script_for_callback!(script_id) + InteropErrorInner::MissingScript { script_id, script_path } => { + missing_script_for_callback!(script_id.clone(), script_path.clone()) }, - InteropErrorInner::MissingContext { script_id } => { + InteropErrorInner::MissingContext { context_key } => { missing_context_for_callback!( - script_id + context_key ) }, InteropErrorInner::MissingSchedule { schedule_name } => { @@ -1573,12 +1597,12 @@ impl DisplayWithWorld for InteropErrorInner { InteropErrorInner::ArgumentCountMismatch { expected, got } => { argument_count_mismatch_msg!(expected, got) }, - InteropErrorInner::MissingScript { script_id } => { - missing_script_for_callback!(script_id) + InteropErrorInner::MissingScript { script_id, script_path } => { + missing_script_for_callback!(script_id.clone(), script_path.clone()) }, - InteropErrorInner::MissingContext { script_id } => { + InteropErrorInner::MissingContext { context_key } => { missing_context_for_callback!( - script_id + context_key ) }, InteropErrorInner::MissingSchedule { schedule_name } => { diff --git a/crates/bevy_mod_scripting_core/src/event.rs b/crates/bevy_mod_scripting_core/src/event.rs index b280ccb503..c4f7ee10e0 100644 --- a/crates/bevy_mod_scripting_core/src/event.rs +++ b/crates/bevy_mod_scripting_core/src/event.rs @@ -1,7 +1,75 @@ //! Event handlers and event types for scripting. -use crate::{bindings::script_value::ScriptValue, error::ScriptError, script::ScriptId}; -use bevy::{ecs::entity::Entity, prelude::Event, reflect::Reflect}; +use std::sync::Arc; + +use crate::{ + asset::Language, + bindings::script_value::ScriptValue, + error::ScriptError, + script::{ScriptAttachment, ScriptContext, ScriptId}, + IntoScriptPluginParams, +}; +use bevy::{asset::Handle, ecs::entity::Entity, prelude::Event, reflect::Reflect}; +use parking_lot::Mutex; + +/// A script event +#[derive(Event, Debug, Clone, PartialEq, Eq)] +pub enum ScriptEvent { + /// A script asset was added. + Added { + /// The script + script: ScriptId, + }, + /// A script asset was removed. + Removed { + /// The script + script: ScriptId, + }, + /// A script asset was modified. + Modified { + /// The script + script: ScriptId, + }, + /// A script was activated and attached via a [`ScriptAttachment`]. + Attached { + /// The script attachment + key: ScriptAttachment, + }, + /// A script was deactivated and detached via a [`ScriptAttachment`]. + Detached { + /// The script attachment which was detached + key: ScriptAttachment, + }, + // These were some other events I was considering. I thought Unloaded might + // be interesting, but if I implemented it the way things work currently it + // could only be a notification. The user wouldn't be able to do anything + // between an Unloaded and Loaded event that could affect the Unloaded + // value. Maybe that's fine. I'm leaving it here purely to communicate the + // idea. It can be removed. + + // /// A script was loaded/evaluated. + // Loaded { + // /// The script + // script: ScriptId, + // /// The entity + // entity: Option, + // /// The domain + // domain: Option, + // }, + // /// A script was unloaded, perhaps producing a value. + // Unloaded { + // /// The context key + // context_key: ContextKey, + // // /// The script + // // script: ScriptId, + // // /// The entity + // // entity: Option, + // // /// The domain + // // domain: Option, + // /// The unloaded value + // value: Option + // }, +} /// An error coming from a script #[derive(Debug, Event)] @@ -122,14 +190,43 @@ impl std::fmt::Display for CallbackLabel { /// Describes the designated recipients of a script event #[derive(Clone, Debug)] pub enum Recipients { - /// The event needs to be handled by all scripts - All, - /// The event is to be handled by a specific script - Script(ScriptId), - /// The event is to be handled by all the scripts on the specified entity - Entity(Entity), - /// The event is to be handled by all the scripts of one language - Language(crate::asset::Language), + /// The event needs to be handled by all scripts, if multiple scripts share a context, the event will be sent once per script in the context. + AllScripts, + /// The event is to be handled by all unique contexts, i.e. if two scripts share the same context, the event will be sent only once per the context. + AllContexts, + /// The event is to be handled by a specific script-entity pair + ScriptEntity(ScriptId, Entity), + /// the event is to be handled by a specific static script + StaticScript(ScriptId), +} + +impl Recipients { + /// Retrieves all the recipients of the event based on existing scripts + pub fn get_recipients( + &self, + script_context: &ScriptContext

, + ) -> Vec<(ScriptAttachment, Arc>)> { + match self { + Recipients::AllScripts => script_context.all_residents().collect(), + Recipients::AllContexts => script_context.first_resident_from_each_context().collect(), + Recipients::ScriptEntity(script, entity) => { + let attachment = ScriptAttachment::EntityScript(*entity, Handle::Weak(*script)); + script_context + .get(&attachment) + .into_iter() + .map(|entry| (attachment.clone(), entry)) + .collect() + } + Recipients::StaticScript(script) => { + let attachment = ScriptAttachment::StaticScript(Handle::Weak(*script)); + script_context + .get(&attachment) + .into_iter() + .map(|entry| (attachment.clone(), entry)) + .collect() + } + } + } } /// A callback event meant to trigger a callback in a subset/set of scripts in the world with the given arguments @@ -140,6 +237,8 @@ pub struct ScriptCallbackEvent { pub label: CallbackLabel, /// The recipients of the callback pub recipients: Recipients, + /// The language of the callback, if unspecified will apply to all languages + pub language: Option, /// The arguments to the callback pub args: Vec, /// Whether the callback should emit a response event @@ -152,9 +251,11 @@ impl ScriptCallbackEvent { label: L, args: Vec, recipients: Recipients, + language: Option, ) -> Self { Self { label: label.into(), + language, args, recipients, trigger_response: false, @@ -169,41 +270,51 @@ impl ScriptCallbackEvent { self } - /// Creates a new callback event with the given label, arguments and all scripts as recipients - pub fn new_for_all>(label: L, args: Vec) -> Self { - Self::new(label, args, Recipients::All) + /// Creates a new callback event with the given label, arguments and all scripts and languages as recipients + pub fn new_for_all_scripts>(label: L, args: Vec) -> Self { + Self::new(label, args, Recipients::AllScripts, None) + } + + /// Creates a new callback event with the given label, arguments and all contexts (which can contain multiple scripts) and languages as recipients + pub fn new_for_all_contexts>(label: L, args: Vec) -> Self { + Self::new(label, args, Recipients::AllContexts, None) } } -/// Event published when a script completes a callback, and a response is requested +/// Event published when a script completes a callback and a response is requested. #[derive(Clone, Event, Debug)] #[non_exhaustive] pub struct ScriptCallbackResponseEvent { - /// the entity that the script was invoked on, - pub entity: Entity, /// the label of the callback pub label: CallbackLabel, - /// the script that replied - pub script: ScriptId, + /// the language of the callback that replied + pub language: Language, + /// the key to the context that replied + pub context_key: ScriptAttachment, /// the response received pub response: Result, } impl ScriptCallbackResponseEvent { - /// Creates a new callback response event with the given label, script and response + /// Creates a new callback response event with the given label, script, and response. pub fn new>( - entity: Entity, label: L, - script: ScriptId, + context_key: ScriptAttachment, response: Result, + language: Language, ) -> Self { Self { - entity, label: label.into(), - script, + context_key, response, + language, } } + + /// Return the source entity for the callback if there was any. + pub fn source_entity(&self) -> Option { + self.context_key.entity() + } } static FORBIDDEN_KEYWORDS: [&str; 82] = [ @@ -295,6 +406,21 @@ static FORBIDDEN_KEYWORDS: [&str; 82] = [ #[cfg(test)] mod test { + use std::sync::Arc; + + use bevy::{ + asset::{AssetId, AssetIndex, Handle}, + ecs::entity::Entity, + }; + use parking_lot::Mutex; + use test_utils::make_test_plugin; + + use crate::{ + bindings::ScriptValue, + event::Recipients, + script::{ContextPolicy, ScriptAttachment, ScriptContext}, + }; + use super::FORBIDDEN_KEYWORDS; #[test] @@ -335,4 +461,180 @@ mod test { assert_eq!(super::CallbackLabel::new_lossy(ident).as_ref(), *ident); }); } + + make_test_plugin!(crate); + + /// make the following arrangement: + /// use AssetId's to identify residents + /// ContextA: + /// - EntityScriptA (Entity::from_raw(0), AssetId::from_bits(0)) + /// - EntityScriptB (Entity::from_raw(0), AssetId::from_bits(1)) + /// + /// ContextB: + /// - EntityScriptC (Entity::from_raw(1), AssetId::from_bits(2)) + /// - EntityScriptD (Entity::from_raw(1), AssetId::from_bits(3)) + /// + /// ContextC: + /// - StaticScriptA (AssetId::from_bits(4)) + /// + /// ContextD: + /// - StaticScriptB (AssetId::from_bits(5)) + fn make_test_contexts() -> ScriptContext { + let policy = ContextPolicy::per_entity(); + let mut script_context = ScriptContext::::new(policy); + let context_a = TestContext { + invocations: vec![ScriptValue::String("a".to_string().into())], + }; + let context_b = TestContext { + invocations: vec![ScriptValue::String("b".to_string().into())], + }; + let context_c = TestContext { + invocations: vec![ScriptValue::String("c".to_string().into())], + }; + let context_d = TestContext { + invocations: vec![ScriptValue::String("d".to_string().into())], + }; + + let entity_script_a = Handle::Weak(AssetId::from(AssetIndex::from_bits(0))); + let entity_script_b = Handle::Weak(AssetId::from(AssetIndex::from_bits(1))); + let entity_script_c = Handle::Weak(AssetId::from(AssetIndex::from_bits(2))); + let entity_script_d = Handle::Weak(AssetId::from(AssetIndex::from_bits(3))); + + let static_script_a = Handle::Weak(AssetId::from(AssetIndex::from_bits(4))); + let static_script_b = Handle::Weak(AssetId::from(AssetIndex::from_bits(5))); + + script_context + .insert( + &ScriptAttachment::EntityScript(Entity::from_raw(0), entity_script_a), + context_a, + ) + .unwrap(); + + script_context + .insert_resident(ScriptAttachment::EntityScript( + Entity::from_raw(0), + entity_script_b, + )) + .unwrap(); + + script_context + .insert( + &ScriptAttachment::EntityScript(Entity::from_raw(1), entity_script_c), + context_b, + ) + .unwrap(); + script_context + .insert_resident(ScriptAttachment::EntityScript( + Entity::from_raw(1), + entity_script_d, + )) + .unwrap(); + + script_context + .insert(&ScriptAttachment::StaticScript(static_script_a), context_c) + .unwrap(); + + script_context + .insert(&ScriptAttachment::StaticScript(static_script_b), context_d) + .unwrap(); + + script_context + } + + fn recipients_to_asset_ids( + recipients: &[(ScriptAttachment, Arc>)], + ) -> Vec<(usize, String)> { + recipients + .iter() + .map(|(attachment, context)| { + if let AssetId::Index { index, .. } = attachment.script().id() { + let locked = context.lock(); + let first_invocation_string = + if let Some(ScriptValue::String(s)) = locked.invocations.first() { + s.clone() + } else { + panic!("Expected first invocation to be a string") + }; + ( + index.to_bits() as usize, + first_invocation_string.to_string(), + ) + } else { + panic!( + "Expected AssetId::Index, got {:?}", + attachment.script().id() + ) + } + }) + .collect() + } + + #[test] + fn test_all_scripts_recipients() { + let script_context = make_test_contexts(); + let recipients = Recipients::AllScripts.get_recipients(&script_context); + assert_eq!(recipients.len(), 6); + let mut id_context_pairs = recipients_to_asset_ids(&recipients); + + id_context_pairs.sort_by_key(|(id, _)| *id); + + assert_eq!( + id_context_pairs, + vec![ + (0, "a".to_string()), + (1, "a".to_string()), + (2, "b".to_string()), + (3, "b".to_string()), + (4, "c".to_string()), + (5, "d".to_string()), + ] + ); + } + + #[test] + fn test_all_contexts_recipients() { + let script_context = make_test_contexts(); + let recipients = Recipients::AllContexts.get_recipients(&script_context); + assert_eq!(recipients.len(), 4); + let mut id_context_pairs = recipients_to_asset_ids(&recipients); + id_context_pairs.sort_by_key(|(id, _)| *id); + + // expect one of 0,1 for context a and one of 2,3 for context b + // and 4 for context c and 5 for context d + + // we can't just use equality here because the order of contexts is not guaranteed + assert!( + id_context_pairs.contains(&(0, "a".to_string())) + || id_context_pairs.contains(&(1, "a".to_string())) + ); + assert!( + id_context_pairs.contains(&(2, "b".to_string())) + || id_context_pairs.contains(&(3, "b".to_string())) + ); + assert!(id_context_pairs.contains(&(4, "c".to_string()))); + assert!(id_context_pairs.contains(&(5, "d".to_string()))); + } + + #[test] + fn test_script_entity_recipients() { + let script_context = make_test_contexts(); + let recipients = + Recipients::ScriptEntity(AssetId::from(AssetIndex::from_bits(0)), Entity::from_raw(0)) + .get_recipients(&script_context); + + assert_eq!(recipients.len(), 1); + let id_context_pairs = recipients_to_asset_ids(&recipients); + assert_eq!(id_context_pairs, vec![(0, "a".to_string())]); + } + + #[test] + fn test_static_script_recipients() { + let script_context = make_test_contexts(); + let recipients = Recipients::StaticScript(AssetId::from(AssetIndex::from_bits(4))) + .get_recipients(&script_context); + + assert_eq!(recipients.len(), 1); + let id_context_pairs = recipients_to_asset_ids(&recipients); + assert_eq!(id_context_pairs, vec![(4, "c".to_string())]); + } } diff --git a/crates/bevy_mod_scripting_core/src/extractors.rs b/crates/bevy_mod_scripting_core/src/extractors.rs index 48d4f5fbe0..e72ea9172e 100644 --- a/crates/bevy_mod_scripting_core/src/extractors.rs +++ b/crates/bevy_mod_scripting_core/src/extractors.rs @@ -2,34 +2,33 @@ //! //! These are designed to be used to pipe inputs into other systems which require them, while handling any configuration erorrs nicely. #![allow(deprecated)] -use std::ops::{Deref, DerefMut}; - -use bevy::{ - ecs::{ - component::ComponentId, - entity::Entity, - query::{Access, AccessConflicts}, - storage::SparseSetIndex, - system::{SystemParam, SystemParamValidationError}, - world::World, - }, - prelude::Resource, -}; -use fixedbitset::FixedBitSet; - +use crate::bindings::pretty_print::DisplayWithWorld; use crate::{ bindings::{ - access_map::ReflectAccessId, pretty_print::DisplayWithWorld, script_value::ScriptValue, - WorldAccessGuard, WorldGuard, + access_map::ReflectAccessId, script_value::ScriptValue, WorldAccessGuard, WorldGuard, }, context::ContextLoadingSettings, error::{InteropError, ScriptError}, event::{CallbackLabel, IntoCallbackLabel}, handler::CallbackSettings, runtime::RuntimeContainer, - script::{ScriptId, Scripts, StaticScripts}, + script::{ScriptAttachment, ScriptContext, StaticScripts}, IntoScriptPluginParams, }; +use bevy::ecs::resource::Resource; +use bevy::ecs::{ + component::ComponentId, + query::{Access, AccessConflicts}, + storage::SparseSetIndex, + system::{SystemParam, SystemParamValidationError}, + world::World, +}; +use fixedbitset::FixedBitSet; +use parking_lot::Mutex; +use std::{ + ops::{Deref, DerefMut}, + sync::Arc, +}; /// Executes `system_state.get_mut` followed by `system_state.apply` after running the given closure, makes sure state is correctly handled in the context of an exclusive system. /// Using system state with a handler ctxt without applying the state after will leave the world in an inconsistent state. @@ -49,16 +48,24 @@ pub fn with_handler_system_state< o } -/// Semantics of [`bevy::ecs::change_detection::Res`] but doesn't claim read or write on the world by removing the resource from it ahead of time. +/// Semantics of [`bevy::ecs::change_detection::Res`] but doesn't claim read or +/// write on the world by removing the resource from it ahead of time. /// /// Similar to using [`World::resource_scope`]. /// -/// This is useful for interacting with scripts, since [`WithWorldGuard`] will ensure scripts cannot gain exclusive access to the world if *any* reads or writes -/// are claimed on the world. Removing the resource from the world lets you access it in the context of running scripts without blocking exclusive world access. +/// This is useful for interacting with scripts, since [`WithWorldGuard`] will +/// ensure scripts cannot gain exclusive access to the world if *any* reads or +/// writes are claimed on the world. Removing the resource from the world lets +/// you access it in the context of running scripts without blocking exclusive +/// world access. /// /// # Safety -/// - Because the resource is removed during the `get_param` call, if there is a conflicting resource access, this will be unsafe -/// - You must ensure you're only using this in combination with system parameters which will not read or write to this resource in `get_param` +/// +/// - Because the resource is removed during the `get_param` call, if there is a +/// conflicting resource access, this will be unsafe +/// +/// - You must ensure you're only using this in combination with system +/// parameters which will not read or write to this resource in `get_param` pub(crate) struct ResScope<'state, T: Resource + Default>(pub &'state mut T); impl Deref for ResScope<'_, T> { @@ -119,12 +126,12 @@ pub struct HandlerContext { pub(crate) callback_settings: CallbackSettings

, /// Settings for loading contexts pub(crate) context_loading_settings: ContextLoadingSettings

, - /// Scripts - pub(crate) scripts: Scripts

, /// The runtime container pub(crate) runtime_container: RuntimeContainer

, /// List of static scripts pub(crate) static_scripts: StaticScripts, + /// Script context + pub(crate) script_context: ScriptContext

, } impl HandlerContext

{ @@ -134,9 +141,9 @@ impl HandlerContext

{ Self { callback_settings: world.remove_resource().unwrap_or_default(), context_loading_settings: world.remove_resource().unwrap_or_default(), - scripts: world.remove_resource().unwrap_or_default(), runtime_container: world.remove_resource().unwrap_or_default(), static_scripts: world.remove_resource().unwrap_or_default(), + script_context: world.remove_resource().unwrap_or_default(), } } @@ -146,9 +153,9 @@ impl HandlerContext

{ // insert the handler context back into the world world.insert_resource(self.callback_settings); world.insert_resource(self.context_loading_settings); - world.insert_resource(self.scripts); world.insert_resource(self.runtime_container); world.insert_resource(self.static_scripts); + world.insert_resource(self.script_context); } /// Splits the handler context into its individual components. @@ -160,14 +167,12 @@ impl HandlerContext

{ ) -> ( &mut CallbackSettings

, &mut ContextLoadingSettings

, - &mut Scripts

, &mut RuntimeContainer

, &mut StaticScripts, ) { ( &mut self.callback_settings, &mut self.context_loading_settings, - &mut self.scripts, &mut self.runtime_container, &mut self.static_scripts, ) @@ -183,11 +188,6 @@ impl HandlerContext

{ &mut self.context_loading_settings } - /// Get the scripts - pub fn scripts(&mut self) -> &mut Scripts

{ - &mut self.scripts - } - /// Get the runtime container pub fn runtime_container(&mut self) -> &mut RuntimeContainer

{ &mut self.runtime_container @@ -198,24 +198,31 @@ impl HandlerContext

{ &mut self.static_scripts } + /// Get the static scripts + pub fn script_context(&mut self) -> &mut ScriptContext

{ + &mut self.script_context + } + /// checks if the script is loaded such that it can be executed. - pub fn is_script_fully_loaded(&self, script_id: ScriptId) -> bool { - self.scripts.scripts.contains_key(&script_id) + /// + /// since the mapping between scripts and contexts is not one-to-one, will map the context key using the + /// context policy to find the script context, if one is found then the script is loaded. + pub fn is_script_fully_loaded(&self, key: &ScriptAttachment) -> bool { + self.script_context.contains(key) } /// Equivalent to [`Self::call`] but with a dynamically passed in label pub fn call_dynamic_label( &self, label: &CallbackLabel, - script_id: &ScriptId, - entity: Entity, + context_key: &ScriptAttachment, + context: Option>>, payload: Vec, guard: WorldGuard<'_>, ) -> Result { // find script - let script = match self.scripts.scripts.get(script_id) { - Some(script) => script, - None => return Err(InteropError::missing_script(script_id.clone()).into()), + let Some(context) = context.or_else(|| self.script_context.get(context_key)) else { + return Err(InteropError::missing_context(context_key.clone()).into()); }; // call the script @@ -225,13 +232,12 @@ impl HandlerContext

{ .context_pre_handling_initializers; let runtime = &self.runtime_container.runtime; - let mut context = script.context.lock(); + let mut context = context.lock(); CallbackSettings::

::call( handler, payload, - entity, - script_id, + context_key, label, &mut context, pre_handling_initializers, @@ -246,12 +252,11 @@ impl HandlerContext

{ /// Run [`Self::is_script_fully_loaded`] before calling the script to ensure the script and context were loaded ahead of time. pub fn call( &self, - script_id: &ScriptId, - entity: Entity, + context_key: &ScriptAttachment, payload: Vec, guard: WorldGuard<'_>, ) -> Result { - self.call_dynamic_label(&C::into_callback_label(), script_id, entity, payload, guard) + self.call_dynamic_label(&C::into_callback_label(), context_key, None, payload, guard) } } diff --git a/crates/bevy_mod_scripting_core/src/handler.rs b/crates/bevy_mod_scripting_core/src/handler.rs index b18d7fdcbb..f6be314c6e 100644 --- a/crates/bevy_mod_scripting_core/src/handler.rs +++ b/crates/bevy_mod_scripting_core/src/handler.rs @@ -5,32 +5,30 @@ use crate::{ WorldAccessGuard, WorldContainer, WorldGuard, }, context::ContextPreHandlingInitializer, - error::{InteropErrorInner, ScriptError}, + error::ScriptError, event::{ CallbackLabel, IntoCallbackLabel, ScriptCallbackEvent, ScriptCallbackResponseEvent, ScriptErrorEvent, }, extractors::{HandlerContext, WithWorldGuard}, - script::{ScriptComponent, ScriptId}, - IntoScriptPluginParams, + script::ScriptAttachment, + IntoScriptPluginParams, Language, }; use bevy::{ ecs::{ - entity::Entity, event::EventCursor, - query::QueryState, + resource::Resource, system::{Local, SystemState}, world::{Mut, World}, }, - log::trace_once, - prelude::{Events, Resource}, + log::error, + prelude::Events, }; /// A function that handles a callback event pub type HandlerFn

= fn( args: Vec, - entity: Entity, - script_id: &ScriptId, + context_key: &ScriptAttachment, callback: &CallbackLabel, context: &mut

::C, pre_handling_initializers: &[ContextPreHandlingInitializer

], @@ -47,7 +45,7 @@ pub struct CallbackSettings { impl Default for CallbackSettings

{ fn default() -> Self { Self { - callback_handler: |_, _, _, _, _, _, _| Ok(ScriptValue::Unit), + callback_handler: |_, _, _, _, _, _| Ok(ScriptValue::Unit), } } } @@ -71,8 +69,7 @@ impl CallbackSettings

{ pub fn call( handler: HandlerFn

, args: Vec, - entity: Entity, - script_id: &ScriptId, + context_key: &ScriptAttachment, callback: &CallbackLabel, script_ctxt: &mut P::C, pre_handling_initializers: &[ContextPreHandlingInitializer

], @@ -83,8 +80,7 @@ impl CallbackSettings

{ ThreadWorldContainer.set_world(world)?; (handler)( args, - entity, - script_id, + context_key, callback, script_ctxt, pre_handling_initializers, @@ -94,18 +90,6 @@ impl CallbackSettings

{ } } -macro_rules! push_err_and_continue { - ($errors:ident, $expr:expr) => { - match $expr { - Ok(v) => v, - Err(e) => { - $errors.push(e); - continue; - } - } - }; -} - /// Passes events with the specified label to the script callback with the same name and runs the callback. /// /// If any of the resources required for the handler are missing, the system will log this issue and do nothing. @@ -117,21 +101,15 @@ pub fn event_handler( // we wrap the inner event handler, so that we can guarantee that the handler context is released statically { let handler_ctxt = HandlerContext::

::yoink(world); - let (query_state, event_cursor, mut guard) = state.get_mut(world); + let (event_cursor, mut guard) = state.get_mut(world); let (guard, _) = guard.get_mut(); - let handler_ctxt = event_handler_inner::

( - L::into_callback_label(), - query_state, - event_cursor, - handler_ctxt, - guard, - ); + let handler_ctxt = + event_handler_inner::

(L::into_callback_label(), event_cursor, handler_ctxt, guard); handler_ctxt.release(world); } } type EventHandlerSystemState<'w, 's> = SystemState<( - Local<'s, QueryState<(Entity, &'w ScriptComponent)>>, Local<'s, EventCursor>, WithWorldGuard<'w, 's, ()>, )>; @@ -140,7 +118,6 @@ type EventHandlerSystemState<'w, 's> = SystemState<( #[allow(deprecated)] pub(crate) fn event_handler_inner( callback_label: CallbackLabel, - mut entity_query_state: Local>, mut event_cursor: Local>, handler_ctxt: HandlerContext

, guard: WorldAccessGuard, @@ -157,115 +134,62 @@ pub(crate) fn event_handler_inner( let events = match events { Ok(events) => events, - Err(e) => { - bevy::log::error!( + Err(err) => { + error!( "Failed to read script callback events: {}", - e.display_with_world(guard.clone()) + err.display_with_world(guard) ); return handler_ctxt; } }; - // query entities + chain static scripts - let entity_and_static_scripts = guard.with_global_access(|w| { - entity_query_state - .iter(w) - .map(|(e, s)| (e, s.0.clone())) - .chain( - handler_ctxt - .static_scripts - .scripts - .iter() - .map(|s| (Entity::from_raw(0), vec![s.clone()])), - ) - .collect::>() - }); - - let entity_and_static_scripts = match entity_and_static_scripts { - Ok(entity_and_static_scripts) => entity_and_static_scripts, - Err(e) => { - bevy::log::error!( - "Failed to query entities with scripts: {}", - e.display_with_world(guard.clone()) + for event in events.into_iter().filter(|e| { + e.label == callback_label && e.language.as_ref().is_none_or(|l| l == &P::LANGUAGE) + }) { + let recipients = event + .recipients + .get_recipients(&handler_ctxt.script_context); + + for (attachment, ctxt) in recipients { + let call_result = handler_ctxt.call_dynamic_label( + &callback_label, + &attachment, + Some(ctxt), + event.args.clone(), + guard.clone(), ); - return handler_ctxt; - } - }; - for event in events.into_iter().filter(|e| e.label == callback_label) { - for (entity, entity_scripts) in &entity_and_static_scripts { - for script_id in entity_scripts.iter() { - match &event.recipients { - crate::event::Recipients::Script(target_script_id) - if target_script_id != script_id => - { - continue - } - crate::event::Recipients::Entity(target_entity) if target_entity != entity => { - continue - } - crate::event::Recipients::Language(target_language) - if *target_language != P::LANGUAGE => - { - continue - } - _ => {} - } - - let call_result = handler_ctxt.call_dynamic_label( - &callback_label, - script_id, - *entity, - event.args.clone(), + if event.trigger_response { + send_callback_response( guard.clone(), + ScriptCallbackResponseEvent::new( + callback_label.clone(), + attachment, + call_result.clone(), + P::LANGUAGE, + ), ); - - if event.trigger_response { - send_callback_response( - guard.clone(), - ScriptCallbackResponseEvent::new( - *entity, - callback_label.clone(), - script_id.clone(), - call_result.clone(), - ), - ); - } - - match call_result { - Ok(_) => {} - Err(e) => { - match e.downcast_interop_inner() { - Some(InteropErrorInner::MissingScript { script_id }) => { - trace_once!( - "{}: Script `{}` on entity `{:?}` is either still loading, doesn't exist, or is for another language, ignoring until the corresponding script is loaded.", - P::LANGUAGE, - script_id, entity - ); - continue; - } - Some(InteropErrorInner::MissingContext { .. }) => { - // if we don't have a context for the script, it's either: - // 1. a script for a different language, in which case we ignore it - // 2. something went wrong. This should not happen though and it's best we ignore this - continue; - } - _ => {} - } - let e = e - .with_script(script_id.clone()) - .with_context(format!("Event handling for: Language: {}", P::LANGUAGE)); - push_err_and_continue!(errors, Err(e)); - } - }; } + collect_errors(call_result, P::LANGUAGE, &mut errors); } } - handle_script_errors(guard, errors.into_iter()); return handler_ctxt; } +fn collect_errors( + call_result: Result, + language: Language, + errors: &mut Vec, +) { + match call_result { + Ok(_) => {} + Err(e) => { + errors.push(e.with_context(format!("Event handling for language {language}"))); + } + } +} + /// Sends a callback response event to the world pub fn send_callback_response(world: WorldGuard, response: ScriptCallbackResponseEvent) { let err = world.with_resource_mut(|mut events: Mut>| { @@ -299,311 +223,3 @@ pub fn handle_script_errors + Clone>(world: Worl bevy::log::error!("{}", error.display_with_world(world.clone())); } } - -#[cfg(test)] -#[allow(clippy::todo)] -mod test { - use std::{borrow::Cow, collections::HashMap, sync::Arc}; - - use bevy::app::{App, Update}; - use parking_lot::Mutex; - use test_utils::make_test_plugin; - - use crate::{ - bindings::script_value::ScriptValue, - context::{ContextBuilder, ContextLoadingSettings}, - event::{CallbackLabel, IntoCallbackLabel, ScriptCallbackEvent, ScriptErrorEvent}, - runtime::RuntimeContainer, - script::{Script, ScriptComponent, ScriptId, Scripts, StaticScripts}, - }; - - use super::*; - struct OnTestCallback; - - impl IntoCallbackLabel for OnTestCallback { - fn into_callback_label() -> CallbackLabel { - "OnTest".into() - } - } - - make_test_plugin!(crate); - - fn assert_response_events( - app: &mut World, - expected: impl Iterator, - ) { - let mut events = app - .get_resource_mut::>() - .unwrap(); - let responses = events.drain().collect::>(); - let expected: Vec<_> = expected.collect(); - assert_eq!( - responses.len(), - expected.len(), - "Incorrect amount of events received" - ); - for (a, b) in responses.iter().zip(expected.iter()) { - assert_eq!(a.label, b.label); - assert_eq!(a.script, b.script); - assert_eq!(a.response, b.response); - } - } - - fn setup_app( - runtime: TestRuntime, - scripts: HashMap>, - ) -> App { - let mut app = App::new(); - - app.add_event::(); - app.add_event::(); - app.add_event::(); - app.insert_resource::>(CallbackSettings { - callback_handler: |args, entity, script, _, ctxt, _, runtime| { - ctxt.invocations.extend(args); - let mut runtime = runtime.invocations.lock(); - runtime.push((entity, script.clone())); - Ok(ScriptValue::Unit) - }, - }); - app.add_systems(Update, event_handler::); - app.insert_resource::>(Scripts { scripts }); - app.insert_resource(RuntimeContainer:: { runtime }); - app.init_resource::(); - app.insert_resource(ContextLoadingSettings:: { - loader: ContextBuilder { - load: |_, _, _, _, _| todo!(), - reload: |_, _, _, _, _, _| todo!(), - }, - assignment_strategy: Default::default(), - context_initializers: vec![], - context_pre_handling_initializers: vec![], - }); - app.finish(); - app.cleanup(); - app - } - - #[test] - fn test_handler_emits_response_events() { - let test_script_id = Cow::Borrowed("test_script"); - let test_script = Script { - id: test_script_id.clone(), - asset: None, - context: Arc::new(Mutex::new(TestContext::default())), - }; - let scripts = HashMap::from_iter(vec![(test_script_id.clone(), test_script.clone())]); - let runtime = TestRuntime { - invocations: vec![].into(), - }; - let mut app = setup_app::(runtime, scripts); - let entity = app - .world_mut() - .spawn(ScriptComponent(vec![test_script_id.clone()])) - .id(); - - app.world_mut().send_event( - ScriptCallbackEvent::new( - OnTestCallback::into_callback_label(), - vec![ScriptValue::String("test_args".into())], - crate::event::Recipients::All, - ) - .with_response(), - ); - app.update(); - - assert_response_events( - app.world_mut(), - vec![ScriptCallbackResponseEvent::new( - entity, - OnTestCallback::into_callback_label(), - test_script_id.clone(), - Ok(ScriptValue::Unit), - )] - .into_iter(), - ); - } - - #[test] - fn test_handler_called_with_right_args() { - let test_script_id = Cow::Borrowed("test_script"); - let test_script = Script { - id: test_script_id.clone(), - asset: None, - context: Arc::new(Mutex::new(TestContext::default())), - }; - let scripts = HashMap::from_iter(vec![(test_script_id.clone(), test_script.clone())]); - let runtime = TestRuntime { - invocations: vec![].into(), - }; - let mut app = setup_app::(runtime, scripts); - let test_entity_id = app - .world_mut() - .spawn(ScriptComponent(vec![test_script_id.clone()])) - .id(); - - app.world_mut().send_event(ScriptCallbackEvent::new_for_all( - OnTestCallback::into_callback_label(), - vec![ScriptValue::String("test_args".into())], - )); - app.update(); - { - let test_script = app - .world() - .get_resource::>() - .unwrap() - .scripts - .get(&test_script_id) - .unwrap(); - - let test_context = test_script.context.lock(); - - let test_runtime = app - .world() - .get_resource::>() - .unwrap(); - - assert_eq!( - test_context.invocations, - vec![ScriptValue::String("test_args".into())] - ); - - let runtime_invocations = test_runtime.runtime.invocations.lock(); - assert_eq!( - runtime_invocations - .iter() - .map(|(e, s)| (*e, s.clone())) - .collect::>(), - vec![(test_entity_id, test_script_id.clone())] - ); - } - assert_response_events(app.world_mut(), vec![].into_iter()); - } - - #[test] - fn test_handler_called_on_right_recipients() { - let test_script_id = Cow::Borrowed("test_script"); - let test_script = Script { - id: test_script_id.clone(), - asset: None, - context: Arc::new(Mutex::new(TestContext::default())), - }; - let scripts = HashMap::from_iter(vec![ - (test_script_id.clone(), test_script.clone()), - ( - "wrong".into(), - Script { - id: "wrong".into(), - asset: None, - context: Arc::new(Mutex::new(TestContext::default())), - }, - ), - ]); - - let runtime = TestRuntime { - invocations: vec![].into(), - }; - let mut app = setup_app::(runtime, scripts); - let test_entity_id = app - .world_mut() - .spawn(ScriptComponent(vec![test_script_id.clone()])) - .id(); - - app.world_mut().send_event(ScriptCallbackEvent::new( - OnTestCallback::into_callback_label(), - vec![ScriptValue::String("test_args_script".into())], - crate::event::Recipients::Script(test_script_id.clone()), - )); - - app.world_mut().send_event(ScriptCallbackEvent::new( - OnTestCallback::into_callback_label(), - vec![ScriptValue::String("test_args_entity".into())], - crate::event::Recipients::Entity(test_entity_id), - )); - - app.update(); - { - let test_scripts = app.world().get_resource::>().unwrap(); - let test_runtime = app - .world() - .get_resource::>() - .unwrap(); - let test_runtime = test_runtime.runtime.invocations.lock(); - let script_after = test_scripts.scripts.get(&test_script_id).unwrap(); - let context_after = script_after.context.lock(); - assert_eq!( - context_after.invocations, - vec![ - ScriptValue::String("test_args_script".into()), - ScriptValue::String("test_args_entity".into()) - ] - ); - - assert_eq!( - test_runtime - .iter() - .map(|(e, s)| (*e, s.clone())) - .collect::>(), - vec![ - (test_entity_id, test_script_id.clone()), - (test_entity_id, test_script_id.clone()) - ] - ); - } - assert_response_events(app.world_mut(), vec![].into_iter()); - } - - #[test] - fn test_handler_called_for_static_scripts() { - let test_script_id = Cow::Borrowed("test_script"); - - let scripts = HashMap::from_iter(vec![( - test_script_id.clone(), - Script { - id: test_script_id.clone(), - asset: None, - context: Arc::new(Mutex::new(TestContext::default())), - }, - )]); - let runtime = TestRuntime { - invocations: vec![].into(), - }; - let mut app = setup_app::(runtime, scripts); - - app.world_mut().insert_resource(StaticScripts { - scripts: vec![test_script_id.clone()].into_iter().collect(), - }); - - app.world_mut().send_event(ScriptCallbackEvent::new( - OnTestCallback::into_callback_label(), - vec![ScriptValue::String("test_args_script".into())], - crate::event::Recipients::All, - )); - - app.world_mut().send_event(ScriptCallbackEvent::new( - OnTestCallback::into_callback_label(), - vec![ScriptValue::String("test_script_id".into())], - crate::event::Recipients::Script(test_script_id.clone()), - )); - - app.update(); - { - let test_scripts = app.world().get_resource::>().unwrap(); - let test_context = test_scripts - .scripts - .get(&test_script_id) - .unwrap() - .context - .lock(); - - assert_eq!( - test_context.invocations, - vec![ - ScriptValue::String("test_args_script".into()), - ScriptValue::String("test_script_id".into()) - ] - ); - } - assert_response_events(app.world_mut(), vec![].into_iter()); - } -} diff --git a/crates/bevy_mod_scripting_core/src/lib.rs b/crates/bevy_mod_scripting_core/src/lib.rs index 15faf31ff4..ff207bec88 100644 --- a/crates/bevy_mod_scripting_core/src/lib.rs +++ b/crates/bevy_mod_scripting_core/src/lib.rs @@ -2,12 +2,12 @@ //! //! Contains language agnostic systems and types for handling scripting in bevy. -use crate::event::ScriptErrorEvent; +use crate::{bindings::MarkAsCore, event::ScriptErrorEvent}; use asset::{ configure_asset_systems, configure_asset_systems_for_plugin, Language, ScriptAsset, - ScriptAssetLoader, ScriptAssetSettings, + ScriptAssetLoader, }; -use bevy::prelude::*; +use bevy::{platform::collections::HashMap, prelude::*}; use bindings::{ function::script_function::AppScriptFunctionRegistry, garbage_collector, schedule::AppScheduleRegistry, script_value::ScriptValue, AppReflectAllocator, @@ -15,14 +15,14 @@ use bindings::{ }; use commands::{AddStaticScript, RemoveStaticScript}; use context::{ - Context, ContextAssignmentStrategy, ContextBuilder, ContextInitializer, ContextLoadingSettings, + Context, ContextBuilder, ContextInitializer, ContextLoadingSettings, ContextPreHandlingInitializer, }; use error::ScriptError; -use event::{ScriptCallbackEvent, ScriptCallbackResponseEvent}; +use event::{ScriptCallbackEvent, ScriptCallbackResponseEvent, ScriptEvent}; use handler::{CallbackSettings, HandlerFn}; use runtime::{initialize_runtime, Runtime, RuntimeContainer, RuntimeInitializer, RuntimeSettings}; -use script::{ScriptComponent, ScriptId, Scripts, StaticScripts}; +use script::{ContextPolicy, ScriptComponent, ScriptContext, StaticScripts}; pub mod asset; pub mod bindings; @@ -46,12 +46,10 @@ pub enum ScriptingSystemSet { ScriptAssetDispatch, /// Systems which read incoming internal script asset events and produce script lifecycle commands ScriptCommandDispatch, - /// Systems which read incoming script asset events and remove metadata for removed assets - ScriptMetadataRemoval, - + // /// Systems which read entity removal events and remove contexts associated with them + // EntityRemoval, /// One time runtime initialization systems RuntimeInitialization, - /// Systems which handle the garbage collection of allocated values GarbageCollection, } @@ -83,19 +81,39 @@ pub struct ScriptingPlugin { /// The context builder for loading contexts pub context_builder: ContextBuilder

, - /// The strategy for assigning contexts to scripts - pub context_assignment_strategy: ContextAssignmentStrategy, + /// The strategy used to assign contexts to scripts + pub context_policy: ContextPolicy, /// The language this plugin declares pub language: Language, - /// Supported extensions to be added to the asset settings without the dot - /// By default BMS populates a set of extensions for the languages it supports. - pub additional_supported_extensions: &'static [&'static str], /// initializers for the contexts, run when loading the script pub context_initializers: Vec>, + /// initializers for the contexts run every time before handling events pub context_pre_handling_initializers: Vec>, + + /// Whether to emit responses from core script callbacks like `on_script_loaded` or `on_script_unloaded`. + pub emit_responses: bool, +} + +impl

std::fmt::Debug for ScriptingPlugin

+where + P: IntoScriptPluginParams, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ScriptingPlugin") + .field("callback_handler", &self.callback_handler) + .field("context_policy", &self.context_policy) + .field("language", &self.language) + .field("context_initializers", &self.context_initializers) + .field( + "context_pre_handling_initializers", + &self.context_pre_handling_initializers, + ) + .field("emit_responses", &self.emit_responses) + .finish() + } } impl Default for ScriptingPlugin

{ @@ -104,11 +122,11 @@ impl Default for ScriptingPlugin

{ runtime_settings: Default::default(), callback_handler: CallbackSettings::

::default().callback_handler, context_builder: Default::default(), - context_assignment_strategy: Default::default(), + context_policy: ContextPolicy::default(), language: Default::default(), context_initializers: Default::default(), context_pre_handling_initializers: Default::default(), - additional_supported_extensions: Default::default(), + emit_responses: false, } } } @@ -125,20 +143,14 @@ impl Plugin for ScriptingPlugin

{ }) .insert_resource::>(ContextLoadingSettings { loader: self.context_builder.clone(), - assignment_strategy: self.context_assignment_strategy, context_initializers: self.context_initializers.clone(), context_pre_handling_initializers: self.context_pre_handling_initializers.clone(), - }) - .init_resource::>(); + emit_responses: self.emit_responses, + }); - register_script_plugin_systems::

(app); + app.insert_resource(ScriptContext::

::new(self.context_policy.clone())); - if !self.additional_supported_extensions.is_empty() { - app.add_supported_script_extensions( - self.additional_supported_extensions, - self.language.clone(), - ); - } + register_script_plugin_systems::

(app); register_types(app); } @@ -190,16 +202,18 @@ pub trait ConfigureScriptPlugin { /// Add a runtime initializer to the plugin fn add_runtime_initializer(self, initializer: RuntimeInitializer) -> Self; - /// Switch the context assigning strategy to a global context assigner. + /// Switch the context assigning strategy to the given policy. /// - /// This means that all scripts will share the same context. This is useful for when you want to share data between scripts easilly. - /// Be careful however as this also means that scripts can interfere with each other in unexpected ways! Including overwriting each other's handlers. - fn enable_context_sharing(self) -> Self; + /// Some context policies might work in unexpected ways. + /// For example, a single shared context might cause issues with scripts overriding each other's handlers. + fn set_context_policy(self, context_policy: ContextPolicy) -> Self; - /// Set the set of extensions to be added for the plugin's language. + /// Whether to emit responses from core script callbacks like `on_script_loaded` or `on_script_unloaded`. + /// By default, this is `false` and responses are not emitted. /// - /// This is useful for adding extensions that are not supported by default by BMS. - fn set_additional_supported_extensions(self, extensions: &'static [&'static str]) -> Self; + /// You won't be able to react to these events until after contexts are fully loaded, + /// but they might be useful for other purposes, such as debugging or logging. + fn emit_core_callback_responses(self, emit_responses: bool) -> Self; } impl>> ConfigureScriptPlugin for P { @@ -224,13 +238,13 @@ impl>> ConfigureScriptPlugi self } - fn enable_context_sharing(mut self) -> Self { - self.as_mut().context_assignment_strategy = ContextAssignmentStrategy::Global; + fn set_context_policy(mut self, policy: ContextPolicy) -> Self { + self.as_mut().context_policy = policy; self } - fn set_additional_supported_extensions(mut self, extensions: &'static [&'static str]) -> Self { - self.as_mut().additional_supported_extensions = extensions; + fn emit_core_callback_responses(mut self, emit_responses: bool) -> Self { + self.as_mut().emit_responses = emit_responses; self } } @@ -257,6 +271,7 @@ pub struct BMSScriptingInfrastructurePlugin; impl Plugin for BMSScriptingInfrastructurePlugin { fn build(&self, app: &mut App) { app.add_event::() + .add_event::() .add_event::() .add_event::() .init_resource::() @@ -265,35 +280,28 @@ impl Plugin for BMSScriptingInfrastructurePlugin { .init_resource::() .insert_resource(AppScheduleRegistry::new()); + app.register_type::(); + app.register_type::>(); + app.register_type_data::, MarkAsCore>(); + app.add_systems( PostUpdate, ((garbage_collector).in_set(ScriptingSystemSet::GarbageCollection),), ); - configure_asset_systems(app); + app.add_plugins(configure_asset_systems); DynamicScriptComponentPlugin.build(app); } fn finish(&self, app: &mut App) { - // read extensions from asset settings - let asset_settings_extensions = app + // Read extensions. + let language_extensions = app .world_mut() - .get_resource_or_init::() - .supported_extensions; - - // convert extensions to static array - bevy::log::info!( - "Initializing BMS with Supported extensions: {:?}", - asset_settings_extensions - ); - - app.register_asset_loader(ScriptAssetLoader { - extensions: asset_settings_extensions, - preprocessor: None, - }); - - // pre-register component id's + .remove_resource::() + .unwrap_or_default(); + app.register_asset_loader(ScriptAssetLoader::new(language_extensions)); + // Pre-register component IDs. pre_register_components(app); DynamicScriptComponentPlugin.finish(app); } @@ -311,7 +319,7 @@ fn register_script_plugin_systems(app: &mut App) { .in_set(ScriptingSystemSet::RuntimeInitialization), ); - configure_asset_systems_for_plugin::

(app); + app.add_plugins(configure_asset_systems_for_plugin::

); } /// Register all types that need to be accessed via reflection @@ -353,21 +361,21 @@ pub trait ManageStaticScripts { /// Registers a script id as a static script. /// /// Event handlers will run these scripts on top of the entity scripts. - fn add_static_script(&mut self, script_id: impl Into) -> &mut Self; + fn add_static_script(&mut self, script_id: impl Into>) -> &mut Self; /// Removes a script id from the list of static scripts. /// /// Does nothing if the script id is not in the list. - fn remove_static_script(&mut self, script_id: impl Into) -> &mut Self; + fn remove_static_script(&mut self, script_id: impl Into>) -> &mut Self; } impl ManageStaticScripts for App { - fn add_static_script(&mut self, script_id: impl Into) -> &mut Self { + fn add_static_script(&mut self, script_id: impl Into>) -> &mut Self { AddStaticScript::new(script_id.into()).apply(self.world_mut()); self } - fn remove_static_script(&mut self, script_id: impl Into) -> &mut Self { + fn remove_static_script(&mut self, script_id: impl Into>) -> &mut Self { RemoveStaticScript::new(script_id.into()).apply(self.world_mut()); self } @@ -388,29 +396,39 @@ pub trait ConfigureScriptAssetSettings { ) -> &mut Self; } +/// Collect the language extensions supported during initialization. +/// +/// NOTE: This resource is removed after plugin setup. +#[derive(Debug, Resource, Deref, DerefMut)] +pub struct LanguageExtensions(HashMap<&'static str, Language>); + +impl Default for LanguageExtensions { + fn default() -> Self { + LanguageExtensions( + [ + ("lua", Language::Lua), + ("rhai", Language::Rhai), + ("rn", Language::Rune), + ] + .into_iter() + .collect(), + ) + } +} + impl ConfigureScriptAssetSettings for App { fn add_supported_script_extensions( &mut self, extensions: &[&'static str], language: Language, ) -> &mut Self { - let mut asset_settings = self + let mut language_extensions = self .world_mut() - .get_resource_or_init::(); - - let mut new_arr = Vec::from(asset_settings.supported_extensions); - - new_arr.extend(extensions); + .get_resource_or_init::(); - let new_arr_static = Vec::leak(new_arr); - - asset_settings.supported_extensions = new_arr_static; for extension in extensions { - asset_settings - .extension_to_language_map - .insert(*extension, language.clone()); + language_extensions.insert(extension, language.clone()); } - self } } @@ -422,13 +440,8 @@ mod test { #[tokio::test] async fn test_asset_extensions_correctly_accumulate() { let mut app = App::new(); - app.init_resource::(); app.add_plugins(AssetPlugin::default()); - app.world_mut() - .resource_mut::() - .supported_extensions = &["lua", "rhai"]; - BMSScriptingInfrastructurePlugin.finish(&mut app); let asset_loader = app diff --git a/crates/bevy_mod_scripting_core/src/script.rs b/crates/bevy_mod_scripting_core/src/script.rs deleted file mode 100644 index dd29fdc22a..0000000000 --- a/crates/bevy_mod_scripting_core/src/script.rs +++ /dev/null @@ -1,177 +0,0 @@ -//! Script related types, functions and components - -use std::{borrow::Cow, collections::HashMap, ops::Deref, sync::Arc}; - -use bevy::{ - asset::Handle, - platform::collections::HashSet, - prelude::{ReflectComponent, Resource}, - reflect::Reflect, -}; -use parking_lot::Mutex; - -use crate::{asset::ScriptAsset, IntoScriptPluginParams}; - -/// A unique identifier for a script, by default corresponds to the path of the asset excluding the asset source. -/// -/// I.e. an asset with the path `path/to/asset.ext` will have the script id `path/to/asset.ext` -pub type ScriptId = Cow<'static, str>; - -#[derive(bevy::ecs::component::Component, Reflect, Clone)] -#[reflect(Component)] -/// A component which identifies the scripts existing on an entity. -/// -/// Event handlers search for components with this component to figure out which scripts to run and on which entities. -pub struct ScriptComponent(pub Vec); - -impl Deref for ScriptComponent { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl ScriptComponent { - /// Creates a new [`ScriptComponent`] with the given ScriptID's - pub fn new, I: IntoIterator>(components: I) -> Self { - Self(components.into_iter().map(Into::into).collect()) - } -} - -/// All the scripts which are currently loaded or loading and their mapping to contexts -#[derive(Resource)] -pub struct Scripts { - pub(crate) scripts: HashMap>, -} - -#[profiling::all_functions] -impl Scripts

{ - /// Inserts a script into the collection - pub fn insert(&mut self, script: Script

) { - self.scripts.insert(script.id.clone(), script); - } - - /// Removes a script from the collection, returning `true` if the script was in the collection, `false` otherwise - pub fn remove>(&mut self, script: S) -> bool { - self.scripts.remove(&script.into()).is_some() - } - - /// Checks if a script is in the collection - /// Returns `true` if the script is in the collection, `false` otherwise - pub fn contains>(&self, script: S) -> bool { - self.scripts.contains_key(&script.into()) - } - - /// Returns a reference to the script with the given id - pub fn get>(&self, script: S) -> Option<&Script

> { - self.scripts.get(&script.into()) - } - - /// Returns a mutable reference to the script with the given id - pub fn get_mut>(&mut self, script: S) -> Option<&mut Script

> { - self.scripts.get_mut(&script.into()) - } - - /// Returns an iterator over the scripts - pub fn iter(&self) -> impl Iterator> { - self.scripts.values() - } - - /// Returns a mutable iterator over the scripts - pub fn iter_mut(&mut self) -> impl Iterator> { - self.scripts.values_mut() - } -} - -impl Default for Scripts

{ - fn default() -> Self { - Self { - scripts: Default::default(), - } - } -} - -/// A script -pub struct Script { - /// The id of the script - pub id: ScriptId, - /// the asset holding the content of the script if it comes from an asset - pub asset: Option>, - /// The context of the script, possibly shared with other scripts - pub context: Arc>, -} - -impl Clone for Script

{ - fn clone(&self) -> Self { - Self { - id: self.id.clone(), - asset: self.asset.clone(), - context: self.context.clone(), - } - } -} - -/// A collection of scripts, not associated with any entity. -/// -/// Useful for `global` or `static` scripts which operate over a larger scope than a single entity. -#[derive(Default, Resource)] -pub struct StaticScripts { - pub(crate) scripts: HashSet, -} - -#[profiling::all_functions] -impl StaticScripts { - /// Inserts a static script into the collection - pub fn insert>(&mut self, script: S) { - self.scripts.insert(script.into()); - } - - /// Removes a static script from the collection, returning `true` if the script was in the collection, `false` otherwise - pub fn remove>(&mut self, script: S) -> bool { - self.scripts.remove(&script.into()) - } - - /// Checks if a static script is in the collection - /// Returns `true` if the script is in the collection, `false` otherwise - pub fn contains>(&self, script: S) -> bool { - self.scripts.contains(&script.into()) - } - - /// Returns an iterator over the static scripts - pub fn iter(&self) -> impl Iterator { - self.scripts.iter() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn static_scripts_insert() { - let mut static_scripts = StaticScripts::default(); - static_scripts.insert("script1"); - assert_eq!(static_scripts.scripts.len(), 1); - assert!(static_scripts.scripts.contains("script1")); - } - - #[test] - fn static_scripts_remove() { - let mut static_scripts = StaticScripts::default(); - static_scripts.insert("script1"); - assert_eq!(static_scripts.scripts.len(), 1); - assert!(static_scripts.scripts.contains("script1")); - assert!(static_scripts.remove("script1")); - assert_eq!(static_scripts.scripts.len(), 0); - assert!(!static_scripts.scripts.contains("script1")); - } - - #[test] - fn static_scripts_contains() { - let mut static_scripts = StaticScripts::default(); - static_scripts.insert("script1"); - assert!(static_scripts.contains("script1")); - assert!(!static_scripts.contains("script2")); - } -} diff --git a/crates/bevy_mod_scripting_core/src/script/context_key.rs b/crates/bevy_mod_scripting_core/src/script/context_key.rs new file mode 100644 index 0000000000..688ed7e900 --- /dev/null +++ b/crates/bevy_mod_scripting_core/src/script/context_key.rs @@ -0,0 +1,147 @@ +use super::*; +use crate::ScriptAsset; +use bevy::prelude::Entity; +use std::fmt; + +/// Specifies a unique attachment of a script. These attachments are mapped to [`ContextKey`]'s depending on the context policy used. +#[derive(Debug, Hash, Clone, PartialEq, Eq, Reflect)] +pub enum ScriptAttachment { + /// a script attached to an entity, with an optional domain. By default selecting a domain will put the context of this script on a per-domain basis. + EntityScript(Entity, Handle), + /// a static script, with an optional domain. By default selecting a domain will put the context of this script on a per-domain basis. + StaticScript(Handle), +} + +impl std::fmt::Display for ScriptAttachment { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ScriptAttachment::EntityScript(entity, script) => { + write!( + f, + "EntityScript(entity: {}, script: {})", + entity, + script.display(), + ) + } + ScriptAttachment::StaticScript(script) => { + write!(f, "StaticScript(script: {})", script.display()) + } + } + } +} + +impl ScriptAttachment { + /// Returns the script handle if it exists. + pub fn script(&self) -> Handle { + match self { + ScriptAttachment::EntityScript(_, script) => script.clone(), + ScriptAttachment::StaticScript(script) => script.clone(), + } + } + + /// Returns the entity if it exists. + pub fn entity(&self) -> Option { + match self { + ScriptAttachment::EntityScript(entity, _) => Some(*entity), + ScriptAttachment::StaticScript(_) => None, + } + } + + /// Downcasts any script handles into weak handles. + pub fn into_weak(self) -> Self { + match self { + ScriptAttachment::EntityScript(entity, script) => { + ScriptAttachment::EntityScript(entity, script.clone_weak()) + } + ScriptAttachment::StaticScript(script) => { + ScriptAttachment::StaticScript(script.clone_weak()) + } + } + } +} + +impl From for ContextKey { + fn from(val: ScriptAttachment) -> Self { + match val { + ScriptAttachment::EntityScript(entity, script) => ContextKey { + entity: Some(entity), + script: Some(script), + }, + ScriptAttachment::StaticScript(script) => ContextKey { + entity: None, + script: Some(script), + }, + } + } +} + +/// The key for a context. The context key is used for: +/// - Identifying the script itself, uniquely. +/// - later on it's mapped to a context, which will determine how the scripts are grouped in execution environments. +#[derive(Debug, Hash, Clone, Default, PartialEq, Eq, Reflect)] +pub struct ContextKey { + /// Entity if there is one. + pub entity: Option, + /// Script ID if there is one. + /// Can be empty if the script is not driven by an asset. + pub script: Option>, +} + +impl fmt::Display for ContextKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // write!(f, "context ")?; + let mut empty = true; + if let Some(script_id) = &self.script { + write!(f, "script {}", script_id.display())?; + empty = false; + } + if let Some(id) = self.entity { + write!(f, "entity {id}")?; + empty = false; + } + if empty { + write!(f, "empty")?; + } + Ok(()) + } +} + +impl ContextKey { + /// Creates an invalid context key, which should never exist. + pub const INVALID: Self = Self { + entity: Some(Entity::from_raw(0)), + script: Some(Handle::Weak(AssetId::invalid())), + }; + + /// Creates a shared context key, which is used for shared contexts + pub const SHARED: Self = { + Self { + entity: None, + script: None, + } + }; + + /// Is the key empty? + pub fn is_empty(&self) -> bool { + self == &Self::default() + } + + /// or with other context + pub fn or(self, other: ContextKey) -> Self { + Self { + entity: self.entity.or(other.entity), + script: self.script.or(other.script), + } + } + + /// If a script handle is present and is strong, convert it to a weak + /// handle. + pub fn into_weak(mut self) -> Self { + if let Some(script) = &self.script { + if script.is_strong() { + self.script = Some(script.clone_weak()); + } + } + self + } +} diff --git a/crates/bevy_mod_scripting_core/src/script/mod.rs b/crates/bevy_mod_scripting_core/src/script/mod.rs new file mode 100644 index 0000000000..02c8cb39a6 --- /dev/null +++ b/crates/bevy_mod_scripting_core/src/script/mod.rs @@ -0,0 +1,224 @@ +//! Script related types, functions and components + +use crate::asset::ScriptAsset; +use crate::event::ScriptEvent; +use bevy::ecs::component::HookContext; +use bevy::ecs::entity::Entity; +use bevy::ecs::resource::Resource; +use bevy::ecs::world::DeferredWorld; +use bevy::platform::collections::HashSet; +use bevy::prelude::ReflectComponent; +use bevy::{ + asset::{Asset, AssetId, Handle}, + reflect::Reflect, +}; +use std::{collections::HashMap, fmt, ops::Deref}; + +mod context_key; +mod script_context; +pub use context_key::*; +pub use script_context::*; + +/// A unique identifier for a script, by default corresponds to the path of the asset excluding the asset source. +/// +/// I.e. an asset with the path `path/to/asset.ext` will have the script id `path/to/asset.ext` +pub type ScriptId = AssetId; + +/// Display the path of a script or its asset ID. +#[doc(hidden)] +pub struct HandleDisplay<'a, T: Asset>(&'a Handle); + +impl<'a, A: Asset> fmt::Display for HandleDisplay<'a, A> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(path) = self.0.path() { + write!(f, "path {path}") + } else { + write!(f, "id {}", self.0.id()) + } + } +} + +impl<'a, A: Asset> fmt::Debug for HandleDisplay<'a, A> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(path) = self.0.path() { + write!(f, "path {path:?}") + } else { + write!(f, "id {:?}", self.0.id()) + } + } +} + +/// Make a type display-able. +pub trait DisplayProxy { + /// The type that does the displaying. + type D<'a>: fmt::Display + fmt::Debug + where + Self: 'a; + /// Return a display-able reference. + fn display<'a>(&'a self) -> Self::D<'a>; +} + +impl DisplayProxy for Handle { + type D<'a> = HandleDisplay<'a, A>; + + fn display<'a>(&'a self) -> Self::D<'a> { + HandleDisplay(self) + } +} + +#[derive(bevy::ecs::component::Component, Reflect, Clone, Default, Debug)] +#[reflect(Component)] +#[component(on_remove=Self::on_remove, on_add=Self::on_add)] +/// A component which identifies the scripts existing on an entity. +/// +/// Event handlers search for components with this component to figure out which scripts to run and on which entities. +pub struct ScriptComponent(pub Vec>); + +impl Deref for ScriptComponent { + type Target = Vec>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl ScriptComponent { + /// Creates a new [`ScriptComponent`] with the given ScriptID's + pub fn new>, I: IntoIterator>(components: I) -> Self { + Self(components.into_iter().map(Into::into).collect()) + } + + fn get_context_keys_present(world: &DeferredWorld, entity: Entity) -> Vec { + let entity_ref = world.entity(entity); + let script_component = entity_ref.components::<&ScriptComponent>(); + let mut context_keys = Vec::new(); + for script in script_component.iter() { + context_keys.push(ScriptAttachment::EntityScript(entity, script.clone())); + } + context_keys + } + + /// the lifecycle hook called when a script component is removed from an entity, emits an appropriate event so we can handle + /// the removal of the script. + pub fn on_remove(mut world: DeferredWorld, context: HookContext) { + let context_keys = Self::get_context_keys_present(&world, context.entity); + world.send_event_batch( + context_keys + .into_iter() + .map(|key| ScriptEvent::Detached { key }), + ); + } + + /// the lifecycle hook called when a script component is added to an entity, emits an appropriate event so we can handle + /// the addition of the script. + pub fn on_add(mut world: DeferredWorld, context: HookContext) { + let context_keys = Self::get_context_keys_present(&world, context.entity); + world.send_event_batch( + context_keys + .into_iter() + .map(|key| ScriptEvent::Attached { key }), + ); + } +} + +/// A collection of scripts, not associated with any entity. +/// +/// Useful for `global` or `static` scripts which operate over a larger scope than a single entity. +#[derive(Default, Resource)] +pub struct StaticScripts { + pub(crate) scripts: HashSet>, +} + +#[profiling::all_functions] +impl StaticScripts { + /// Inserts a static script into the collection + pub fn insert>>(&mut self, script: S) { + self.scripts.insert(script.into()); + } + + /// Removes a static script from the collection, returning `true` if the script was in the collection, `false` otherwise + pub fn remove(&mut self, script_id: impl Into) -> bool { + let script_id = script_id.into(); + self.scripts + .extract_if(|handle| handle.id() == script_id) + .next() + .is_some() + } + + /// Checks if a static script is in the collection + /// Returns `true` if the script is in the collection, `false` otherwise + pub fn contains(&self, script_id: impl Into) -> bool { + let script_id = script_id.into(); + self.scripts.iter().any(|handle| handle.id() == script_id) + } + + /// Returns an iterator over the static scripts + pub fn values(&self) -> impl Iterator> { + self.scripts.iter() + } +} + +#[cfg(test)] +mod tests { + use bevy::ecs::{event::Events, world::World}; + + use super::*; + + #[test] + fn static_scripts_insert() { + let mut static_scripts = StaticScripts::default(); + let script1 = Handle::default(); + static_scripts.insert(script1.clone()); + assert_eq!(static_scripts.scripts.len(), 1); + assert!(static_scripts.scripts.contains(&script1)); + } + + #[test] + fn static_scripts_remove() { + let mut static_scripts = StaticScripts::default(); + let script1 = Handle::default(); + static_scripts.insert(script1.clone()); + assert_eq!(static_scripts.scripts.len(), 1); + assert!(static_scripts.scripts.contains(&script1)); + assert!(static_scripts.remove(&script1)); + assert_eq!(static_scripts.scripts.len(), 0); + assert!(!static_scripts.scripts.contains(&script1)); + } + + fn scriptid_from_u128(uuid: u128) -> ScriptId { + ScriptId::from(uuid::Builder::from_random_bytes(uuid.to_le_bytes()).into_uuid()) + } + + fn handle_from_u128(uuid: u128) -> Handle { + Handle::Weak(scriptid_from_u128(uuid)) + } + + #[test] + fn static_scripts_contains() { + let mut static_scripts = StaticScripts::default(); + let script1 = handle_from_u128(0); + let script2 = handle_from_u128(1); + static_scripts.insert(script1.clone()); + assert!(static_scripts.contains(&script1)); + assert!(!static_scripts.contains(&script2)); + } + + #[test] + fn test_component_add() { + let mut world = World::new(); + world.init_resource::>(); + // spawn new script component + let entity = world + .spawn(ScriptComponent::new([Handle::Weak(AssetId::invalid())])) + .id(); + + // check that the event was sent + let mut events = world.resource_mut::>(); + assert_eq!( + Some(ScriptEvent::Attached { + key: ScriptAttachment::EntityScript(entity, Handle::Weak(AssetId::invalid())) + }), + events.drain().next() + ); + } +} diff --git a/crates/bevy_mod_scripting_core/src/script/script_context.rs b/crates/bevy_mod_scripting_core/src/script/script_context.rs new file mode 100644 index 0000000000..ccaa578bf7 --- /dev/null +++ b/crates/bevy_mod_scripting_core/src/script/script_context.rs @@ -0,0 +1,392 @@ +use super::*; +use crate::IntoScriptPluginParams; +use parking_lot::Mutex; +use std::{hash::Hash, sync::Arc}; + +/// Determines how contexts are grouped by manipulating the context key. +pub trait ContextKeySelector: Send + Sync + std::fmt::Debug + 'static { + /// The given context key represents a possible script, entity that + /// is requesting a context. + /// + /// This selector returns + /// - `None` when the given `context_key` is not relevant to its policy, or + /// - `Some(selected_key)` when the appropriate key has been determined. + fn select(&self, context_key: &ScriptAttachment) -> Option; +} + +impl Option + Send + Sync + std::fmt::Debug + 'static> + ContextKeySelector for F +{ + fn select(&self, context_key: &ScriptAttachment) -> Option { + (self)(context_key) + } +} + +/// A rule for context selection. +/// +/// Maps a `ContextKey` to a `Option`. +/// +/// If the rule is not applicable, it returns `None`. +/// +/// If the rule is applicable, it returns an equivalent or "susbset" `ContextKey` that represents the +/// context assignment +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum ContextRule { + /// If entity-script pair exists, return only that. + EntityScript, + /// If entity exists, return only that. + Entity, + /// If script exists, return only that. + Script, + /// Check nothing; return empty context key. + Shared, +} + +impl ContextKeySelector for ContextRule { + /// Depending on the enum variant, executes that rule. + fn select(&self, context_key: &ScriptAttachment) -> Option { + // extract the components from the input, i.e. entity, script, fill with None if not present + let context_key: ContextKey = context_key.clone().into(); + + match self { + ContextRule::Entity => context_key.entity.map(|e| ContextKey { + entity: Some(e), + script: None, + }), + ContextRule::Script => context_key.script.map(|h| ContextKey { + entity: None, + script: Some(h), + }), + ContextRule::EntityScript => { + context_key + .entity + .zip(context_key.script.clone()) + .map(|(entity, script)| ContextKey { + entity: Some(entity), + script: Some(script), + }) + } + ContextRule::Shared => Some(ContextKey::default()), + } + } +} + +/// This is a configurable context policy based on priority. +#[derive(Debug)] +pub struct ContextPolicy { + /// The rules in order of priority. + pub priorities: Vec>, +} + +impl Clone for ContextPolicy { + fn clone(&self) -> Self { + Self { + priorities: self.priorities.to_vec(), + } + } +} + +/// Returns a default context policy. i.e. `[ContextPolicy::per_entity_and_script]`. +impl Default for ContextPolicy { + fn default() -> Self { + ContextPolicy::per_entity_and_script() + } +} + +impl ContextPolicy { + /// Return which rule is used for context_key. + pub fn which_rule(&self, context_key: &ScriptAttachment) -> Option<&dyn ContextKeySelector> { + self.priorities + .iter() + .find_map(|rule| rule.select(context_key).is_some().then_some(rule.as_ref())) + } + + /// Use a shared script context. + pub fn shared() -> Self { + ContextPolicy { + priorities: vec![Arc::new(ContextRule::Shared)], + } + } + + /// Use one script context per entity or a shared context. + /// + /// For example, given: + /// - `script_id: Some("script1")` + /// - `entity: Some(1)` + /// + /// + /// The context key will purely use the entity, resulting in a context key + /// of `ContextKey { entity: Some(1) }`. + /// + /// resulting in each entity having its own context regardless of the script id. + /// + /// static scripts will get their own context per script asset. + /// + /// The default is then to use a shared context for no matches + pub fn per_entity() -> Self { + ContextPolicy { + priorities: vec![ + Arc::new(ContextRule::Entity), + Arc::new(ContextRule::Script), + Arc::new(ContextRule::Shared), + ], + } + } + + /// Use one script context per script or a shared context. + /// + /// For example, given: + /// - `script_id: Some("script1")` + /// - `entity: Some(1)` + /// + /// The context key will purely use the script, resulting in a context key + /// of `ContextKey { script: Some("script1") }`. + /// + /// resulting in each script having its own context regardless of the entity. + /// + /// If no script is given it will be the default, i.e. shared context. + pub fn per_script() -> Self { + ContextPolicy { + priorities: vec![Arc::new(ContextRule::Script), Arc::new(ContextRule::Shared)], + } + } + + /// Use one script context per entity-script, or a script context, or a shared context. + /// + /// For example, given: + /// - `script_id: Some("script1")` + /// - `entity: Some(1)` + /// + /// The context key will use the entity-script pair, resulting in a context key + /// of `ContextKey { entity: Some(1), script: Some("script1") }`. + /// + /// resulting in each entity-script combination having its own context. + /// + /// If no entity-script pair is given it will be the default, i.e. shared context. + pub fn per_entity_and_script() -> Self { + ContextPolicy { + priorities: vec![ + Arc::new(ContextRule::EntityScript), + Arc::new(ContextRule::Script), + Arc::new(ContextRule::Shared), + ], + } + } +} + +impl ContextKeySelector for ContextPolicy { + fn select(&self, context_key: &ScriptAttachment) -> Option { + self.priorities + .iter() + .find_map(|priority| priority.select(context_key)) + } +} + +struct ContextEntry { + context: Arc>, + residents: HashSet, +} + +#[derive(Resource)] +/// Keeps track of script contexts and enforces the context selection policy. +pub struct ScriptContext { + /// script contexts and the counts of how many scripts are associated with them. + map: HashMap>, + /// The policy used to determine the context key. + pub policy: ContextPolicy, +} + +impl ScriptContext

{ + /// Construct a new ScriptContext with the given policy. + pub fn new(policy: ContextPolicy) -> Self { + Self { + map: HashMap::default(), + policy, + } + } + + fn get_entry(&self, context_key: &ScriptAttachment) -> Option<&ContextEntry

> { + self.policy + .select(context_key) + .and_then(|key| self.map.get(&key)) + } + + fn get_entry_mut(&mut self, context_key: &ScriptAttachment) -> Option<&mut ContextEntry

> { + self.policy + .select(context_key) + .and_then(|key| self.map.get_mut(&key)) + } + + /// Get the context. + pub fn get(&self, context_key: &ScriptAttachment) -> Option>> { + self.get_entry(context_key) + .map(|entry| entry.context.clone()) + } + + /// Insert a context. + /// + /// If the context cannot be inserted, it is returned as an `Err`. + /// + /// The attachment is also inserted as resident into the context. + pub fn insert(&mut self, context_key: &ScriptAttachment, context: P::C) -> Result<(), P::C> { + match self.policy.select(context_key) { + Some(key) => { + let entry = ContextEntry { + context: Arc::new(Mutex::new(context)), + residents: HashSet::from_iter([context_key.clone()]), // context with a residency of one + }; + self.map.insert(key.into_weak(), entry); + Ok(()) + } + None => Err(context), + } + } + + /// Mark a context as resident. + /// This needs to be called when a script is added to a context. + /// + /// Returns true if the context was inserted as a resident, false if it was already present. + /// Errors if no matching context is found for the given attachment. + pub fn insert_resident( + &mut self, + context_key: ScriptAttachment, + ) -> Result { + if let Some(entry) = self.get_entry_mut(&context_key) { + Ok(entry.residents.insert(context_key)) + } else { + Err(context_key) + } + } + + /// Remove a resident context. + /// This needs to be called when a script is deleted. + pub fn remove_resident(&mut self, context_key: &ScriptAttachment) { + if let Some(entry) = self.get_entry_mut(context_key) { + entry.residents.remove(context_key); + } + } + + /// Iterates through all context & corresponding script attachment pairs. + pub fn all_residents( + &self, + ) -> impl Iterator>)> + use<'_, P> { + self.map.values().flat_map(|entry| { + entry + .residents + .iter() + .map(move |resident| (resident.clone(), entry.context.clone())) + }) + } + + /// Retrieves the first resident from each context. + /// + /// For example if using a single global context, and with 2 scripts: + /// `script1` and `script2` + /// this will return: + /// `(&context_key, &script1)` + pub fn first_resident_from_each_context( + &self, + ) -> impl Iterator>)> + use<'_, P> { + self.map.values().filter_map(|entry| { + entry + .residents + .iter() + .next() + .map(|resident| (resident.clone(), entry.context.clone())) + }) + } + + /// Iterates over the residents living in the same script context as the one mapped to by the context policy input + pub fn residents( + &self, + context_key: &ScriptAttachment, + ) -> impl Iterator>)> + use<'_, P> { + self.get_entry(context_key).into_iter().flat_map(|entry| { + entry + .residents + .iter() + .map(move |resident| (resident.clone(), entry.context.clone())) + }) + } + + /// Returns the number of residents in the context shared by the given attachment. + pub fn residents_len(&self, context_key: &ScriptAttachment) -> usize { + self.get_entry(context_key) + .map_or(0, |entry| entry.residents.len()) + } + + /// Returns true if a context contains this given attachment + pub fn contains(&self, context_key: &ScriptAttachment) -> bool { + self.get_entry(context_key) + .is_some_and(|entry| entry.residents.contains(context_key)) + } + + /// Remove a context. + /// + /// Returns context if removed. + pub fn remove(&mut self, context_key: &ScriptAttachment) -> Option>> { + self.policy + .select(context_key) + .and_then(|key| self.map.remove(&key).map(|entry| entry.context)) + } +} + +/// Use one script context per entity and script by default; see +/// [ScriptContext::per_entity_and_script]. +impl Default for ScriptContext

{ + fn default() -> Self { + Self { + map: HashMap::default(), + policy: ContextPolicy::default(), + } + } +} + +#[cfg(test)] +mod tests { + use bevy::asset::AssetIndex; + use test_utils::make_test_plugin; + + use super::*; + + make_test_plugin!(crate); + + #[test] + fn test_insertion_per_script_policy() { + let policy = ContextPolicy::per_script(); + + let mut script_context = ScriptContext::::new(policy.clone()); + let context_key = ScriptAttachment::EntityScript( + Entity::from_raw(1), + Handle::Weak(AssetIndex::from_bits(1).into()), + ); + let context_key2 = ScriptAttachment::EntityScript( + Entity::from_raw(2), + Handle::Weak(AssetIndex::from_bits(1).into()), + ); + assert_eq!(policy.select(&context_key), policy.select(&context_key2)); + + script_context + .insert(&context_key, TestContext::default()) + .unwrap(); + + assert!(script_context.contains(&context_key)); + assert_eq!(script_context.residents_len(&context_key), 1); + let resident = script_context.residents(&context_key).next().unwrap(); + assert_eq!(resident.0, context_key); + assert!(script_context.get(&context_key).is_some()); + + // insert another into the same context + assert!(script_context + .insert_resident(context_key2.clone()) + .unwrap()); + + assert!(script_context.contains(&context_key2)); + let mut residents = script_context.residents(&context_key2).collect::>(); + residents.sort_by_key(|r| r.0.entity()); + assert_eq!(residents[0].0, context_key); + assert_eq!(residents[1].0, context_key2); + assert_eq!(residents.len(), 2); + assert_eq!(script_context.residents_len(&context_key2), 2); + } +} diff --git a/crates/bevy_mod_scripting_derive/CHANGELOG.md b/crates/bevy_mod_scripting_derive/CHANGELOG.md index b3c5037aa4..4556098aaa 100644 --- a/crates/bevy_mod_scripting_derive/CHANGELOG.md +++ b/crates/bevy_mod_scripting_derive/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.15.0](https://github.com/makspll/bevy_mod_scripting/compare/bevy_mod_scripting_derive-v0.14.0...bevy_mod_scripting_derive-v0.15.0) - 2025-08-14 + +### Added + +- Use the Handles, Luke! ([#427](https://github.com/makspll/bevy_mod_scripting/pull/427)) + +### Other + +- update versions to currently released ones + ## [0.12.0](https://github.com/makspll/bevy_mod_scripting/compare/bevy_mod_scripting_derive-v0.11.1...bevy_mod_scripting_derive-v0.12.0) - 2025-04-07 ### Fixed diff --git a/crates/bevy_mod_scripting_derive/Cargo.toml b/crates/bevy_mod_scripting_derive/Cargo.toml index 5d205a8837..01bed73310 100644 --- a/crates/bevy_mod_scripting_derive/Cargo.toml +++ b/crates/bevy_mod_scripting_derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_mod_scripting_derive" -version = "0.14.0" +version = "0.15.0" edition = "2021" authors = ["Maksymilian Mozolewski "] license = "MIT OR Apache-2.0" diff --git a/crates/bevy_mod_scripting_functions/CHANGELOG.md b/crates/bevy_mod_scripting_functions/CHANGELOG.md index c5d26b0b0c..c5ba393cb7 100644 --- a/crates/bevy_mod_scripting_functions/CHANGELOG.md +++ b/crates/bevy_mod_scripting_functions/CHANGELOG.md @@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.15.0](https://github.com/makspll/bevy_mod_scripting/compare/bevy_mod_scripting_functions-v0.14.0...bevy_mod_scripting_functions-v0.15.0) - 2025-08-14 + +### Added + +- Use the Handles, Luke! ([#427](https://github.com/makspll/bevy_mod_scripting/pull/427)) + +### Fixed + +- fix version + +### Other + +- update versions to currently released ones + ## [0.12.0](https://github.com/makspll/bevy_mod_scripting/compare/bevy_mod_scripting_functions-v0.11.1...bevy_mod_scripting_functions-v0.12.0) - 2025-04-07 ### Added diff --git a/crates/bevy_mod_scripting_functions/Cargo.toml b/crates/bevy_mod_scripting_functions/Cargo.toml index ffb4ab474a..91dac19e11 100644 --- a/crates/bevy_mod_scripting_functions/Cargo.toml +++ b/crates/bevy_mod_scripting_functions/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_mod_scripting_functions" -version = "0.14.0" +version = "0.15.0" edition = "2021" authors = ["Maksymilian Mozolewski "] license = "MIT OR Apache-2.0" @@ -35,8 +35,8 @@ uuid = "1.11" smol_str = "0.2.0" bevy_mod_scripting_core = { workspace = true } bevy_mod_scripting_derive = { workspace = true } -bevy_mod_scripting_lua = { path = "../languages/bevy_mod_scripting_lua", optional = true, version = "0.14.0" } -bevy_mod_scripting_rhai = { path = "../languages/bevy_mod_scripting_rhai", optional = true, version = "0.14.0" } +bevy_mod_scripting_lua = { path = "../languages/bevy_mod_scripting_lua", optional = true, version = "0.15.0" } +bevy_mod_scripting_rhai = { path = "../languages/bevy_mod_scripting_rhai", optional = true, version = "0.15.0" } bevy_system_reflection = { path = "../bevy_system_reflection", version = "0.2.0" } [lints] diff --git a/crates/bevy_mod_scripting_functions/src/core.rs b/crates/bevy_mod_scripting_functions/src/core.rs index 0cd5fbc969..9c0dfe2611 100644 --- a/crates/bevy_mod_scripting_functions/src/core.rs +++ b/crates/bevy_mod_scripting_functions/src/core.rs @@ -4,6 +4,7 @@ use std::{collections::HashMap, ops::Deref}; use bevy::prelude::*; use bevy_mod_scripting_core::{ + asset::ScriptAsset, bindings::{ function::{ from::Union, namespace::GlobalNamespace, script_function::DynamicScriptFunctionMut, @@ -11,6 +12,7 @@ use bevy_mod_scripting_core::{ script_system::ScriptSystemBuilder, }, docgen::info::FunctionInfo, + script::ScriptAttachment, *, }; use bevy_mod_scripting_derive::script_bindings; @@ -413,20 +415,20 @@ impl World { /// * `system`: The system that was added. fn add_system( ctxt: FunctionCallContext, - schedule: Val, - builder: Val, + #[allow(unused_variables)] schedule: Val, + #[allow(unused_variables)] builder: Val, ) -> Result, InteropError> { profiling::function_scope!("add_system"); - let world = ctxt.world()?; - let system = match ctxt.language() { + let _world = ctxt.world()?; + let _system = match ctxt.language() { #[cfg(feature = "lua_bindings")] - asset::Language::Lua => world + asset::Language::Lua => _world .add_system::( &schedule, builder.into_inner(), )?, #[cfg(feature = "rhai_bindings")] - asset::Language::Rhai => world + asset::Language::Rhai => _world .add_system::( &schedule, builder.into_inner(), @@ -442,7 +444,8 @@ impl World { )) } }; - Ok(Val(system)) + #[allow(unreachable_code)] + Ok(Val(_system)) } /// Quits the program. @@ -1045,9 +1048,10 @@ impl ReflectSchedule { profiling::function_scope!("system_by_name"); let world = ctxt.world()?; let system = world.systems(&schedule)?; - Ok(system - .into_iter() - .find_map(|s| (s.identifier() == name || s.path() == name).then_some(s.into()))) + Ok(system.into_iter().find_map(|s| { + (s.identifier() == name || s.path() == name || s.path().contains(&name)) + .then_some(s.into()) + })) } /// Renders the schedule as a dot graph string. @@ -1198,6 +1202,74 @@ impl ScriptSystemBuilder { } } +#[script_bindings( + remote, + bms_core_path = "bevy_mod_scripting_core", + name = "script_attachment_functions", + core +)] +impl ScriptAttachment { + /// Creates a new script attachment descriptor from a script asset. + /// + /// Arguments: + /// * `script`: The script asset to create the attachment from. + /// Returns: + /// * `attachment`: The new script attachment. + pub fn new_static_script( + script: Val>, + ) -> Result, InteropError> { + profiling::function_scope!("new_static_script"); + Ok(Val(ScriptAttachment::StaticScript(script.into_inner()))) + } + + /// Creates a new script attachment descriptor for an entity attached script. + /// + /// Arguments: + /// * `entity`: The entity to attach the script to. + /// * `script`: The script asset to attach to the entity. + /// Returns: + /// * `attachment`: The new script attachment for the entity. + pub fn new_entity_script( + entity: Val, + script: Val>, + ) -> Result, InteropError> { + profiling::function_scope!("new_entity_script"); + Ok(Val(ScriptAttachment::EntityScript( + *entity, + script.into_inner(), + ))) + } +} + +#[script_bindings( + remote, + bms_core_path = "bevy_mod_scripting_core", + name = "script_handle_functions", + core +)] +impl Handle { + /// Retrieves the path of the script asset if present. + /// Assets can be unloaded, and as such if the given handle is no longer active, this will return `None`. + /// + /// Arguments: + /// * `handle`: The handle to the script asset. + /// Returns: + /// * `path`: The asset path of the script asset. + fn asset_path(ctxt: FunctionCallContext, handle: Ref>) -> Option { + profiling::function_scope!("path"); + ctxt.world().ok().and_then(|w| { + w.with_resource(|assets: &Assets| { + // debug + assets + .get(&*handle) + .map(|asset| asset.asset_path.to_string()) + }) + .ok() + .flatten() + }) + } +} + #[script_bindings( remote, bms_core_path = "bevy_mod_scripting_core", @@ -1251,14 +1323,14 @@ impl GlobalNamespace { /// /// Arguments: /// * `callback`: The function name in the script this system should call when run. - /// * `script_id`: The id of the script this system will execute when run. + /// * `attachment`: The script attachment to use for the system. This is the attachment that will be used for the system's callback. /// Returns: /// * `builder`: The system builder fn system_builder( callback: String, - script_id: String, + attachment: Val, ) -> Result, InteropError> { - Ok(ScriptSystemBuilder::new(callback.into(), script_id.into()).into()) + Ok(ScriptSystemBuilder::new(callback.into(), attachment.into_inner()).into()) } } @@ -1285,6 +1357,9 @@ pub fn register_core_functions(app: &mut App) { register_reflect_system_functions(world); register_script_system_builder_functions(world); + register_script_attachment_functions(world); + register_script_handle_functions(world); + register_global_namespace_functions(world); } } diff --git a/crates/bevy_mod_scripting_functions/src/lib.rs b/crates/bevy_mod_scripting_functions/src/lib.rs index 2bfbe22357..a30e0fffd0 100644 --- a/crates/bevy_mod_scripting_functions/src/lib.rs +++ b/crates/bevy_mod_scripting_functions/src/lib.rs @@ -15,7 +15,8 @@ impl Plugin for ScriptFunctionsPlugin { register_core_functions(app); // TODO: if bevy ever does this itself we should remove this - app.world_mut().register_component::(); - app.world_mut().register_component::(); + let world_mut = app.world_mut(); + world_mut.register_component::(); + world_mut.register_component::(); } } diff --git a/crates/lad_backends/mdbook_lad_preprocessor/CHANGELOG.md b/crates/lad_backends/mdbook_lad_preprocessor/CHANGELOG.md index 0313b7d6b6..20d7a067d9 100644 --- a/crates/lad_backends/mdbook_lad_preprocessor/CHANGELOG.md +++ b/crates/lad_backends/mdbook_lad_preprocessor/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.1.11](https://github.com/makspll/bevy_mod_scripting/compare/v0.1.10-mdbook_lad_preprocessor...v0.1.11-mdbook_lad_preprocessor) - 2025-08-14 + +### Added + +- Use the Handles, Luke! ([#427](https://github.com/makspll/bevy_mod_scripting/pull/427)) + +### Other + +- update versions to currently released ones + ## [0.1.7](https://github.com/makspll/bevy_mod_scripting/compare/v0.1.6-mdbook_lad_preprocessor...v0.1.7-mdbook_lad_preprocessor) - 2025-04-07 ### Added diff --git a/crates/lad_backends/mdbook_lad_preprocessor/Cargo.toml b/crates/lad_backends/mdbook_lad_preprocessor/Cargo.toml index 6f117ccfd9..7d66f0cd61 100644 --- a/crates/lad_backends/mdbook_lad_preprocessor/Cargo.toml +++ b/crates/lad_backends/mdbook_lad_preprocessor/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mdbook_lad_preprocessor" -version = "0.1.10" +version = "0.1.11" edition = "2021" authors = ["Maksymilian Mozolewski "] license = "MIT OR Apache-2.0" diff --git a/crates/lad_backends/mdbook_lad_preprocessor/src/sections.rs b/crates/lad_backends/mdbook_lad_preprocessor/src/sections.rs index e7f2aeb9cf..412a59a130 100644 --- a/crates/lad_backends/mdbook_lad_preprocessor/src/sections.rs +++ b/crates/lad_backends/mdbook_lad_preprocessor/src/sections.rs @@ -245,7 +245,7 @@ impl<'a> Section<'a> { } } - pub(crate) fn section_items(&self) -> Vec { + pub(crate) fn section_items(&self) -> Vec> { match self.data { SectionData::Summary { .. } => { let title = self.title().clone(); diff --git a/crates/ladfile_builder/CHANGELOG.md b/crates/ladfile_builder/CHANGELOG.md index ccad3c80da..cfb67a4d30 100644 --- a/crates/ladfile_builder/CHANGELOG.md +++ b/crates/ladfile_builder/CHANGELOG.md @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.4.1](https://github.com/makspll/bevy_mod_scripting/compare/v0.4.0-ladfile_builder...v0.4.1-ladfile_builder) - 2025-08-14 + +### Added + +- Use the Handles, Luke! ([#427](https://github.com/makspll/bevy_mod_scripting/pull/427)) + +### Other + +- improve docs, fix issues +- update versions to currently released ones +- Merge remote-tracking branch 'origin/main' into staging + ## [0.4.0](https://github.com/makspll/bevy_mod_scripting/compare/v0.3.4-ladfile_builder...v0.4.0-ladfile_builder) - 2025-08-13 ### Added diff --git a/crates/ladfile_builder/Cargo.toml b/crates/ladfile_builder/Cargo.toml index af422f63d2..691a4e3420 100644 --- a/crates/ladfile_builder/Cargo.toml +++ b/crates/ladfile_builder/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ladfile_builder" -version = "0.4.0" +version = "0.4.1" edition = "2021" authors = ["Maksymilian Mozolewski "] license = "MIT OR Apache-2.0" diff --git a/crates/ladfile_builder/src/lib.rs b/crates/ladfile_builder/src/lib.rs index 469f249d35..75e71b979f 100644 --- a/crates/ladfile_builder/src/lib.rs +++ b/crates/ladfile_builder/src/lib.rs @@ -1,16 +1,7 @@ //! Parsing definitions for the LAD (Language Agnostic Decleration) file format. pub mod plugin; -use std::{ - any::TypeId, - borrow::Cow, - cmp::{max, min}, - collections::HashMap, - ffi::OsString, - path::PathBuf, -}; - -use bevy::{ecs::world::World, platform::collections::HashSet}; +use bevy::{ecs::world::World, log, platform::collections::HashSet}; use bevy_mod_scripting_core::{ bindings::{ function::{ @@ -19,7 +10,7 @@ use bevy_mod_scripting_core::{ DynamicScriptFunction, DynamicScriptFunctionMut, FunctionCallContext, }, }, - ReflectReference, + MarkAsCore, MarkAsGenerated, MarkAsSignificant, ReflectReference, }, docgen::{ info::FunctionInfo, @@ -30,6 +21,14 @@ use bevy_mod_scripting_core::{ }; use bevy_reflect::{NamedField, TypeInfo, TypeRegistry, Typed, UnnamedField}; use ladfile::*; +use std::{ + any::TypeId, + borrow::Cow, + cmp::{max, min}, + collections::HashMap, + ffi::OsString, + path::PathBuf, +}; /// We can assume that the types here will be either primitives /// or reflect types, as the rest will be covered by typed wrappers @@ -274,6 +273,22 @@ impl<'t> LadFileBuilder<'t> { /// Add a type definition to the LAD file. /// Will overwrite any existing type definitions with the same type id. pub fn add_type_info(&mut self, type_info: &TypeInfo) -> &mut Self { + let registration = self.type_registry.get(type_info.type_id()); + + let mut insignificance = default_importance(); + let mut generated = false; + if let Some(registration) = registration { + if registration.contains::() { + generated = true; + } + if registration.contains::() { + insignificance = default_importance() / 2; + } + if registration.contains::() { + insignificance = default_importance() / 4; + } + } + let type_id = self.lad_id_from_type_id(type_info.type_id()); let lad_type = LadType { identifier: type_info @@ -297,13 +312,55 @@ impl<'t> LadFileBuilder<'t> { .map(|s| s.to_owned()), path: type_info.type_path_table().path().to_owned(), layout: self.lad_layout_from_type_info(type_info), - generated: false, - insignificance: default_importance(), + generated, + insignificance, }; self.file.types.insert(type_id, lad_type); self } + /// Adds all nested types within the given `ThroughTypeInfo`. + pub fn add_through_type_info(&mut self, type_info: &ThroughTypeInfo) -> &mut Self { + match type_info { + ThroughTypeInfo::UntypedWrapper { through_type, .. } => { + self.add_type_info(through_type); + } + ThroughTypeInfo::TypedWrapper(typed_wrapper_kind) => match typed_wrapper_kind { + TypedWrapperKind::Union(ti) => { + for ti in ti { + self.add_through_type_info(ti); + } + } + TypedWrapperKind::Vec(ti) => { + self.add_through_type_info(ti); + } + TypedWrapperKind::HashMap(til, tir) => { + self.add_through_type_info(til); + self.add_through_type_info(tir); + } + TypedWrapperKind::Array(ti, _) => { + self.add_through_type_info(ti); + } + TypedWrapperKind::Option(ti) => { + self.add_through_type_info(ti); + } + TypedWrapperKind::InteropResult(ti) => { + self.add_through_type_info(ti); + } + TypedWrapperKind::Tuple(ti) => { + for ti in ti { + self.add_through_type_info(ti); + } + } + }, + ThroughTypeInfo::TypeInfo(type_info) => { + self.add_type_info(type_info); + } + } + + self + } + /// Add a function definition to the LAD file. /// Will overwrite any existing function definitions with the same function id. /// @@ -434,6 +491,12 @@ impl<'t> LadFileBuilder<'t> { LadFunctionNamespace::Type(type_id) => { if let Some(t) = file.types.get_mut(type_id) { t.associated_functions.push(function_id.clone()); + } else { + log::warn!( + "Function {} is on type {}, but the type is not registered in the LAD file.", + function_id, + type_id + ); } } LadFunctionNamespace::Global => {} @@ -788,6 +851,8 @@ impl<'t> LadFileBuilder<'t> { #[cfg(test)] mod test { + use std::collections::HashMap; + use bevy_mod_scripting_core::{ bindings::{ function::{ diff --git a/crates/ladfile_builder/src/plugin.rs b/crates/ladfile_builder/src/plugin.rs index 1bc1858b86..7a1c2f4619 100644 --- a/crates/ladfile_builder/src/plugin.rs +++ b/crates/ladfile_builder/src/plugin.rs @@ -9,7 +9,7 @@ use bevy::{ use bevy_mod_scripting_core::bindings::{ function::{namespace::Namespace, script_function::AppScriptFunctionRegistry}, globals::AppScriptGlobalsRegistry, - IntoNamespace, MarkAsCore, MarkAsGenerated, MarkAsSignificant, + IntoNamespace, }; use ladfile::{default_importance, LadTypeKind}; @@ -111,18 +111,6 @@ pub fn generate_lad_file( builder.add_type_info(type_info); - if registration.contains::() { - builder.mark_generated(registration.type_id()); - } - - if registration.contains::() { - builder.set_insignificance(registration.type_id(), default_importance() / 2); - } - - if registration.contains::() { - builder.set_insignificance(registration.type_id(), default_importance() / 4); - } - // find functions on the namespace for (_, function) in function_registry.iter_namespace(Namespace::OnType(type_info.type_id())) @@ -144,8 +132,14 @@ pub fn generate_lad_file( // find global dummies for (key, global) in global_registry.iter_dummies() { - let lad_type_id = builder.lad_id_from_type_id(global.type_id); - builder.add_instance_manually(key.to_string(), false, LadTypeKind::Val(lad_type_id)); + let kind = if let Some(type_info) = &global.type_information { + builder.add_through_type_info(type_info); + builder.lad_type_kind_from_through_type(type_info) + } else { + LadTypeKind::Val(builder.lad_id_from_type_id(global.type_id)) + }; + + builder.add_instance_manually(key.to_string(), false, kind); } let file = builder.build(); diff --git a/crates/languages/bevy_mod_scripting_lua/CHANGELOG.md b/crates/languages/bevy_mod_scripting_lua/CHANGELOG.md index 05c4e1947d..4cc90637f6 100644 --- a/crates/languages/bevy_mod_scripting_lua/CHANGELOG.md +++ b/crates/languages/bevy_mod_scripting_lua/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.15.0](https://github.com/makspll/bevy_mod_scripting/compare/bevy_mod_scripting_lua-v0.14.0...bevy_mod_scripting_lua-v0.15.0) - 2025-08-14 + +### Added + +- Use the Handles, Luke! ([#427](https://github.com/makspll/bevy_mod_scripting/pull/427)) + +### Other + +- update versions to currently released ones + ## [0.11.0](https://github.com/makspll/bevy_mod_scripting/compare/bevy_mod_scripting_lua-v0.10.0...bevy_mod_scripting_lua-v0.11.0) - 2025-03-29 ### Added diff --git a/crates/languages/bevy_mod_scripting_lua/Cargo.toml b/crates/languages/bevy_mod_scripting_lua/Cargo.toml index 56b21a8c77..d43cf70c43 100644 --- a/crates/languages/bevy_mod_scripting_lua/Cargo.toml +++ b/crates/languages/bevy_mod_scripting_lua/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_mod_scripting_lua" -version = "0.14.0" +version = "0.15.0" authors = ["Maksymilian Mozolewski "] edition = "2021" license = "MIT OR Apache-2.0" diff --git a/crates/languages/bevy_mod_scripting_lua/src/lib.rs b/crates/languages/bevy_mod_scripting_lua/src/lib.rs index 33df2015d0..a99404d47d 100644 --- a/crates/languages/bevy_mod_scripting_lua/src/lib.rs +++ b/crates/languages/bevy_mod_scripting_lua/src/lib.rs @@ -1,10 +1,11 @@ //! Lua integration for the bevy_mod_scripting system. use bevy::{ app::Plugin, + asset::Handle, ecs::{entity::Entity, world::World}, }; use bevy_mod_scripting_core::{ - asset::Language, + asset::{Language, ScriptAsset}, bindings::{ function::namespace::Namespace, globals::AppScriptGlobalsRegistry, script_value::ScriptValue, ThreadWorldContainer, WorldContainer, @@ -14,7 +15,7 @@ use bevy_mod_scripting_core::{ event::CallbackLabel, reflection_extensions::PartialReflectExt, runtime::RuntimeSettings, - script::ScriptId, + script::{ContextPolicy, ScriptAttachment}, IntoScriptPluginParams, ScriptingPlugin, }; use bindings::{ @@ -52,7 +53,6 @@ impl Default for LuaScriptingPlugin { fn default() -> Self { LuaScriptingPlugin { scripting_plugin: ScriptingPlugin { - context_assignment_strategy: Default::default(), runtime_settings: RuntimeSettings::default(), callback_handler: lua_handler, context_builder: ContextBuilder:: { @@ -115,23 +115,37 @@ impl Default for LuaScriptingPlugin { Ok(()) }, ], - context_pre_handling_initializers: vec![|script_id, entity, context| { + context_pre_handling_initializers: vec![|context_key, context| { + // TODO: convert these to functions let world = ThreadWorldContainer.try_get_world()?; + if let Some(entity) = context_key.entity() { + context + .globals() + .set( + "entity", + LuaReflectReference(::allocate( + Box::new(entity), + world.clone(), + )), + ) + .map_err(ScriptError::from_mlua_error)?; + } context .globals() .set( - "entity", - LuaReflectReference(::allocate(Box::new(entity), world)), + "script_asset", + LuaReflectReference(>::allocate( + Box::new(context_key.script()), + world, + )), ) .map_err(ScriptError::from_mlua_error)?; - context - .globals() - .set("script_id", script_id) - .map_err(ScriptError::from_mlua_error)?; + Ok(()) }], - additional_supported_extensions: &[], language: Language::Lua, + context_policy: ContextPolicy::default(), + emit_responses: false, }, } } @@ -149,18 +163,18 @@ impl Plugin for LuaScriptingPlugin { fn load_lua_content_into_context( context: &mut Lua, - script_id: &ScriptId, + context_key: &ScriptAttachment, content: &[u8], initializers: &[ContextInitializer], pre_handling_initializers: &[ContextPreHandlingInitializer], ) -> Result<(), ScriptError> { initializers .iter() - .try_for_each(|init| init(script_id, context))?; + .try_for_each(|init| init(context_key, context))?; pre_handling_initializers .iter() - .try_for_each(|init| init(script_id, Entity::from_raw(0), context))?; + .try_for_each(|init| init(context_key, context))?; context .load(content) @@ -173,7 +187,7 @@ fn load_lua_content_into_context( #[profiling::function] /// Load a lua context from a script pub fn lua_context_load( - script_id: &ScriptId, + context_key: &ScriptAttachment, content: &[u8], initializers: &[ContextInitializer], pre_handling_initializers: &[ContextPreHandlingInitializer], @@ -186,7 +200,7 @@ pub fn lua_context_load( load_lua_content_into_context( &mut context, - script_id, + context_key, content, initializers, pre_handling_initializers, @@ -197,7 +211,7 @@ pub fn lua_context_load( #[profiling::function] /// Reload a lua context from a script pub fn lua_context_reload( - script: &ScriptId, + context_key: &ScriptAttachment, content: &[u8], old_ctxt: &mut Lua, initializers: &[ContextInitializer], @@ -206,7 +220,7 @@ pub fn lua_context_reload( ) -> Result<(), ScriptError> { load_lua_content_into_context( old_ctxt, - script, + context_key, content, initializers, pre_handling_initializers, @@ -219,8 +233,7 @@ pub fn lua_context_reload( /// The lua handler for events pub fn lua_handler( args: Vec, - entity: bevy::ecs::entity::Entity, - script_id: &ScriptId, + context_key: &ScriptAttachment, callback_label: &CallbackLabel, context: &mut Lua, pre_handling_initializers: &[ContextPreHandlingInitializer], @@ -228,15 +241,15 @@ pub fn lua_handler( ) -> Result { pre_handling_initializers .iter() - .try_for_each(|init| init(script_id, entity, context))?; + .try_for_each(|init| init(context_key, context))?; let handler: Function = match context.globals().raw_get(callback_label.as_ref()) { Ok(handler) => handler, // not subscribed to this event type Err(_) => { bevy::log::trace!( - "Script {} is not subscribed to callback {}", - script_id, + "Context {} is not subscribed to callback {}", + context_key, callback_label.as_ref() ); return Ok(ScriptValue::Unit); @@ -255,6 +268,10 @@ pub fn lua_handler( #[cfg(test)] mod test { + use bevy::{ + asset::{AssetId, AssetIndex}, + prelude::Handle, + }; use mlua::Value; use super::*; @@ -262,15 +279,16 @@ mod test { #[test] fn test_reload_doesnt_overwrite_old_context() { let lua = Lua::new(); - let script_id = ScriptId::from("asd.lua"); let initializers = vec![]; let pre_handling_initializers = vec![]; let mut old_ctxt = lua.clone(); + let handle = Handle::Weak(AssetId::from(AssetIndex::from_bits(0))); + let context_key = ScriptAttachment::EntityScript(Entity::from_raw(1), handle); lua_context_load( - &script_id, + &context_key, "function hello_world_from_first_load() - + end" .as_bytes(), &initializers, @@ -280,9 +298,9 @@ mod test { .unwrap(); lua_context_reload( - &script_id, + &context_key, "function hello_world_from_second_load() - + end" .as_bytes(), &mut old_ctxt, diff --git a/crates/languages/bevy_mod_scripting_rhai/CHANGELOG.md b/crates/languages/bevy_mod_scripting_rhai/CHANGELOG.md index 187c3df3f4..89d4c92a93 100644 --- a/crates/languages/bevy_mod_scripting_rhai/CHANGELOG.md +++ b/crates/languages/bevy_mod_scripting_rhai/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.15.0](https://github.com/makspll/bevy_mod_scripting/compare/bevy_mod_scripting_rhai-v0.14.0...bevy_mod_scripting_rhai-v0.15.0) - 2025-08-14 + +### Added + +- Use the Handles, Luke! ([#427](https://github.com/makspll/bevy_mod_scripting/pull/427)) + +### Other + +- update versions to currently released ones + ## [0.11.0](https://github.com/makspll/bevy_mod_scripting/compare/bevy_mod_scripting_rhai-v0.10.0...bevy_mod_scripting_rhai-v0.11.0) - 2025-03-29 ### Added diff --git a/crates/languages/bevy_mod_scripting_rhai/Cargo.toml b/crates/languages/bevy_mod_scripting_rhai/Cargo.toml index 17878ddef3..33de227bfe 100644 --- a/crates/languages/bevy_mod_scripting_rhai/Cargo.toml +++ b/crates/languages/bevy_mod_scripting_rhai/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_mod_scripting_rhai" -version = "0.14.0" +version = "0.15.0" authors = ["Maksymilian Mozolewski "] edition = "2021" license = "MIT OR Apache-2.0" diff --git a/crates/languages/bevy_mod_scripting_rhai/src/lib.rs b/crates/languages/bevy_mod_scripting_rhai/src/lib.rs index aed318fbf7..5eb1b479d8 100644 --- a/crates/languages/bevy_mod_scripting_rhai/src/lib.rs +++ b/crates/languages/bevy_mod_scripting_rhai/src/lib.rs @@ -4,10 +4,11 @@ use std::ops::Deref; use bevy::{ app::Plugin, + asset::Handle, ecs::{entity::Entity, world::World}, }; use bevy_mod_scripting_core::{ - asset::Language, + asset::{Language, ScriptAsset}, bindings::{ function::namespace::Namespace, globals::AppScriptGlobalsRegistry, script_value::ScriptValue, ThreadWorldContainer, WorldContainer, @@ -17,7 +18,7 @@ use bevy_mod_scripting_core::{ event::CallbackLabel, reflection_extensions::PartialReflectExt, runtime::RuntimeSettings, - script::ScriptId, + script::{ContextPolicy, DisplayProxy, ScriptAttachment}, IntoScriptPluginParams, ScriptingPlugin, }; use bindings::{ @@ -68,7 +69,6 @@ impl Default for RhaiScriptingPlugin { fn default() -> Self { RhaiScriptingPlugin { scripting_plugin: ScriptingPlugin { - context_assignment_strategy: Default::default(), runtime_settings: RuntimeSettings { initializers: vec![|runtime| { let mut engine = runtime.write(); @@ -149,18 +149,32 @@ impl Default for RhaiScriptingPlugin { Ok(()) }, ], - context_pre_handling_initializers: vec![|script, entity, context| { + context_pre_handling_initializers: vec![|context_key, context| { let world = ThreadWorldContainer.try_get_world()?; + + if let Some(entity) = context_key.entity() { + context.scope.set_or_push( + "entity", + RhaiReflectReference(::allocate( + Box::new(entity), + world.clone(), + )), + ); + } context.scope.set_or_push( - "entity", - RhaiReflectReference(::allocate(Box::new(entity), world)), + "script_asset", + RhaiReflectReference(>::allocate( + Box::new(context_key.script().clone()), + world, + )), ); - context.scope.set_or_push("script_id", script.to_owned()); + Ok(()) }], // already supported by BMS core - additional_supported_extensions: &[], language: Language::Rhai, + context_policy: ContextPolicy::default(), + emit_responses: false, }, } } @@ -179,7 +193,7 @@ impl Plugin for RhaiScriptingPlugin { // NEW helper function to load content into an existing context without clearing previous definitions. fn load_rhai_content_into_context( context: &mut RhaiScriptContext, - script: &ScriptId, + context_key: &ScriptAttachment, content: &[u8], initializers: &[ContextInitializer], pre_handling_initializers: &[ContextPreHandlingInitializer], @@ -188,14 +202,16 @@ fn load_rhai_content_into_context( let runtime = runtime.read(); context.ast = runtime.compile(std::str::from_utf8(content)?)?; - context.ast.set_source(script.to_string()); + context + .ast + .set_source(context_key.script().display().to_string()); initializers .iter() - .try_for_each(|init| init(script, context))?; + .try_for_each(|init| init(context_key, context))?; pre_handling_initializers .iter() - .try_for_each(|init| init(script, Entity::from_raw(0), context))?; + .try_for_each(|init| init(context_key, context))?; runtime.eval_ast_with_scope(&mut context.scope, &context.ast)?; context.ast.clear_statements(); @@ -204,7 +220,7 @@ fn load_rhai_content_into_context( /// Load a rhai context from a script. pub fn rhai_context_load( - script: &ScriptId, + context_key: &ScriptAttachment, content: &[u8], initializers: &[ContextInitializer], pre_handling_initializers: &[ContextPreHandlingInitializer], @@ -217,7 +233,7 @@ pub fn rhai_context_load( }; load_rhai_content_into_context( &mut context, - script, + context_key, content, initializers, pre_handling_initializers, @@ -228,7 +244,7 @@ pub fn rhai_context_load( /// Reload a rhai context from a script. New content is appended to the existing context. pub fn rhai_context_reload( - script: &ScriptId, + context_key: &ScriptAttachment, content: &[u8], context: &mut RhaiScriptContext, initializers: &[ContextInitializer], @@ -237,7 +253,7 @@ pub fn rhai_context_reload( ) -> Result<(), ScriptError> { load_rhai_content_into_context( context, - script, + context_key, content, initializers, pre_handling_initializers, @@ -249,8 +265,7 @@ pub fn rhai_context_reload( /// The rhai callback handler. pub fn rhai_callback_handler( args: Vec, - entity: Entity, - script_id: &ScriptId, + context_key: &ScriptAttachment, callback: &CallbackLabel, context: &mut RhaiScriptContext, pre_handling_initializers: &[ContextPreHandlingInitializer], @@ -258,7 +273,7 @@ pub fn rhai_callback_handler( ) -> Result { pre_handling_initializers .iter() - .try_for_each(|init| init(script_id, entity, context))?; + .try_for_each(|init| init(context_key, context))?; // we want the call to be able to impact the scope let options = CallFnOptions::new().rewind_scope(false); @@ -268,9 +283,9 @@ pub fn rhai_callback_handler( .collect::, _>>()?; bevy::log::trace!( - "Calling callback {} in script {} with args: {:?}", + "Calling callback {} in context {} with args: {:?}", callback, - script_id, + context_key, args ); let runtime = runtime.read(); @@ -286,8 +301,8 @@ pub fn rhai_callback_handler( Err(e) => { if let EvalAltResult::ErrorFunctionNotFound(_, _) = e.unwrap_inner() { bevy::log::trace!( - "Script {} is not subscribed to callback {} with the provided arguments.", - script_id, + "Context {} is not subscribed to callback {} with the provided arguments.", + context_key, callback ); Ok(ScriptValue::Unit) @@ -297,45 +312,3 @@ pub fn rhai_callback_handler( } } } - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_reload_doesnt_overwrite_old_context() { - let runtime = RhaiRuntime::new(Engine::new()); - let script_id = ScriptId::from("asd.rhai"); - let initializers: Vec> = vec![]; - let pre_handling_initializers: Vec> = - vec![]; - - // Load first content defining a function that returns 42. - let mut context = rhai_context_load( - &script_id, - b"let hello = 2;", - &initializers, - &pre_handling_initializers, - &runtime, - ) - .unwrap(); - - // Reload with additional content defining a second function that returns 24. - rhai_context_reload( - &script_id, - b"let hello2 = 3", - &mut context, - &initializers, - &pre_handling_initializers, - &runtime, - ) - .unwrap(); - - // get first var - let hello = context.scope.get_value::("hello").unwrap(); - assert_eq!(hello, 2); - // get second var - let hello2 = context.scope.get_value::("hello2").unwrap(); - assert_eq!(hello2, 3); - } -} diff --git a/crates/testing_crates/script_integration_test_harness/Cargo.toml b/crates/testing_crates/script_integration_test_harness/Cargo.toml index 723e8a8cae..6891665daf 100644 --- a/crates/testing_crates/script_integration_test_harness/Cargo.toml +++ b/crates/testing_crates/script_integration_test_harness/Cargo.toml @@ -6,7 +6,11 @@ publish = false [features] default = ["lua", "rhai"] -lua = ["bevy_mod_scripting_lua", "bevy_mod_scripting_functions/lua_bindings"] +lua = [ + "bevy_mod_scripting_lua", + "bevy_mod_scripting_functions/lua_bindings", + "bevy_mod_scripting_lua/lua54", +] rhai = ["bevy_mod_scripting_rhai", "bevy_mod_scripting_functions/rhai_bindings"] [dependencies] @@ -23,3 +27,7 @@ bevy_mod_scripting_rhai = { path = "../../languages/bevy_mod_scripting_rhai", op criterion = "0.5" rand = "0.9" rand_chacha = "0.9" +uuid = "1.11" +anyhow = "1.0" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" diff --git a/crates/testing_crates/script_integration_test_harness/src/lib.rs b/crates/testing_crates/script_integration_test_harness/src/lib.rs index 16908a3be6..f3f8733709 100644 --- a/crates/testing_crates/script_integration_test_harness/src/lib.rs +++ b/crates/testing_crates/script_integration_test_harness/src/lib.rs @@ -1,40 +1,35 @@ +pub mod parse; +pub mod scenario; pub mod test_functions; use std::{ - marker::PhantomData, path::PathBuf, time::{Duration, Instant}, }; use bevy::{ - app::{Last, Plugin, PostUpdate, Startup, Update}, - asset::{AssetServer, Handle}, + app::{App, Plugin, PostUpdate, Startup, Update}, + asset::{AssetPath, AssetServer, Handle, LoadState}, ecs::{ - component::Component, - event::{Event, Events}, - prelude::{Command, Resource}, - schedule::ScheduleConfigs, - system::{BoxedSystem, InfallibleSystemWrapper, IntoSystem, Local, Res}, - world::{FromWorld, Mut}, + component::Component, resource::Resource, schedule::IntoScheduleConfigs, system::Command, + world::FromWorld, }, - log::{tracing, tracing::event, Level}, - prelude::{BevyError, Entity, IntoScheduleConfigs, World}, - reflect::{Reflect, TypeRegistry}, + log::{ + tracing::{self, event}, + Level, + }, + reflect::Reflect, }; use bevy_mod_scripting_core::{ - asset::ScriptAsset, bindings::{ - pretty_print::DisplayWithWorld, script_value::ScriptValue, CoreScriptGlobalsPlugin, - ReflectAccessId, WorldAccessGuard, WorldGuard, + pretty_print::DisplayWithWorld, CoreScriptGlobalsPlugin, ReflectAccessId, WorldAccessGuard, + WorldGuard, }, - callback_labels, commands::CreateOrUpdateScript, - error::{InteropError, ScriptError}, - event::{IntoCallbackLabel, ScriptErrorEvent}, + error::ScriptError, extractors::HandlerContext, - handler::handle_script_errors, - script::ScriptId, - BMSScriptingInfrastructurePlugin, IntoScriptPluginParams, ScriptingPlugin, + script::{DisplayProxy, ScriptAttachment, ScriptComponent, ScriptId}, + BMSScriptingInfrastructurePlugin, IntoScriptPluginParams, }; use bevy_mod_scripting_functions::ScriptFunctionsPlugin; use criterion::{measurement::Measurement, BatchSize}; @@ -42,58 +37,30 @@ use rand::{Rng, SeedableRng}; use test_functions::{register_test_functions, RNG}; use test_utils::test_data::setup_integration_test; +use crate::scenario::Scenario; + fn dummy_update_system() {} fn dummy_startup_system() {} fn dummy_before_post_update_system() {} fn dummy_post_update_system() {} -#[derive(Event)] -struct TestEventFinished; - -struct TestCallbackBuilder { - _ph: PhantomData<(P, L)>, -} - -impl TestCallbackBuilder { - fn build( - script_id: impl Into, - expect_response: bool, - ) -> ScheduleConfigs>> { - let script_id = script_id.into(); - let system = Box::new(InfallibleSystemWrapper::new( - IntoSystem::into_system(move |world: &mut World| { - let mut handler_ctxt = HandlerContext::

::yoink(world); - let guard = WorldAccessGuard::new_exclusive(world); - let _ = run_test_callback::( - &script_id.clone(), - guard, - &mut handler_ctxt, - expect_response, - ); - - handler_ctxt.release(world); - }) - .with_name(L::into_callback_label().to_string()), - )); - - system.into_configs() - } -} - -pub fn install_test_plugin( - app: &mut bevy::app::App, - plugin: P, - include_test_functions: bool, -) { +pub fn install_test_plugin(app: &mut bevy::app::App, include_test_functions: bool) { app.add_plugins(( ScriptFunctionsPlugin, CoreScriptGlobalsPlugin::default(), BMSScriptingInfrastructurePlugin, - plugin, )); if include_test_functions { register_test_functions(app); } + app.add_systems(Update, dummy_update_system); + app.add_systems(Startup, dummy_startup_system::); + + app.add_systems( + PostUpdate, + dummy_before_post_update_system.before(dummy_post_update_system), + ); + app.add_systems(PostUpdate, dummy_post_update_system); } #[cfg(feature = "lua")] @@ -196,26 +163,7 @@ pub fn make_test_rhai_plugin() -> bevy_mod_scripting_rhai::RhaiScriptingPlugin { }) } -#[cfg(feature = "lua")] -pub fn execute_lua_integration_test(script_id: &str) -> Result<(), String> { - let plugin = make_test_lua_plugin(); - execute_integration_test(plugin, |_, _| {}, script_id) -} - -#[cfg(feature = "rhai")] -pub fn execute_rhai_integration_test(script_id: &str) -> Result<(), String> { - let plugin = make_test_rhai_plugin(); - execute_integration_test(plugin, |_, _| {}, script_id) -} - -pub fn execute_integration_test< - P: IntoScriptPluginParams + Plugin + AsMut>, - F: FnOnce(&mut World, &mut TypeRegistry), ->( - plugin: P, - init: F, - script_id: &str, -) -> Result<(), String> { +pub fn execute_integration_test(scenario: Scenario) -> Result<(), String> { // set "BEVY_ASSET_ROOT" to the global assets folder, i.e. CARGO_MANIFEST_DIR/../../../assets let mut manifest_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); @@ -229,127 +177,18 @@ pub fn execute_integration_test< std::env::set_var("BEVY_ASSET_ROOT", manifest_dir.clone()); - let mut app = setup_integration_test(init); - - install_test_plugin(&mut app, plugin, true); - - app.add_event::(); - - callback_labels!( - OnTest => "on_test", - OnTestPostUpdate => "on_test_post_update", - OnTestLast => "on_test_last", - ); - - let script_id = script_id.to_owned(); - let script_id: &'static str = Box::leak(script_id.into_boxed_str()); - - let load_system = |server: Res, mut handle: Local>| { - *handle = server.load(script_id.to_owned()); - }; - - // tests can opt in to this via "__RETURN" - let expect_callback_response = script_id.contains("__RETURN"); - - app.add_systems(Startup, load_system); - app.add_systems( - Update, - TestCallbackBuilder::::build(script_id, expect_callback_response), - ); - app.add_systems( - PostUpdate, - TestCallbackBuilder::::build(script_id, expect_callback_response), - ); - app.add_systems( - Last, - TestCallbackBuilder::::build(script_id, expect_callback_response), - ); - app.add_systems(Update, dummy_update_system); - app.add_systems(Startup, dummy_startup_system::); - - app.add_systems( - PostUpdate, - dummy_before_post_update_system.before(dummy_post_update_system), - ); - app.add_systems(PostUpdate, dummy_post_update_system); - - app.cleanup(); - app.finish(); - - let start = Instant::now(); // start the timer - - loop { - app.update(); - - if start.elapsed() > Duration::from_secs(10) { - return Err("Timeout after 10 seconds".into()); - } - - let error_events = app - .world_mut() - .resource_mut::>() - .drain() - .collect::>(); - - if let Some(event) = error_events.into_iter().next() { - return Err(event - .error - .display_with_world(WorldGuard::new_exclusive(app.world_mut()))); - } - - let events_completed = app.world_mut().resource_ref::>(); - if !events_completed.is_empty() { - return Ok(()); - } + match scenario.execute(App::default()) { + Ok(_) => Ok(()), + Err(e) => Err(format!("{e:?}")), } } -fn run_test_callback( - script_id: &str, - guard: WorldGuard, - handler_ctxt: &mut HandlerContext

, - expect_response: bool, -) -> Result { - if !handler_ctxt.is_script_fully_loaded(script_id.to_string().into()) { - return Ok(ScriptValue::Unit); - } - - let res = handler_ctxt.call::( - &script_id.to_string().into(), - Entity::from_raw(0), - vec![], - guard.clone(), - ); - - let e = match res { - Ok(ScriptValue::Error(e)) => e.into(), - Err(e) => e, - Ok(v) => { - if expect_response && !matches!(v, ScriptValue::Bool(true)) { - InteropError::external_error(format!("Response from callback {} was either not received or wasn't correct. Expected true, got: {v:?}", C::into_callback_label()).into()).into() - } else { - match guard.with_resource_mut(|mut events: Mut>| { - events.send(TestEventFinished) - }) { - Ok(_) => return Ok(v), - Err(e) => e.into(), - } - } - } - }; - - handle_script_errors(guard, vec![e.clone()].into_iter()); - - Err(e) -} - #[cfg(feature = "lua")] pub fn run_lua_benchmark( script_id: &str, label: &str, criterion: &mut criterion::BenchmarkGroup, ) -> Result<(), String> { - use bevy::log::Level; use bevy_mod_scripting_lua::mlua::Function; let plugin = make_test_lua_plugin(); @@ -366,6 +205,8 @@ pub fn run_lua_benchmark( pre_bencher.call::<()>(()).unwrap(); } c.iter(|| { + use bevy::log::{tracing, Level}; + tracing::event!(Level::TRACE, "profiling_iter {}", label); bencher.call::<()>(()).unwrap(); }) @@ -381,7 +222,6 @@ pub fn run_rhai_benchmark( label: &str, criterion: &mut criterion::BenchmarkGroup, ) -> Result<(), String> { - use bevy::log::Level; use bevy_mod_scripting_rhai::rhai::Dynamic; let plugin = make_test_rhai_plugin(); @@ -403,6 +243,8 @@ pub fn run_rhai_benchmark( } c.iter(|| { + use bevy::log::{tracing, Level}; + tracing::event!(Level::TRACE, "profiling_iter {}", label); let _ = runtime .call_fn::(&mut ctxt.scope, &ctxt.ast, "bench", ARGS) @@ -414,9 +256,9 @@ pub fn run_rhai_benchmark( ) } -pub fn run_plugin_benchmark( +pub fn run_plugin_benchmark<'a, P, F, M: criterion::measurement::Measurement>( plugin: P, - script_id: &str, + script_path: impl Into>, label: &str, criterion: &mut criterion::BenchmarkGroup, bench_fn: F, @@ -431,16 +273,15 @@ where let mut app = setup_integration_test(|_, _| {}); - install_test_plugin(&mut app, plugin, true); - - let script_id = script_id.to_owned(); - let script_id_clone = script_id.clone(); - app.add_systems( - Startup, - move |server: Res, mut handle: Local>| { - *handle = server.load(script_id_clone.to_owned()); - }, - ); + install_test_plugin(&mut app, true); + app.add_plugins(plugin); + let script_path = script_path.into(); + let script_handle = app.world().resource::().load(script_path); + let script_id = script_handle.id(); + let entity = app + .world_mut() + .spawn(ScriptComponent(vec![script_handle.clone()])) + .id(); // finalize app.cleanup(); @@ -448,36 +289,46 @@ where let timer = Instant::now(); + // Wait until script is loaded. loop { - app.update(); - - let mut context = HandlerContext::

::yoink(app.world_mut()); - let guard = WorldAccessGuard::new_exclusive(app.world_mut()); - - if context.is_script_fully_loaded(script_id.clone().into()) { - let script = context - .scripts() - .get_mut(script_id.to_owned()) - .ok_or_else(|| String::from("Could not find scripts resource"))?; - let ctxt_arc = script.context.clone(); - let mut ctxt_locked = ctxt_arc.lock(); - - let runtime = &context.runtime_container().runtime; - - return WorldAccessGuard::with_existing_static_guard(guard, |guard| { - // Ensure the world is available via ThreadWorldContainer - ThreadWorldContainer - .set_world(guard.clone()) - .map_err(|e| e.display_with_world(guard))?; - // Pass the locked context to the closure for benchmarking its Lua (or generic) part - bench_fn(&mut ctxt_locked, runtime, label, criterion) - }); - } - context.release(app.world_mut()); if timer.elapsed() > Duration::from_secs(30) { return Err("Timeout after 30 seconds, could not load script".into()); } + app.update(); + match app.world().resource::().load_state(script_id) { + LoadState::Loaded => break, + LoadState::Failed(e) => { + return Err(format!( + "Failed to load script {}: {e}", + script_handle.display() + )); + } + _ => continue, + } } + + app.update(); + + let mut context = HandlerContext::

::yoink(app.world_mut()); + let guard = WorldGuard::new_exclusive(app.world_mut()); + + let context_key = ScriptAttachment::EntityScript(entity, Handle::Weak(script_id)); + + let ctxt_arc = context.script_context().get(&context_key).unwrap(); + let mut ctxt_locked = ctxt_arc.lock(); + + let runtime = &context.runtime_container().runtime; + + let _ = WorldAccessGuard::with_existing_static_guard(guard, |guard| { + // Ensure the world is available via ThreadWorldContainer + ThreadWorldContainer + .set_world(guard.clone()) + .map_err(|e| e.display_with_world(guard))?; + // Pass the locked context to the closure for benchmarking its Lua (or generic) part + bench_fn(&mut ctxt_locked, runtime, label, criterion) + }); + context.release(app.world_mut()); + Ok(()) } pub fn run_plugin_script_load_benchmark< @@ -488,11 +339,11 @@ pub fn run_plugin_script_load_benchmark< benchmark_id: &str, content: &str, criterion: &mut criterion::BenchmarkGroup, - script_id_generator: impl Fn(u64) -> String, reload_probability: f32, ) { let mut app = setup_integration_test(|_, _| {}); - install_test_plugin(&mut app, plugin, false); + install_test_plugin(&mut app, false); + app.add_plugins(plugin); let mut rng_guard = RNG.lock().unwrap(); *rng_guard = rand_chacha::ChaCha12Rng::from_seed([42u8; 32]); drop(rng_guard); @@ -501,17 +352,16 @@ pub fn run_plugin_script_load_benchmark< || { let mut rng = RNG.lock().unwrap(); let is_reload = rng.random_range(0f32..=1f32) < reload_probability; - let random_id = if is_reload { 0 } else { rng.random::() }; - - let random_script_id = script_id_generator(random_id); - // we manually load the script inside a command - let content = content.to_string().into_boxed_str(); + let random_id = if is_reload { 0 } else { rng.random::() }; + let random_script_id: ScriptId = ScriptId::from( + uuid::Builder::from_random_bytes(random_id.to_le_bytes()).into_uuid(), + ); + // We manually load the script inside a command. ( - CreateOrUpdateScript::

::new( - random_script_id.into(), - content.clone().into(), - None, - ), + CreateOrUpdateScript::

::new(ScriptAttachment::StaticScript(Handle::Weak( + random_script_id, + ))) + .with_content(content), is_reload, ) }, diff --git a/crates/testing_crates/script_integration_test_harness/src/parse.rs b/crates/testing_crates/script_integration_test_harness/src/parse.rs new file mode 100644 index 0000000000..1b6142e274 --- /dev/null +++ b/crates/testing_crates/script_integration_test_harness/src/parse.rs @@ -0,0 +1,461 @@ +use std::path::PathBuf; + +use anyhow::Error; +use bevy_mod_scripting_core::{ + asset::Language, + bindings::ScriptValue, + callback_labels, + event::{ + CallbackLabel, OnScriptLoaded, OnScriptReloaded, OnScriptUnloaded, Recipients, + ScriptCallbackEvent, + }, + script::{ContextPolicy, ScriptAttachment}, +}; + +use crate::scenario::{ScenarioContext, ScenarioStep, SCENARIO_SELF_LANGUAGE_NAME}; + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, PartialEq, Eq)] +pub enum ScenarioSchedule { + Startup, + Update, + FixedUpdate, + PostUpdate, + Last, +} + +/// The mode of context assignment for scripts in the scenario. +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, PartialEq, Eq)] +pub enum ContextMode { + Global, + PerEntity, + PerEntityPerScript, +} + +callback_labels!( + OnTest => "on_test", + OnTestPostUpdate => "on_test_post_update", + OnTestLast => "on_test_last", + CallbackA => "callback_a", + CallbackB => "callback_b", + CallbackC => "callback_c", +); + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, PartialEq, Eq)] +#[serde(tag = "step")] +pub enum ScenarioStepSerialized { + Comment { + comment: String, + }, + /// Installs the scripting plugin with the given settings and intializes the app + InstallPlugin { + context_policy: Option, + emit_responses: Option, + }, + /// Called after the app config is set up, but before we run anything + FinalizeApp, + /// Sets up a handler for the given schedule and label. + /// You can onle use one of the following callbacks: + /// - `on_test` + /// - `on_test_post_update` + /// - `on_test_last` + /// - `callback_a` + /// - `callback_b` + /// - `callback_c` + /// + /// and main bevy schedule labels. + SetupHandler { + #[serde(flatten)] + schedule: ScenarioSchedule, + #[serde(flatten)] + label: ScenarioLabel, + }, + /// Loads a script from the given path and assigns it a name, + /// this handle can be used later when loaded. + LoadScriptAs { + path: PathBuf, + as_name: String, + }, + /// Waits until the script with the given name is loaded. + WaitForScriptLoaded { + name: String, + }, + /// Spawns an entity with the given name and attaches the given script to it. + SpawnEntityWithScript { + name: String, + script: String, + }, + AttachStaticScript { + script: String, + }, + DetachStaticScript { + script: String, + }, + /// Drops the script asset from the scenario context. + DropScriptAsset { + script: String, + }, + /// Despawns the entity with the given name. + DespawnEntity { + entity: String, + }, + /// Emits a ScriptCallbackEvent + EmitScriptCallbackEvent { + label: ScenarioLabel, + #[serde(flatten)] + recipients: ScenarioRecipients, + language: Option, + #[serde(default)] + emit_response: bool, + string_value: Option, + }, + + /// Run the app update loop once + RunUpdateOnce, + + /// Asserts that a callback response was triggered for the given script attachment + AssertCallbackSuccess { + label: ScenarioLabel, + #[serde(flatten)] + attachment: ScenarioAttachment, + expect_string_value: Option, + language: Option, + }, + AssertNoCallbackResponsesEmitted, + AssertContextResidents { + #[serde(flatten)] + script: ScenarioAttachment, + residents_num: usize, + }, + /// Modifies the existing script asset by reloading it from the given path. + ReloadScriptFrom { + script: String, + path: PathBuf, + }, + + /// Sets the current script language context to be used untll this is changed + SetCurrentLanguage { + language: ScenarioLanguage, + }, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, PartialEq, Eq)] +#[serde(tag = "attachment")] +pub enum ScenarioAttachment { + EntityScript { entity: String, script: String }, + StaticScript { script: String }, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, PartialEq, Eq)] +pub enum ScenarioLanguage { + Lua, + Rhai, + #[serde(rename = "@this_script_language")] + ThisScriptLanguage, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, PartialEq, Eq)] +pub enum ScenarioLabel { + OnTest, + OnTestPostUpdate, + OnTestLast, + CallbackA, + CallbackB, + CallbackC, + OnScriptLoaded, + OnScriptUnloaded, + OnScriptReloaded, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, PartialEq, Eq)] +#[serde(tag = "recipients")] +pub enum ScenarioRecipients { + AllScripts, + AllContexts, + EntityScript { entity: String, script: String }, + StaticScript { script: String }, +} + +impl ScenarioStepSerialized { + pub fn parse_language(language: ScenarioLanguage) -> Language { + match language { + ScenarioLanguage::Lua => Language::Lua, + ScenarioLanguage::Rhai => Language::Rhai, + ScenarioLanguage::ThisScriptLanguage => { + Language::External(SCENARIO_SELF_LANGUAGE_NAME.into()) + } + } + } + + pub fn resolve_attachment( + context: &ScenarioContext, + attachment: ScenarioAttachment, + ) -> Result { + match attachment { + ScenarioAttachment::EntityScript { entity, script } => { + let entity = context.get_entity(&entity)?; + let script = context.get_script_handle(&script)?; + Ok(ScriptAttachment::EntityScript(entity, script)) + } + ScenarioAttachment::StaticScript { script } => { + let script = context.get_script_handle(&script)?; + Ok(ScriptAttachment::StaticScript(script)) + } + } + } + + pub fn resolve_recipients( + context: &ScenarioContext, + recipients: ScenarioRecipients, + ) -> Result { + Ok(match recipients { + ScenarioRecipients::AllScripts => Recipients::AllScripts, + ScenarioRecipients::AllContexts => Recipients::AllContexts, + ScenarioRecipients::EntityScript { entity, script } => Recipients::ScriptEntity( + context.get_script_handle(&script)?.id(), + context.get_entity(&entity)?, + ), + ScenarioRecipients::StaticScript { script } => { + Recipients::StaticScript(context.get_script_handle(&script)?.id()) + } + }) + } + + pub fn resolve_label(label: ScenarioLabel) -> CallbackLabel { + match label { + ScenarioLabel::OnTest => OnTest.into(), + ScenarioLabel::OnTestPostUpdate => OnTestPostUpdate.into(), + ScenarioLabel::OnTestLast => OnTestLast.into(), + ScenarioLabel::CallbackA => CallbackA.into(), + ScenarioLabel::CallbackB => CallbackB.into(), + ScenarioLabel::CallbackC => CallbackC.into(), + ScenarioLabel::OnScriptLoaded => OnScriptLoaded.into(), + ScenarioLabel::OnScriptUnloaded => OnScriptUnloaded.into(), + ScenarioLabel::OnScriptReloaded => OnScriptReloaded.into(), + } + } + + pub fn resolve_context_policy(context_policy: Option) -> ContextPolicy { + match context_policy { + Some(ContextMode::Global) => ContextPolicy::shared(), + Some(ContextMode::PerEntity) => ContextPolicy::per_entity(), + Some(ContextMode::PerEntityPerScript) => ContextPolicy::per_entity_and_script(), + None => ContextPolicy::default(), + } + } + + pub fn parse_and_resolve(self, context: &ScenarioContext) -> Result { + Ok(match self { + Self::FinalizeApp => ScenarioStep::FinalizeApp, + Self::AssertContextResidents { + script, + residents_num, + } => ScenarioStep::AssertContextResidents { + script: Self::resolve_attachment(context, script)?, + residents_num, + }, + Self::AttachStaticScript { script } => ScenarioStep::AttachStaticScript { + script: context.get_script_handle(&script)?, + }, + Self::DetachStaticScript { script } => ScenarioStep::DetachStaticScript { + script: context.get_script_handle(&script)?, + }, + Self::SetCurrentLanguage { language } => ScenarioStep::SetCurrentLanguage { + language: Self::parse_language(language), + }, + Self::InstallPlugin { + context_policy, + emit_responses, + } => ScenarioStep::InstallPlugin { + context_policy: Self::resolve_context_policy(context_policy), + emit_responses: emit_responses.unwrap_or(false), + }, + Self::DropScriptAsset { script } => ScenarioStep::DropScriptAsset { + script: context.get_script_handle(&script)?, + }, + Self::RunUpdateOnce => ScenarioStep::RunUpdateOnce, + Self::EmitScriptCallbackEvent { + label, + recipients, + language, + emit_response, + string_value, + } => { + let label = Self::resolve_label(label.clone()); + let recipients = Self::resolve_recipients(context, recipients.clone())?; + let language = language.map(Self::parse_language); + let payload = string_value + .map(|s| vec![ScriptValue::String(s.into())]) + .unwrap_or(vec![]); + let mut event = ScriptCallbackEvent::new(label, payload, recipients, language); + if emit_response { + event = event.with_response(); + } + ScenarioStep::EmitScriptCallbackEvent { event } + } + Self::AssertCallbackSuccess { + label, + attachment, + expect_string_value, + language, + } => ScenarioStep::AssertCallbackSuccess { + label: Self::resolve_label(label.clone()), + script: Self::resolve_attachment(context, attachment)?, + expect_string_value, + language: language.map(Self::parse_language), + }, + Self::SetupHandler { schedule, label } => ScenarioStep::SetupHandler { + schedule, + label: Self::resolve_label(label), + }, + Self::LoadScriptAs { path, as_name } => ScenarioStep::LoadScriptAs { + path, + as_name: as_name.to_string(), + }, + Self::WaitForScriptLoaded { name } => ScenarioStep::WaitForScriptLoaded { + script: context.get_script_handle(&name)?, + }, + Self::SpawnEntityWithScript { name, script } => ScenarioStep::SpawnEntityWithScript { + script: context.get_script_handle(&script)?, + entity: name, + }, + Self::ReloadScriptFrom { script, path } => ScenarioStep::ReloadScriptFrom { + script: context.get_script_handle(&script)?, + path, + }, + Self::AssertNoCallbackResponsesEmitted => { + ScenarioStep::AssertNoCallbackResponsesEmitted + } + Self::DespawnEntity { entity } => ScenarioStep::DespawnEntity { + entity: context.get_entity(&entity)?, + }, + Self::Comment { comment } => ScenarioStep::Comment { comment }, + }) + } + + pub fn to_json(&self) -> Result { + Ok(serde_json::to_string(self)?) + } + + pub fn from_json(json: &str) -> Result { + Ok(serde_json::from_str(json)?) + } + + /// converts to json object then converts to a format like: + /// StepName key="value", key2=value2 + pub fn to_flat_string(&self) -> Result { + let json = self.to_json()?; + let json_obj: serde_json::Value = serde_json::from_str(&json)?; + let mut flat_string = String::new(); + if let serde_json::Value::Object(map) = json_obj { + // the `step` key is the name of the step + if let Some(step_name) = map.get("step") { + flat_string.push_str(&format!("{} ", step_name.as_str().unwrap_or(""))); + } + let non_step_keys: Vec<_> = map.into_iter().filter(|(k, _)| k != "step").collect(); + for (index, (key, value)) in non_step_keys.iter().enumerate() { + if key != "step" { + flat_string.push_str(&format!( + "{}=\"{}\"", + key, + value.as_str().unwrap_or(value.to_string().as_str()) + )); + } + if index + 1 != non_step_keys.len() { + flat_string.push_str(", "); + } + } + } + Ok(flat_string.trim().to_string()) + } + + pub fn from_flat_string(flat_string: &str) -> Result { + let flat_string = flat_string.trim(); + if flat_string.starts_with("//") { + // This is a comment, ignore it + return Ok(ScenarioStepSerialized::Comment { + comment: flat_string.trim_start_matches("//").trim().to_string(), + }); + } + + let mut parts = flat_string.split_whitespace(); + let step_name = parts + .next() + .ok_or_else(|| anyhow::anyhow!("Invalid flat string step: `{}`", flat_string))?; + let mut map = serde_json::Map::new(); + map.insert( + "step".to_string(), + serde_json::Value::String(step_name.to_string()), + ); + + let arg_part = parts.collect::>().join(" "); + + let args = arg_part + .split(',') + .map(str::trim) + .filter(|p| !p.is_empty()) + .collect::>(); + + for part in args { + let mut kv = part.split('='); + let key = kv.next().ok_or_else(|| { + anyhow::anyhow!( + "Key-value pair must be in the format key=\"value\" in part: `{part}`" + ) + })?; + let value = kv + .next() + .ok_or_else(|| { + anyhow::anyhow!( + "Key-value pair must be in the format key=\"value\" in part: `{part}`" + ) + })? + .trim(); + let parsed_value = if !value.starts_with('"') && !value.ends_with('"') { + serde_json::from_str::(value) + .map_err(|e| Error::msg(format!("Failed to parse value: {e}")))? + } else { + serde_json::Value::String(value.trim_matches('"').to_string()) + }; + map.insert(key.trim().to_string(), parsed_value); + } + + Ok(serde_json::from_value(serde_json::Value::Object(map))?) + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_scenario_step_serialized_to_flat_string() { + let step = ScenarioStepSerialized::AssertCallbackSuccess { + label: ScenarioLabel::OnTest, + attachment: ScenarioAttachment::EntityScript { + entity: "entity1".to_string(), + script: "script1".to_string(), + }, + expect_string_value: None, + language: None, + }; + let flat_string = step.to_flat_string().unwrap(); + assert_eq!(flat_string, "AssertCallbackSuccess attachment=\"EntityScript\", entity=\"entity1\", expect_string_value=\"null\", label=\"OnTest\", language=\"null\", script=\"script1\""); + } + + #[test] + fn test_scenario_step_serialized_from_flat_string() { + let flat_string = "AssertCallbackSuccess attachment=\"EntityScript\", entity=\"entity1\", label=\"OnTest\", script=\"script1\""; + let step = ScenarioStepSerialized::from_flat_string(flat_string).unwrap(); + assert_eq!( + step, + ScenarioStepSerialized::AssertCallbackSuccess { + label: ScenarioLabel::OnTest, + attachment: ScenarioAttachment::EntityScript { + entity: "entity1".to_string(), + script: "script1".to_string(), + }, + expect_string_value: None, + language: None, + } + ); + } +} diff --git a/crates/testing_crates/script_integration_test_harness/src/scenario.rs b/crates/testing_crates/script_integration_test_harness/src/scenario.rs new file mode 100644 index 0000000000..1c5fa8a71f --- /dev/null +++ b/crates/testing_crates/script_integration_test_harness/src/scenario.rs @@ -0,0 +1,788 @@ +use crate::{install_test_plugin, parse::*}; +use anyhow::{anyhow, Context, Error}; +use bevy::ecs::entity::Entity; +use bevy::ecs::system::Command; +use bevy::prelude::IntoSystem; +use bevy::{ + app::App, + asset::{AssetEvent, Handle, LoadState}, + ecs::{ + event::{Event, EventCursor, Events}, + schedule::ScheduleLabel, + world::World, + }, +}; +use bevy_mod_scripting_core::asset::Language; +use bevy_mod_scripting_core::bindings::{DisplayWithWorld, ScriptValue, WorldGuard}; +use bevy_mod_scripting_core::commands::{AddStaticScript, RemoveStaticScript}; +use bevy_mod_scripting_core::event::ScriptEvent; +use bevy_mod_scripting_core::script::ContextPolicy; +use bevy_mod_scripting_core::script::ScriptContext; +use bevy_mod_scripting_core::{ + asset::ScriptAsset, + event::{CallbackLabel, IntoCallbackLabel, ScriptCallbackEvent, ScriptCallbackResponseEvent}, + handler::event_handler, + script::{ScriptAttachment, ScriptComponent}, +}; +use bevy_mod_scripting_core::{ConfigureScriptPlugin, LanguageExtensions}; +use std::borrow::Cow; +use std::collections::VecDeque; +use std::{ + collections::HashMap, + path::{Path, PathBuf}, + time::Instant, +}; +use test_utils::test_data::setup_integration_test; + +const TIMEOUT_SECONDS: u64 = 10; +pub const SCENARIO_SELF_SCRIPT_NAME: &str = "@this_script"; +pub const SCENARIO_SELF_LANGUAGE_NAME: &str = "@this_language"; + +pub struct Scenario { + pub steps: Vec, + pub context: ScenarioContext, +} + +impl Scenario { + /// Parses a scenario from a file. + pub fn from_scenario_file( + this_script_path: &Path, + scenario_path: &Path, + ) -> Result { + let content = std::fs::read_to_string(scenario_path).with_context(|| { + format!("Failed to read scenario file: {}", scenario_path.display()) + })?; + let lines = content + .lines() + .map(|line| line.trim()) + .filter(|line| !line.is_empty()) + .collect::>(); + + let steps = lines + .into_iter() + .map(|line| { + ScenarioStepSerialized::from_flat_string(line) + .with_context(|| format!("Failed to parse scenario step: {line}")) + }) + .collect::, Error>>()?; + Ok(Self { + steps, + context: ScenarioContext::new(this_script_path.to_path_buf()), + }) + } + + pub fn scenario_error( + watcher: &InterestingEventWatcher, + steps: &[ScenarioStepSerialized], + error_step: (usize, Error), + ) -> Error { + let msg = steps + .iter() + .enumerate() + .map(|(i, step)| { + let step = format!("#{i}: {}", step.to_flat_string().unwrap_or_default()); + // print events, in blue + let event_colour_start = "\x1b[34m"; + let event_colour_end = "\x1b[0m"; + let step_events = watcher + .events + .iter() + .filter(|(_, step_no)| *step_no == i) + .flat_map(|(event, _)| event.lines()) + .map(|line| format!("\n\t{event_colour_start}{line}{event_colour_end}")) + .collect::>() + .join(""); + + if i == error_step.0 { + let anyhow_error_pretty = format!("{:?}", error_step.1); + let tabulated = anyhow_error_pretty + .lines() + .map(|line| format!("\t{line}")) + .collect::>() + .join("\n"); + let colour_start = "\x1b[31m"; + let colour_end = "\x1b[0m"; + + format!("{colour_start}{step}\n{tabulated}{colour_end}{step_events}") + } else { + format!("{step}{step_events}") + } + }) + .collect::>() + .join("\n"); + + anyhow::anyhow!( + "Error in scenario:\n{}\n\nWith steps:\n{}", + error_step.1, + msg + ) + } + + pub fn execute(mut self, mut app: App) -> Result<(), Error> { + let original_steps = self.steps.clone(); + for (i, step) in self.steps.into_iter().enumerate() { + bevy::log::info!( + "Executing step #{i}: {}", + step.to_flat_string().unwrap_or_default() + ); + self.context.current_step_no = i; + let parsed_step = step.parse_and_resolve(&self.context)?; + if let Err(err) = parsed_step.execute(&mut self.context, &mut app) { + let error = + Scenario::scenario_error(&self.context.event_log, &original_steps, (i, err)); + return Err(error); + } + } + Ok(()) + } +} + +/// Serves as a chalkboard for the test scenario to write to and read from. +#[derive(Debug, Clone)] +pub struct ScenarioContext { + pub script_handles: HashMap>, + pub entities: HashMap, + pub scenario_time_started: Instant, + pub this_script_asset_relative_path: PathBuf, + pub event_log: InterestingEventWatcher, + pub current_step_no: usize, + pub current_script_language: Option, + pub initialized_app: bool, +} + +#[derive(Debug, Clone, Default)] +pub struct InterestingEventWatcher { + pub events: Vec<(String, usize)>, + pub asset_event_cursor: EventCursor>, + pub script_events_cursor: EventCursor, + pub script_response_cursor: EventCursor, + pub script_responses_queue: VecDeque, +} + +impl InterestingEventWatcher { + pub fn log_events(&mut self, step_no: usize, world: &bevy::ecs::world::World) { + let asset_events = world.resource::>>(); + let script_events = world.resource::>(); + let script_responses = world.resource::>(); + let mut tracked_with_id = Vec::default(); + for (event, id) in self.asset_event_cursor.read_with_id(asset_events) { + tracked_with_id.push((id.id, format!("AssetEvent : {event:?}"))); + } + for (event, id) in self.script_events_cursor.read_with_id(script_events) { + tracked_with_id.push((id.id, format!("ScriptEvent: {event:?}"))); + } + let mut script_responses_by_id = Vec::default(); + for (event, id) in self.script_response_cursor.read_with_id(script_responses) { + script_responses_by_id.push((id.id, event.clone())); + tracked_with_id.push((id.id, format!("ScriptResponse: {event:?}"))); + } + + script_responses_by_id.sort_by_key(|(id, _)| *id); + tracked_with_id.sort_by_key(|(id, _)| *id); + for (_, event) in tracked_with_id { + self.events.push((event, step_no)); + } + for event in script_responses_by_id { + self.script_responses_queue.push_back(event.1); + } + } +} + +impl ScenarioContext { + pub fn new(script_asset_path: PathBuf) -> Self { + Self { + scenario_time_started: Instant::now(), + script_handles: HashMap::new(), + this_script_asset_relative_path: script_asset_path, + entities: HashMap::new(), + event_log: InterestingEventWatcher::default(), + current_step_no: 0, + current_script_language: None, + initialized_app: false, + } + } + + /// Returns a path relative to the parent of the current script in the "assets" frame of reference. + pub fn scenario_path(&self, path: &PathBuf) -> PathBuf { + self.this_script_asset_relative_path + .parent() + .unwrap_or(&PathBuf::new()) + .join(path) + } + + /// Returns the absolute path to the assets directory + pub fn assets_path(&self) -> PathBuf { + PathBuf::from(std::env::var("BEVY_ASSET_ROOT").ok().unwrap()).join("assets") + } + + /// Resolves an assset relative scenario path to an absolute path using the assets manifest path. + pub fn absolute_scenario_path(&self, path: &PathBuf) -> PathBuf { + self.assets_path().join(self.scenario_path(path)) + } + + pub fn get_script_handle(&self, name: &str) -> Result, Error> { + self + .script_handles + .get(name) + .cloned() + .ok_or_else(|| anyhow!("Script with name '{name}' not found in context. Did you miss a `LoadScriptAs` step?")) + } + + pub fn get_entity(&self, name: &str) -> Result { + self + .entities + .get(name) + .cloned() + .ok_or_else(|| anyhow!("Entity with name '{name}' not found in context. Did you miss a `SpawnEntityWithScript` step?")) + } +} + +impl ScenarioSchedule { + pub fn add_handler( + &self, + language: Option, + app: &mut App, + ) { + let language = language.unwrap_or(Language::External("Unset language".into())); + match language { + #[cfg(feature = "lua")] + Language::Lua => { + let system = IntoSystem::into_system( + event_handler::, + ) + .with_name(T::into_callback_label().to_string()); + app.add_systems(self.clone(), system); + } + #[cfg(feature = "rhai")] + Language::Rhai => { + let system = IntoSystem::into_system( + event_handler::, + ) + .with_name(T::into_callback_label().to_string()); + app.add_systems(self.clone(), system); + } + _ => { + panic!("Unsupported language for scenario schedule: {language:?}"); + } + } + } +} +impl ScheduleLabel for ScenarioSchedule { + fn dyn_clone(&self) -> Box { + match self { + ScenarioSchedule::Startup => bevy::app::Startup.dyn_clone(), + ScenarioSchedule::Update => bevy::app::Update.dyn_clone(), + ScenarioSchedule::FixedUpdate => bevy::app::FixedUpdate.dyn_clone(), + ScenarioSchedule::PostUpdate => bevy::app::PostUpdate.dyn_clone(), + ScenarioSchedule::Last => bevy::app::Last.dyn_clone(), + } + } + + fn as_dyn_eq(&self) -> &dyn bevy::ecs::label::DynEq { + match self { + ScenarioSchedule::Startup => bevy::app::Startup.as_dyn_eq(), + ScenarioSchedule::Update => bevy::app::Update.as_dyn_eq(), + ScenarioSchedule::FixedUpdate => bevy::app::FixedUpdate.as_dyn_eq(), + ScenarioSchedule::PostUpdate => bevy::app::PostUpdate.as_dyn_eq(), + ScenarioSchedule::Last => bevy::app::Last.as_dyn_eq(), + } + } + + fn dyn_hash(&self, state: &mut dyn ::core::hash::Hasher) { + match self { + ScenarioSchedule::Startup => bevy::app::Startup.dyn_hash(state), + ScenarioSchedule::Update => bevy::app::Update.dyn_hash(state), + ScenarioSchedule::FixedUpdate => bevy::app::FixedUpdate.dyn_hash(state), + ScenarioSchedule::PostUpdate => bevy::app::PostUpdate.dyn_hash(state), + ScenarioSchedule::Last => bevy::app::Last.dyn_hash(state), + } + } +} + +#[derive(Debug)] +pub enum ScenarioStep { + /// A comment in the scenario, ignored during execution. + Comment { + comment: String, + }, + /// Installs the scripting plugin with the given context policy and whether to emit responses. + InstallPlugin { + context_policy: ContextPolicy, + emit_responses: bool, + }, + /// Finalizes the app, cleaning up resources and preparing for the next steps. + FinalizeApp, + SetCurrentLanguage { + language: Language, + }, + + /// Sets up a handler for the given schedule and label. + /// You can onle use one of the following callbacks: + /// - `on_test` + /// - `on_test_post_update` + /// - `on_test_last` + /// - `callback_a` + /// - `callback_b` + /// - `callback_c` + /// + /// and main bevy schedule labels. + SetupHandler { + schedule: ScenarioSchedule, + label: CallbackLabel, + }, + /// Loads a script from the given path and assigns it a name, + /// this handle can be used later when loaded. + LoadScriptAs { + path: PathBuf, + as_name: String, + }, + /// Waits until the script with the given name is loaded. + WaitForScriptLoaded { + script: Handle, + }, + /// Spawns an entity with the given name and attaches the given script to it. + SpawnEntityWithScript { + script: Handle, + entity: String, + }, + AttachStaticScript { + script: Handle, + }, + DetachStaticScript { + script: Handle, + }, + /// Drops the named script asset from the scenario context. + DropScriptAsset { + script: Handle, + }, + + /// Emits a ScriptCallbackEvent + EmitScriptCallbackEvent { + event: ScriptCallbackEvent, + }, + + /// Run the app update loop once + RunUpdateOnce, + + /// Asserts that a callback response was triggered for the given label and from the given recipient + AssertCallbackSuccess { + label: CallbackLabel, + script: ScriptAttachment, + expect_string_value: Option, + language: Option, + }, + + /// Asserts that no more callback events are left to process. + AssertNoCallbackResponsesEmitted, + + /// Reloads script with the given name from the specified path. + ReloadScriptFrom { + script: Handle, + path: PathBuf, + }, + /// Asserts that a context for the given attachment does not exist + AssertContextResidents { + script: ScriptAttachment, + residents_num: usize, + }, + + /// Despawns the entity with the given name. + DespawnEntity { + entity: Entity, + }, +} + +/// Execution +impl ScenarioStep { + pub fn run_update_catching_error_events( + context: &mut ScenarioContext, + app: &mut App, + ) -> Result<(), Error> { + app.update(); + + // add watched events + let world = app.world_mut(); + context.event_log.log_events(context.current_step_no, world); + if context.scenario_time_started.elapsed().as_secs() > TIMEOUT_SECONDS { + return Err(anyhow!( + "Test scenario timed out after {} seconds", + TIMEOUT_SECONDS + )); + } + Ok(()) + } + + /// Will execute the app update loop until an event of type `T` is received or we timeout. + pub fn execute_until_event< + T: Event + Clone, + E, + F: Fn(&T) -> bool, + G: Fn(&World) -> Option, + >( + context: &mut ScenarioContext, + app: &mut App, + filter: F, + early_exit: G, + ) -> Result, E>, Error> { + let mut event_cursor = EventCursor::::default(); + loop { + { + let world = app.world_mut(); + let events = world.resource::>(); + + let events = event_cursor + .read(events) + .filter(|&e| filter(e)) + .cloned() + .collect::>(); + if !events.is_empty() { + return Ok(Ok(events)); + } + if let Some(e) = early_exit(world) { + return Ok(Err(e)); + } + } + Self::run_update_catching_error_events(context, app) + .with_context(|| format!("timed out waiting for event {}", stringify!(T)))?; + } + } + + pub fn execute(self, context: &mut ScenarioContext, app: &mut App) -> Result<(), Error> { + match self { + ScenarioStep::SetCurrentLanguage { language } => { + let language = if language == Language::External(SCENARIO_SELF_LANGUAGE_NAME.into()) + { + // main script language can be gotten from the "this_script_asset_relative_path" + let extension = context + .this_script_asset_relative_path + .extension() + .and_then(|ext| ext.to_str()) + .unwrap_or_default(); + let extensions = LanguageExtensions::default(); + match extensions.get(extension) { + Some(language) => language.clone(), + None => { + return Err(anyhow!( + "Unknown script language for extension: {}", + extension + )); + } + } + } else { + language + }; + + context.current_script_language = Some(language); + bevy::log::info!( + "Set current script language to: {:?}", + context.current_script_language + ); + } + ScenarioStep::FinalizeApp => { + app.finish(); + app.cleanup(); + bevy::log::info!("App finalized and cleaned up"); + } + ScenarioStep::InstallPlugin { + context_policy, + emit_responses, + } => { + if !context.initialized_app { + *app = setup_integration_test(|_, _| {}); + install_test_plugin(app, true); + context.initialized_app = true; + } + + match context.current_script_language { + #[cfg(feature = "lua")] + Some(Language::Lua) => { + let plugin = crate::make_test_lua_plugin(); + let plugin = plugin + .set_context_policy(context_policy) + .emit_core_callback_responses(emit_responses); + app.add_plugins(plugin); + } + #[cfg(feature = "rhai")] + Some(Language::Rhai) => { + let plugin = crate::make_test_rhai_plugin(); + let plugin = plugin + .set_context_policy(context_policy) + .emit_core_callback_responses(emit_responses); + app.add_plugins(plugin); + } + _ => { + return Err(anyhow!( + "Scenario step InstallPlugin is not supported for the current plugin type: '{}'", + context.current_script_language + .as_ref() + .map(|l| l.to_string()) + .unwrap_or_else(|| "None".to_string()) + )); + } + } + return Ok(()); + } + ScenarioStep::LoadScriptAs { path, as_name } => { + let path = if path.ends_with(SCENARIO_SELF_SCRIPT_NAME) { + context + .this_script_asset_relative_path + .file_name() + .unwrap_or_default() + .into() + } else { + path.clone() + }; + let asset_server = app.world_mut().resource::(); + let script_handle = asset_server.load(context.scenario_path(&path)); + context + .script_handles + .insert(as_name.to_string(), script_handle); + + bevy::log::info!( + "Script '{}' marked for loading from path '{}'", + as_name, + path.display() + ); + } + ScenarioStep::WaitForScriptLoaded { script } => { + let res = Self::execute_until_event::, _, _, _>( + context, + app, + |e| e.is_added(script.id()), + |w| { + let server = w.resource::(); + if let LoadState::Failed(r) = server.load_state(script.id()) { + Some(r) + } else { + None + } + }, + )?; + + if let Err(e) = res { + return Err(anyhow!("Failed to load script: {e}")); + } + + bevy::log::info!("Script '{}' loaded successfully", script.id()); + } + ScenarioStep::SetupHandler { schedule, label } => { + match label.to_string().as_str() { + "on_test" => { + schedule.add_handler::(context.current_script_language.clone(), app) + } + "on_test_post_update" => schedule.add_handler::( + context.current_script_language.clone(), + app, + ), + "on_test_last" => schedule + .add_handler::(context.current_script_language.clone(), app), + "callback_a" => schedule + .add_handler::(context.current_script_language.clone(), app), + "callback_b" => schedule + .add_handler::(context.current_script_language.clone(), app), + "callback_c" => schedule + .add_handler::(context.current_script_language.clone(), app), + _ => { + return Err(anyhow!( + "callback label: {} is not allowed, you can only use one of a set of labels", + label + )) + } + } + } + ScenarioStep::SpawnEntityWithScript { + entity: name, + script, + } => { + let entity = app + .world_mut() + .spawn(ScriptComponent::new([script.clone()])) + .id(); + + context.entities.insert(name.to_string(), entity); + bevy::log::info!("Spawned entity '{}' with script '{}'", entity, script.id()); + } + ScenarioStep::EmitScriptCallbackEvent { event } => { + app.world_mut().send_event(event.clone()); + } + ScenarioStep::AssertCallbackSuccess { + label, + script, + expect_string_value, + language, + } => { + let next_event = context.event_log.script_responses_queue.pop_front(); + + if let Some(event) = next_event { + let language_correct = language.is_none_or(|l| l == event.language); + if event.label != label || event.context_key != script || !language_correct { + return Err(anyhow!( + "Callback '{}' for attachment: '{}' was not the next event, found: {:?}. Order of events was incorrect.", + label, + script.to_string(), + event + )); + } + + match &event.response { + Ok(val) => { + bevy::log::info!( + "Callback '{}' for attachment: '{}' succeeded, with value: {:?}", + label, + script.to_string(), + &val + ); + + if let Some(expected_string) = expect_string_value.as_ref() { + if ScriptValue::String(Cow::Owned(expected_string.clone())) != *val + { + return Err(anyhow!( + "Callback '{}' for attachment: '{}' expected: {}, but got: {}", + label, + script.to_string(), + expected_string, + val.display_with_world(WorldGuard::new_exclusive(app.world_mut())) + )); + } + } + } + Err(e) => { + return Err(anyhow!( + "Callback '{}' for attachment: '{}' failed with error: {}", + label, + script.to_string(), + e.display_with_world(WorldGuard::new_exclusive(app.world_mut())) + )); + } + } + } else { + return Err(anyhow!( + "No callback response event found for label: {} and attachment: {}", + label, + script.to_string() + )); + } + } + ScenarioStep::RunUpdateOnce => { + Self::run_update_catching_error_events(context, app)?; + } + ScenarioStep::DropScriptAsset { script } => { + let name = context + .script_handles + .iter_mut() + .find_map(|(name, handle)| { + if handle.id() == script.id() { + *handle = handle.clone_weak(); + Some(name.clone()) + } else { + None + } + }) + .ok_or_else(|| { + anyhow!( + "Script asset with id '{}' not found in context", + script.id() + ) + })?; + bevy::log::info!("Dropped script asset '{}' from context", name); + } + ScenarioStep::ReloadScriptFrom { script, path } => { + let mut assets = app + .world_mut() + .resource_mut::>(); + + let absolute_path = context.absolute_scenario_path(&path); + + if let Some(existing) = assets.get_mut(&script) { + let content = std::fs::read_to_string(&absolute_path).with_context(|| { + format!("Failed to read script file: {}", absolute_path.display()) + })?; + let boxed_byte_arr = content.into_bytes().into_boxed_slice(); + *existing = ScriptAsset { + content: boxed_byte_arr, + asset_path: path.into(), + language: existing.language.clone(), + }; + } else { + return Err(anyhow!( + "Script asset with id '{}' not found in context. Tried reloading from path: {}", + script.id(), + path.display() + )); + } + } + ScenarioStep::AssertNoCallbackResponsesEmitted => { + let next_event = context.event_log.script_responses_queue.pop_front(); + if next_event.is_some() { + return Err(anyhow!( + "Expected no callback responses to be emitted, but found: {:?}", + next_event + )); + } else { + bevy::log::info!("No callback responses emitted as expected"); + } + } + ScenarioStep::DespawnEntity { entity } => { + let success = app.world_mut().despawn(entity); + if !success { + return Err(anyhow!( + "Failed to despawn entity with name '{}'. It may not exist.", + entity + )); + } else { + bevy::log::info!("Despawning entity with name '{}'", entity); + } + } + ScenarioStep::AttachStaticScript { script } => { + AddStaticScript::new(script.clone()).apply(app.world_mut()); + bevy::log::info!("Attached static script with handle: {}", script.id()); + } + ScenarioStep::DetachStaticScript { script } => { + RemoveStaticScript::new(script.clone()).apply(app.world_mut()); + bevy::log::info!("Detached static script with handle: {}", script.id()); + } + ScenarioStep::AssertContextResidents { + script, + residents_num, + } => { + let world = app.world_mut(); + let residents = match context.current_script_language { + #[cfg(feature = "lua")] + Some(Language::Lua) => world + .resource::>() + .residents_len(&script), + #[cfg(feature = "rhai")] + Some(Language::Rhai) => world + .resource::>() + .residents_len(&script), + _ => { + return Err(anyhow!( + "Scenario step AssertContextRemoved is not supported for the current plugin type: '{:?}'", + context.current_script_language + )); + } + }; + + if residents != residents_num { + return Err(anyhow!( + "Expected {} residents for script attachment: {}, but found {}", + residents_num, + script.to_string(), + residents + )); + } else { + bevy::log::info!( + "Script attachment: {} has {} residents as expected", + script.to_string(), + residents + ); + } + } + ScenarioStep::Comment { comment } => { + // Comments are ignored, do nothing, log it though for debugging + bevy::log::info!("Comment: {}", comment); + } + } + Ok(()) + } +} diff --git a/crates/testing_crates/test_utils/src/lib.rs b/crates/testing_crates/test_utils/src/lib.rs index 4bfee6a338..3d7ca0893e 100644 --- a/crates/testing_crates/test_utils/src/lib.rs +++ b/crates/testing_crates/test_utils/src/lib.rs @@ -24,8 +24,10 @@ impl std::fmt::Display for TestKind { #[derive(Debug, Clone)] pub struct Test { - pub path: PathBuf, + pub script_asset_path: PathBuf, pub kind: TestKind, + /// If the test contains an explicit scenario, this will be set. + pub scenario_path: Option, } fn visit_dirs(dir: &Path, cb: &mut dyn FnMut(&DirEntry)) -> io::Result<()> { @@ -45,6 +47,34 @@ fn visit_dirs(dir: &Path, cb: &mut dyn FnMut(&DirEntry)) -> io::Result<()> { Ok(()) } +/// searches for the nearest ancestor of the given path that satisfies the condition. +/// stops the search upon reaching the manifest path. +fn find_nearest_ancestor(path: &Path, condition: impl Fn(&Path) -> bool) -> Option { + // check path is within the manifest path + let manifest_path = std::env::var("CARGO_MANIFEST_DIR") + .ok() + .map(PathBuf::from) + .unwrap(); + + let manifest_path_ancestors = path + .ancestors() + .filter(|p| p.starts_with(&manifest_path) && p.is_dir()) + .collect::>(); + + for ancestor in manifest_path_ancestors { + let siblings = fs::read_dir(ancestor).ok()?; + let entries = siblings.filter_map(Result::ok).collect::>(); + for entry in entries { + let entry_path = entry.path(); + if entry_path.is_file() && condition(&entry_path) { + return Some(entry_path); + } + } + } + + None +} + pub fn discover_all_tests(manifest_dir: PathBuf, filter: impl Fn(&Test) -> bool) -> Vec { let assets_root = manifest_dir.join("assets"); let mut test_files = Vec::new(); @@ -60,9 +90,45 @@ pub fn discover_all_tests(manifest_dir: PathBuf, filter: impl Fn(&Test) -> bool) { // only take the path from the assets bit let relative = path.strip_prefix(&assets_root).unwrap(); + + let scenario_path = find_nearest_ancestor(&path, |p| { + p.file_name() + .and_then(|f| f.to_str()) + .is_some_and(|p| p == "scenario.txt" || p == "group_scenario.txt") + }); + + // if the scenario has a `// #main_script filename` line, check if this script is the main script in the scenario + // if not ignore it. we only want to run against the main script in the scenario. + let main_script_path = scenario_path.as_ref().and_then(|scenario| { + let scenario_content = fs::read_to_string(scenario).unwrap_or_default(); + scenario_content.lines().find_map(|line| { + let main_script_line = line.contains("#main_script"); + if !main_script_line { + return None; + } + + let main_script_path = line + .split_once("#main_script ") + .map(|(_, main_script_path)| main_script_path.trim()) + .unwrap_or_default(); + let main_script_path = PathBuf::from(main_script_path); + Some(main_script_path) + }) + }); + + let is_main_script_in_scenario = + main_script_path.as_ref().is_none_or(|main_script_path| { + relative.file_name() == main_script_path.file_name() + }); + + if !is_main_script_in_scenario { + return; + } + let test = Test { - path: relative.to_path_buf(), + script_asset_path: relative.to_path_buf(), kind, + scenario_path, }; if !filter(&test) { diff --git a/crates/testing_crates/test_utils/src/test_data.rs b/crates/testing_crates/test_utils/src/test_data.rs index 55b4b8daa4..199305d50e 100644 --- a/crates/testing_crates/test_utils/src/test_data.rs +++ b/crates/testing_crates/test_utils/src/test_data.rs @@ -1,13 +1,10 @@ use std::{alloc::Layout, collections::HashMap}; -use bevy::{ - asset::AssetPlugin, - diagnostic::DiagnosticsPlugin, - ecs::{component::*, world::World}, - log::LogPlugin, - prelude::*, - reflect::*, -}; +use bevy::asset::AssetPlugin; +use bevy::diagnostic::DiagnosticsPlugin; +use bevy::ecs::{component::*, world::World}; +use bevy::prelude::*; +use bevy::reflect::*; /// Test component with Reflect and ReflectComponent registered #[derive(Component, Reflect, PartialEq, Eq, Debug)] @@ -350,7 +347,7 @@ pub fn setup_integration_test(init: F) MinimalPlugins, AssetPlugin::default(), DiagnosticsPlugin, - LogPlugin { + bevy::log::LogPlugin { filter: log_level, ..Default::default() }, diff --git a/crates/testing_crates/test_utils/src/test_plugin.rs b/crates/testing_crates/test_utils/src/test_plugin.rs index 7a091f6c8d..c3b2a955c8 100644 --- a/crates/testing_crates/test_utils/src/test_plugin.rs +++ b/crates/testing_crates/test_utils/src/test_plugin.rs @@ -3,7 +3,7 @@ #[macro_export] macro_rules! make_test_plugin { ($ident: ident) => { - // #[derive(Default)] + #[derive(std::fmt::Debug)] struct TestPlugin($ident::ScriptingPlugin); impl Default for TestPlugin { @@ -31,13 +31,17 @@ macro_rules! make_test_plugin { } } - #[derive(Default)] + #[derive(Default, std::fmt::Debug)] struct TestRuntime { - pub invocations: - parking_lot::Mutex>, + pub invocations: parking_lot::Mutex< + Vec<( + Option, + Option<$ident::script::ScriptId>, + )>, + >, } - #[derive(Default)] + #[derive(Default, std::fmt::Debug, Clone)] struct TestContext { pub invocations: Vec<$ident::bindings::script_value::ScriptValue>, } diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000000..3f78fdc5a1 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,27 @@ +# BMS Docs + +## How To + +### Run Tests + +To run the tests within the book do this: +``` sh +cd docs; +mdbook test -L ../target/debug/deps +``` + +To run tests on a particular chapter, say the "Managing Scripts" chapter, do this: +``` sh +cd docs; +mdbook test -L ../target/debug/deps -c "Managing Scripts" +``` + +## Troubleshooting + +If there are errors that complain about different rustc versions, consider reinstalling "mdbook" using whatever toolchain BMS is built with. + +``` sh +cargo uninstall mdbook +cargo +stable install mdbook +``` + diff --git a/docs/src/ReleaseNotes/0.13-to-0.14.md b/docs/src/ReleaseNotes/0.13-to-0.14.md new file mode 100644 index 0000000000..6d3d8b754d --- /dev/null +++ b/docs/src/ReleaseNotes/0.13-to-0.14.md @@ -0,0 +1,4 @@ +# 0.12-to-0.13 + +## Bevy Version Upgrade +This version of BMS requires Bevy 0.16.0, see the bevy migration guide for details on how to upgrade. \ No newline at end of file diff --git a/docs/src/ReleaseNotes/0.14-to-0.15.md b/docs/src/ReleaseNotes/0.14-to-0.15.md new file mode 100644 index 0000000000..497188bdfa --- /dev/null +++ b/docs/src/ReleaseNotes/0.14-to-0.15.md @@ -0,0 +1,221 @@ +# Migration Guide: 0.13 to 0.14-alpha or 0.14 to 0.15 + +The most important changes to be aware of in this release are: + +- BMS now uses `Handle` as its principle means of referring to + scripts as opposed to the `ScriptId` that was used previously. + +- BMS exposes many more choices for deciding how scripts are associated with the + contexts in which they run. + +- `script_id` is replaced with `script_asset` which now has a type of `Handle`. + +## Handles + +The use of handles for scripts is perhaps the biggest user-facing change. It makes BMS asset usage follow Bevy's conventions. It requires less configuration. It is more idiomatic. + +### `ScriptComponent` Change + +In prior versions, `ScriptComponent` accepted a vector of `ScriptId`s, which was a type alias for `Cow<'static, str>`. +```diff +-pub struct ScriptComponent(pub Vec>); ++pub struct ScriptComponent(pub Vec>); +``` +Because `ScriptComponent` accepts handles, it is no longer necessary to store the handles somewhere to prevent the script from unloading. Nor is it necessary to configure an asset path to script id mapper. +```rust,ignore +ScriptComponent(vec![asset_server.load("foo.lua")]) +``` + +It is still beneficial to retain script assets in memory, for certain features to work. For example, +`script_asset` will not be able to retrieve the asset path of the script if it's not retained. + +### `ScriptId` Change + +The `ScriptId` has changed from being a string to being an asset ID. + +```diff +-type ScriptId = Cow<'static, str> ++type ScriptId = AssetId +``` + +### No Evaluation on Load + +In prior versions, BMS would immediately evaluate a `ScriptAsset` when it was loaded, and if multiple script assets were loaded, they would be evaluated in non-deterministic order. (See [issue #426](https://github.com/makspll/bevy_mod_scripting/issues/426).) Script assets no longer evaluate immediately. Script assets are only evaluated when their handles are added to a `ScriptComponent` or they are added to `StaticScripts`. + +In addition when a `ScriptComponent` loads its scripts, it loads them sequentially. + +### `ScriptAssetSettings` Removed + +The `ScriptAssetSettings` has been removed. Let us address each of its fields in turn. + +#### `script_id_mapper` + +See `AssetPathToScriptIdMapper` section. + +#### `extension_to_language_map` and `supported_extensions` +This is now represented by the `LanguageExtensions` resource, which can be configured directly during initialization +```rust,ignore +pub struct LanguageExtensions(HashMap<&'static str, Language>); +``` +or by the `ConfigureScriptAssetSettings` trait: +```rust,ignore +app.add_supported_script_extensions(&["p8lua"], Language::Lua); +``` +In addition one can configure the language of an asset when it is loaded: +```rust,ignore +asset_server.load_with_settings("hello.p8lua", |settings: &mut ScriptSettings| { + settings.language = Some(Language::Lua); +}); +``` +or when it is created: +```rust,ignore +let content = String::from("x = 0"); +let mut script = ScriptAsset::from(content); +script.language = Language::Lua; +``` +### `ScriptMetadata` and `ScriptMetadataStore` Removed + +These were present to associate the previous `ScriptId` with the asset ID and language. +That is no longer necessary as the `ScriptAsset` knows its own language. + +```rust,ignore +pub struct ScriptAsset { + /// The body of the script + pub content: Box<[u8]>, + /// The language of the script + pub language: Language, +} +``` +### `AssetPathToScriptIdMapper` Removed + +No mapper is necessary between a script and a script ID. If you have a script handle, you have its script ID + +```rust,ignore +let handle: Handle = ...; +let script_id: ScriptId = handle.id(); // ScriptId = AssetId +``` +and vice versa. + +```rust,ignore +let script_id: ScriptId = ...; +let handle: Handle = Handle::Weak(script_id); +``` +## Contexts +Choosing how scripts run is a big policy decision. Previously BMS had two options: +- Each script ran in its own context. +- All scripts ran in one context. + +This was controlled by the `enable_context_sharing()` method on +`ConfigureScriptPlugin`. That function is now deprecated. Instead use the `set_context_policy` method: + +```rust,ignore +app.add_plugins(LuaScriptingPlugin::default().set_context_policy( + ContextPolicy::shared(), +)); +``` +The reason for this change is there are many more choices than before, namely: + +- `ContextPolicy::shared()` +- `ContextPolicy::per_script()` +- `ContextPolicy::per_entity()` +- `ContextPolicy::per_entity_and_script()` + +See [Contexts](../Summary/Contexts.md) for more information. + +### `ContextKey` Added +Previously BMS used a `ScriptId` and sometimes an `Entity` to refer to a +context. If there was no entity then `Entity::from_raw(0)` was used. Instead BMS +now uses this `ContextKey` to look up contexts. + +```rust,ignore +pub struct ContextKey { + /// Entity if there is one. + pub entity: Option, + /// Script ID if there is one. + pub script_id: Option> +} +``` + +This change affects the parameters for the `context_pre_handling_initializers` +```diff +- context_pre_handling_initializers: vec![|script_id, entity, context| { ++ context_pre_handling_initializers: vec![|context_key, context| { +``` +and `context_initializers`: +```diff +- context_initializers: vec![|script_id, context| { ++ context_initializers: vec![|context_key, context| { +``` + +## `Recipients` Changes + +The `Recipients` enum now looks like this: + +```rust,ignore +#[derive(Clone, Debug)] +pub enum Recipients { + /// The event needs to be handled by all scripts, if multiple scripts share a context, the event will be sent once per script in the context. + AllScripts, + /// The event is to be handled by all unique contexts, i.e. if two scripts share the same context, the event will be sent only once per the context. + AllContexts, + /// The event is to be handled by a specific script-entity pair + ScriptEntity(ScriptId, Entity), + /// the event is to be handled by a specific static script + StaticScript(ScriptId), +} +``` + +This aligns with how scripts are associated with contexts better. + +## `ScriptCallbackEvent` changes + +The language for recipients is now controlled through the `language` field of the `ScriptCallbackEvent`. This allows for more flexibility in how callbacks are handled, especially when multiple languages are involved. + +If no language is specified, the callback will apply to all languages. + +```diff,ignore +#[derive(Clone, Event, Debug)] +#[non_exhaustive] +pub struct ScriptCallbackEvent { + /// The label of the callback + pub label: CallbackLabel, + /// The recipients of the callback + pub recipients: Recipients, + /// The language of the callback, if unspecified will apply to all languages ++ pub language: Option + /// The arguments to the callback + pub args: Vec, + /// Whether the callback should emit a response event + pub trigger_response: bool, +} +``` + +## Bindings Changes + +### ScriptId +`script_id` is replaced with `script_asset` which now has a type of `Handle`. This means that you can no longer use a string to refer to a script, but rather you must use a handle. + +Scripts can still access the asset path of the script using: +```lua,ignore +-- prints: "my_script.lua" +print(script_asset:asset_path()) +``` + +### ScriptAttachment +The concept of script attachments has been introduced to describe the idea of a script instance. Previously this was muddied due to the fact script ID's were the primary way to refer to scripts. Now the instance of a script and its mapping to a context is clearer. + +This is reflected in a brand new set of bindings: + +```lua,ignore +-- will create backing ScriptAttachment instances, that can be used in other bindings. +local entity_script = ScriptAttachment.new_entity_script(entity, script_asset) +local static_script = ScriptAttachment.new_static_script(script_asset) +``` + +### System Builder +the system builder no longer accepts script id's as parameters. Instead it accepts script attachments + +```diff,lua +-system_builder("my_callback", "this_script_id.lua") ++system_builder("my_callback", ScriptAttachment.new_static_script(script_asset)) +``` \ No newline at end of file diff --git a/docs/src/ReleaseNotes/0.15.0.md b/docs/src/ReleaseNotes/0.15.0.md new file mode 100644 index 0000000000..08f36a82d9 --- /dev/null +++ b/docs/src/ReleaseNotes/0.15.0.md @@ -0,0 +1,36 @@ +# 0.15.0 - Asset Handles and Context Policies + +This release focuses on aligning `bevy_mod_scripting` with modern Bevy practices, most notably by switching to `Handle` for script management. This change simplifies the API, removes boilerplate, and makes script handling more idiomatic. + +## Summary + +### Asset-First Workflow +Scripts are now treated as first-class Bevy assets. The old `ScriptId` (which was a string) has been replaced by `AssetId`, and you'll primarily interact with scripts via `Handle`. + +```rust,ignore +// New way +let handle: Handle = asset_server.load("my_script.lua"); +commands.spawn(ScriptComponent(vec![handle])); +``` + +Scripts are now only evaluated when they are attached to a `ScriptComponent` or added to `StaticScripts`, which means you have more control over when and how scripts are executed. + +### Flexible Context Policies +You now have much finer control over how script contexts (i.e., the environment a script runs in) are created. The old `enable_context_sharing()` has been replaced with `set_context_policy()` which accepts a `ContextPolicy`: + +- `ContextPolicy::shared()`: All scripts run in one global context +- `ContextPolicy::per_script()`: Each script asset gets its own context (the old default.) +- `ContextPolicy::per_entity()`: Each entity with scripts gets its own context. +- `ContextPolicy::per_entity_and_script()`: A unique context for every script on every entity (the new default). + +This means that each script is maximally isolated by default, but you can still opt for shared contexts if needed. + + +### Other Changes +- **`Recipients` Enum:** The `Recipients` enum for events has been redesigned to align with the new context policies, offering `AllScripts` and `AllContexts` variants, and removing some variants which don't fit the new model. If you need the old behaviour, you can simply query the ECS first before sending events. +- **API Cleanup:** Several types and traits were removed or simplified, including `ScriptAssetSettings`, `AssetPathToScriptIdMapper`, and `ScriptMetadataStore`, as they are no longer needed with the new asset-based approach. + +## Migration Guide +This release contains significant breaking changes. Please refer to the migration guide for detailed instructions on updating your project. + +- [Migration Guide: 0.14 to 0.15](https://makspll.github.io/bevy_mod_scripting/Migration/0.14-to-0.15.html) \ No newline at end of file diff --git a/docs/src/ReleaseNotes/guides.md b/docs/src/ReleaseNotes/guides.md new file mode 100644 index 0000000000..ad7e7a64f8 --- /dev/null +++ b/docs/src/ReleaseNotes/guides.md @@ -0,0 +1,5 @@ +# Release Notes + +This section contains migration guides and release notes relevant to the `bevy_mod_scripting` crate. + +This used to live in the repository, so for older release notes or guides, refer to the [GitHub repository](https://github.com/makspll/bevy_mod_scripting/blob/main/release-notes/) \ No newline at end of file diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index cb80e83102..357b3de6bb 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -8,11 +8,16 @@ - [Running Scripts](./Summary/running-scripts.md) - [Controlling Script Bindings](./Summary/controlling-script-bindings.md) - [Modifying Script Contexts](./Summary/customizing-script-contexts.md) -- [Shared Contexts](./Summary/sharing-contexts-between-scripts.md) -- [Script ID Mapping](./Summary/script-id-mapping.md) +- [Contexts](./Summary/contexts.md) - [Script Systems](./ScriptSystems/introduction.md) - [Examples](./Examples/introduction.md) +# Release Notes + +- [Release Notes](./ReleaseNotes/guides.md) + - [0.14-to-0.15](./ReleaseNotes/0.14-to-0.15.md) + - [0.15.0](./ReleaseNotes/0.15.0.md) + # Scripting Reference - [Introduction](./ScriptingReference/introduction.md) diff --git a/docs/src/Summary/contexts.md b/docs/src/Summary/contexts.md new file mode 100644 index 0000000000..274b93a12f --- /dev/null +++ b/docs/src/Summary/contexts.md @@ -0,0 +1,72 @@ +# Contexts + +Each script runs in a context. By default BMS will create an individual context, or sandbox, for each script-entity pair that is run. This means that each script-entity pair will have its own set of global variables and functions that are isolated from other scripts. However, sometimes this might not be desirable. If you are not worried about scripts interfering with each other, or if you want to easily share data between scripts, you can consider using a different context policy. + +## Context Policies + +### Shared Context +A shared context means that all scripts run in the same context; there is in fact only one context to run in. If the scripts interact heavily with each other, this may be what you want. + +To enable a shared context, set the corresponding context policy on the scripting plugin: +```rust,ignore +app.add_plugins(LuaScriptingPlugin::default().set_context_policy( + ContextPolicy::shared(), +));``` + +### Per Script Context +A per script context provides each script with their own context. However, scripts may be attached to multiple entities, in which case a single script context is shared by multiple entities. + +To enable per script contexts, insert the `ContextPolicy::per_script()` resource. +```rust,ignore +app.add_plugins(LuaScriptingPlugin::default().set_context_policy( + ContextPolicy::per_script(), +));``` + +### Per Entity Context +A per entity context provides each entity with their own context. The scripts attached to an entity via `ScriptComponent` all run in the same context. + +To enable per entity contexts, insert the `ContextPolicy::per_entity()` resource. +```rust,ignore +app.add_plugins(LuaScriptingPlugin::default().set_context_policy( + ContextPolicy::per_entity(), +));``` + +### Per Entity and Script Context +A per entity-and-script context provides each entity-script pair with their own context. This is a maximally isolated way to run scripts. + +To enable per entity-and-script contexts, insert the `ContextPolicy::per_entity_and_script()` resource. +```rust,ignore +app.add_plugins(LuaScriptingPlugin::default().set_context_policy( + ContextPolicy::per_entity_and_script(), +)); +``` + +## Custom Policies + +Here is another way to write the `per_script()` policy. +```rust,ignore +let policy_a = ContextPolicy::per_script(); +let policy_b = ContextPolicy { priorities: vec![ContextRule::Script, ContextRule::Shared] }; +assert_eq!(policy_a, policy_b); +``` +Reminding ourselves how `ContextKey` is defined, +```rust,ignore +pub struct ContextKey { + pub entity: Option, + pub script: Option>, +} +``` +we read `policy_b` like this: if `ContextKey` has a script, return a `ContextKey` with only a script. Failing that `ContextRule::Shared` always returns an empty `ContextKey`. + +One may also provide an entirely custom rule by implementing the `ContextKeySelector` trait. + +## Context Loading Settings + +All context loading settings are stored in a separate resource per scripting plugin namely: `ContextLoadingSettings`. + +The settings are as follows: +- `loader` - the load and unload strategy for contexts. Each scripting plugin will have a load and unload function which is hooked up through here +- `context_initializers` - stores all context initializers for the plugin +- `context_pre_handling_initializers` - stores all context pre-handling initializers for the plugin + +More advanced applications might want to customize these settings to suit their needs. diff --git a/docs/src/Summary/customizing-script-contexts.md b/docs/src/Summary/customizing-script-contexts.md index 071a7a72bc..93ebf368b2 100644 --- a/docs/src/Summary/customizing-script-contexts.md +++ b/docs/src/Summary/customizing-script-contexts.md @@ -12,7 +12,7 @@ For example, let's say you want to set a dynamic amount of globals in your scrip You could do this by customizing the scripting plugin: ```rust,ignore -let plugin = LuaScriptingPlugin::default().add_context_initializer(|script_id: &str, context: &mut Lua| { +let plugin = LuaScriptingPlugin::default().add_context_initializer(|_context_key: &ContextKey, context: &mut Lua| { let globals = context.globals(); for i in 0..10 { globals.set(i, i); @@ -28,21 +28,33 @@ The above will run every time the script is loaded or re-loaded and before it ha ## Context Pre Handling Initializers If you want to customize your context before every time it's about to handle events (and when it's loaded + reloaded), you can use `Context Pre Handling Initializers`: -```rust,ignore -let plugin = LuaScriptingPlugin::default().add_context_pre_handling_initializer(|script_id: &str, entity: Entity, context: &mut Lua| { - let globals = context.globals(); - globals.set("script_name", script_id.to_owned()); - Ok(()) -}); +```rust +use bevy::prelude::*; +use bevy_mod_scripting::prelude::*; +fn scripting_plugin(app: &mut App) { + app.add_plugins(LuaScriptingPlugin::default() + .add_context_pre_handling_initializer(|context_key: &ContextKey, entity: Entity, context: &mut Lua| { + let globals = context.globals(); + if let Some(script_id) = context_key.script_id.as_ref() { + globals.set("script_name", script_id.to_owned()); + } + Ok(()) + })); +} ``` ## Runtime Initializers Some scripting languages, have the concept of a `runtime`. This is a global object which is shared between all contexts. You can customize this object using `Runtime Initializers`: -```rust,ignore -let plugin = SomeScriptingPlugin::default().add_runtime_initializer(|runtime: &mut Runtime| { - runtime.set_max_stack_size(1000); - Ok(()) -}); +```rust +use bevy::prelude::*; +use bevy_mod_scripting::prelude::*; +fn scripting_plugin(app: &mut App) { + app.add_plugin(SomeScriptingPlugin::default().add_runtime_initializer(|runtime: &mut Runtime| { + runtime.set_max_stack_size(1000); + Ok(()) + })); + +} ``` In the case of Lua, the runtime type is `()` i.e. This is because `mlua` does not have a separate runtime concept. @@ -50,12 +62,16 @@ In the case of Lua, the runtime type is `()` i.e. This is because `mlua` does no ## Accessing the World in Initializers You can access the world in these initializers by using the thread local: `ThreadWorldContainer`: -```rust,ignore - -let plugin = LuaScriptingPlugin::default(); -plugin.add_context_initializer(|script_id: &str, context: &mut Lua| { - let world = ThreadWorldContainer.try_get_world().unwrap(); - world.with_resource::(|res| println!("My resource: {:?}", res)); - Ok(()) -}); -``` \ No newline at end of file +```rust +use bevy::prelude::*; +use bevy_mod_scripting::prelude::*; +fn scripting_plugin(app: &mut App) { + let plugin = LuaScriptingPlugin::default(); + plugin.add_context_initializer(|_context_key: &ContextKey, context: &mut Lua| { + let world = ThreadWorldContainer.try_get_world().unwrap(); + world.with_resource::(|res| println!("My resource: {:?}", res)); + Ok(()) + }); + app.add_plugins(plugin); +} +``` diff --git a/docs/src/Summary/managing-scripts.md b/docs/src/Summary/managing-scripts.md index e2f93aecbc..9916ce71b8 100644 --- a/docs/src/Summary/managing-scripts.md +++ b/docs/src/Summary/managing-scripts.md @@ -1,32 +1,64 @@ # Managing Scripts -Scripts live in the standard bevy `assets` directory. Loading a script means: -- Parsing the script body -- Creating or updating the resources which store script state -- Assigning a name/id to the script so it can be referred to by the rest of the application. +Scripts live in the standard Bevy `assets` directory. Loading a script means obtaining its byte representation and associated language. -## Loading -BMS listens to `ScriptAsset` events and reacts accordingly. In order to load a script, all you need to do is request a handle to it via the asset server and store it somewhere. - -Below is an example system which loads a script called `assets/my_script.lua` and stores the handle in a local system parameter: +Evaluating a script means: +- parsing the script body, +- and creating or updating the resources which store script state. +## Loading +Scripts can be loaded into memory via the `AssetServer`. ```rust,ignore -fn load_script(server: Res, mut handle: Local>) { - let handle_ = server.load::("my_script.lua"); - *handle = handle_; +let handle = asset_server.load::("my_script.lua"); +``` +Or scripts can be created in memory. +```rust,ignore +let mut script = ScriptAsset::from("x = 0".into()); +script.language = Language::Lua; +let handle = script_assets.add(script); +``` +This will not evaluate any code yet. + +## Evaluating +A script does not participate in any callbacks until it is evaluated, to evaluate a script you must first attach it to an entity, or to a static script entry. + +To evaluate a script, add it to a `ScriptComponent` or to `StaticScripts`. +### Load File via `AssetServer` +```rust +# extern crate bevy; +# extern crate bevy_mod_scripting; +# use bevy::prelude::*; +# use bevy_mod_scripting::prelude::*; + +fn load_script(asset_server: Res, mut commands: Commands) { + let handle = asset_server.load::("my_script.lua"); + commands.spawn(ScriptComponent(vec![handle])); +} +``` +### Create `ScriptAsset` and Add It +```rust +# extern crate bevy; +# extern crate bevy_mod_scripting; +# use bevy::{asset::Assets, prelude::*}; +# use bevy_mod_scripting::prelude::*; + +fn add_script(mut script_assets: ResMut>, mut commands: Commands) { + let content: String = "x = 0".into(); + let mut script = ScriptAsset::from(content); + script.language = Language::Lua; + let handle = script_assets.add(script); + commands.spawn(ScriptComponent(vec![handle])); } ``` -In practice you will likely store this handle in a resource or component, when your load all the scripts necessary for your application. - -## Unloading -Scripts are automatically unloaded when the asset is dropped. This means that if you have a handle to a script and it goes out of scope, the script will be unloaded. - +## Unloading +When you no longer need a script asset you can freely unload it, but the script attachment will persist. +In order to trigger the `on_script_unloaded` etc. callbacks, you need to remove the script from the `ScriptComponent` or `StaticScripts`. -This will delete references to the script and remove any internal handles to the asset. You will also need to clean up any handles to the asset you hold in your application in order for the asset to be unloaded. +When that happens a corresponding `ScriptEvent::Detached` will be dispatched, and then handled by a `DeleteScript` command. Once the last script in a context is removed, the context itself will also be removed. ## Hot-loading scripts -To enable hot-loading of assets, you need to enable the necessary bevy features as normal [see the bevy cheatbook for instructions](https://bevy-cheatbook.github.io/assets/hot-reload.html). +To enable hot-loading of assets, you need to enable the necessary Bevy features as normal [see the bevy cheatbook for instructions](https://bevy-cheatbook.github.io/assets/hot-reload.html). Assuming that hot-reloading is enabled for your app, any changes to script assets will automatically be picked up and the scripts re-loaded. @@ -35,18 +67,25 @@ Normally the set of supported extensions is pre-decided by each language plugin. I.e. Lua supports ".lua" extensions and Rhai supports ".rhai" extensions. -Scripts are mapped to the corresponding language plugin based on these and so it's important to use them correctly. +Scripts are mapped to the corresponding language plugin based on these and so it is important to use them correctly. -If you would like to add more extensions you need to populate them via `app.add_supported_script_extensions`. +If you would like to add more extensions, you need to populate them via `app.add_supported_script_extensions`. +```rust,ignore + app.add_supported_script_extensions(&[".pua"], Language::Lua); +``` ## Advanced -Normally not necessary, but knowing these exist could be useful for more advanced use cases. +Normally not necessary but knowing these exist could be useful for more advanced use cases. ### Manually (re)loading scripts In order to manually re-load or load a script you can issue the `CreateOrUpdateScript` command: ```rust,ignore -CreateOrUpdateScript::::new("my_script.lua".into(), "print(\"hello world from new script body\")".into(), asset_handle) +# use bevy::prelude::*; +# use bevy_mod_scripting::prelude::*; +let create_or_update = CreateOrUpdateScript::::new(script_handle) + .with_content("print(\"hello world from new script body\")"); +commands.queue(create_or_update); ``` replace `LuaScriptingPlugin` with the scripting plugin you are using. @@ -55,10 +94,12 @@ replace `LuaScriptingPlugin` with the scripting plugin you are using. In order to delete a previously loaded script, you will need to issue a `DeleteScript` command like so: ```rust,ignore -DeleteScript::::new("my_script.lua".into()) +commands.queue(DeleteScript::::new(script_handle)); ``` -replace `LuaScriptingPlugin` with the scripting plugin you are using. +Replace `LuaScriptingPlugin` with the scripting plugin you are using. ### Loading/Unloading timeframe -Scripts asset events are processed within the same frame they are issued. This means the moment an asset is loaded, it should be loaded and ready to use in the `Update` schedule. Similarly, the moment an asset is deleted, it should be unloaded and no longer usable in the `Update` schedule. \ No newline at end of file + +Script asset changes are processed together with bevy asset systems, in the `Last` schedule. +These are converted to `ScriptEvent`'s which are handled right after via the `ScriptingSystemSet::ScriptingCommandDispatch` system set. diff --git a/docs/src/Summary/running-scripts.md b/docs/src/Summary/running-scripts.md index 239ab42ef0..fd4caab145 100644 --- a/docs/src/Summary/running-scripts.md +++ b/docs/src/Summary/running-scripts.md @@ -14,7 +14,7 @@ And then sending script event's which trigger callbacks on the scripts. In order to attach a script and make it runnable simply add a `ScriptComponent` to an entity ```rust,ignore - commands.entity(my_entity).insert(ScriptComponent::new(vec!["my_script.lua", "my_other_script.lua"])); + commands.entity(my_entity).insert(ScriptComponent::new(vec![my_script_handle, another_script_handle])); ``` When this script is run the `entity` global will represent the entity the script is attached to. This allows you to interact with the entity in your script easilly. @@ -28,7 +28,7 @@ Be wary of path separators, by default script ID's are derived from asset paths, Some scripts do not require attaching to an entity. You can run these scripts by loading them first as you would with any other script, then either adding them at app level via `add_static_script` or by issuing a `AddStaticScript` command like so: ```rust,ignore - commands.queue(AddStaticScript::new("my_static_script.lua")); + commands.queue(AddStaticScript::new(my_script_handle)); ``` The script will then be run as any other script but without being attached to any entity. and as such the `entity` global will always represent an invalid entity. @@ -64,7 +64,7 @@ fn send_event(mut writer: EventWriter, mut allocator: ResMu let allocator = allocator.write(); let my_reflect_payload = ReflectReference::new_allocated(MyReflectType, &mut allocator); - writer.send(ScriptCallbackEvent::new_for_all( + writer.send(ScriptCallbackEvent::new_for_all_scripts( OnEvent, vec![my_reflect_payload.into()], )); @@ -90,3 +90,6 @@ The event handler will catch all events with the label `OnEvent` and trigger the In order to handle events in the same frame and not accidentally have events "spill over" into the next frame, you should make sure to order any systems which produce these events *before* the event handler systems. +# Commands + +You can also use manually issued `RunScriptCallback` commands to trigger script callbacks as well. These must be run from a exclusive system, or via a any other system but with limited access to the world (See the `WithWorldGuard` system param, which will allow you to create a `WorldGuard` and use it to run the commands) \ No newline at end of file diff --git a/docs/src/Summary/script-id-mapping.md b/docs/src/Summary/script-id-mapping.md deleted file mode 100644 index fe8027b0f7..0000000000 --- a/docs/src/Summary/script-id-mapping.md +++ /dev/null @@ -1,11 +0,0 @@ -# Script ID mapping - -Every script is currently identified by a unique ID. - -ID's are derived from the script asset path for scripts loaded via the asset system. - -By default this is an identity mapping, but you can override this by modifying the `AssetPathToScriptIdMapper` inside the `ScriptAssetSettings` resource before loading the script. - -

\ No newline at end of file diff --git a/docs/src/Summary/sharing-contexts-between-scripts.md b/docs/src/Summary/sharing-contexts-between-scripts.md deleted file mode 100644 index e0fb9951ba..0000000000 --- a/docs/src/Summary/sharing-contexts-between-scripts.md +++ /dev/null @@ -1,24 +0,0 @@ -# Shared Contexts - -By default BMS will create an individual script context, or sandbox, for each script that is run. This means that each script will have its own set of global variables and functions that are isolated from other scripts. However, sometimes this might not be desirable, if you aren't worried about scripts interfering with each other, or if you want to easilly share data between scripts. In these cases, you can use shared contexts. - -## Enabling Shared Contexts - -You can enable shared contexts by configuring the relevant scripting plugin like so: -```rust,ignore -let mut plugin = LuaScriptingPlugin::default().enable_context_sharing(); - -app.add_plugins(plugin); -``` - -## Context Loading Settings - -All context loading settings are stored in a separate resource per scripting plugin namely: `ContextLoadingSettings`. - -The settings are as follows: -- `loader` - the load and unload strategy for contexts. Each scripting plugin will have a load and unload function which is hooked up through here -- `assigner` - the strategy for assigning/unassigning contexts to scripts. This is used to determine how to assign a context to a script when it is run, and what to do with the context when the script is finished. -- `context_initializers` - stores all context initializers for the plugin -- `context_pre_handling_initializers` - stores all context pre-handling initializers for the plugin - -More advanced applications might want to customize these settings to suit their needs. \ No newline at end of file diff --git a/examples/docgen.rs b/examples/docgen.rs index 0a85736ee2..27a1932e13 100644 --- a/examples/docgen.rs +++ b/examples/docgen.rs @@ -4,6 +4,7 @@ use bevy_mod_scripting::ScriptFunctionsPlugin; use bevy_mod_scripting_core::bindings::function::script_function::AppScriptFunctionRegistry; use bevy_mod_scripting_core::bindings::globals::core::CoreScriptGlobalsPlugin; use bevy_mod_scripting_core::bindings::globals::AppScriptGlobalsRegistry; +use bevy_mod_scripting_core::BMSScriptingInfrastructurePlugin; use ladfile_builder::plugin::{generate_lad_file, LadFileSettings, ScriptingDocgenPlugin}; fn main() -> std::io::Result<()> { @@ -18,6 +19,7 @@ fn main() -> std::io::Result<()> { // the definitions by themselves CoreScriptGlobalsPlugin::default(), ScriptFunctionsPlugin, + BMSScriptingInfrastructurePlugin, )); // there are two ways to generate the ladfile diff --git a/examples/game_of_life.rs b/examples/game_of_life.rs index a65b907596..0f87bae6ae 100644 --- a/examples/game_of_life.rs +++ b/examples/game_of_life.rs @@ -15,22 +15,7 @@ use bevy::{ window::{PrimaryWindow, WindowResized}, }; use bevy_console::{make_layer, AddConsoleCommand, ConsoleCommand, ConsoleOpen, ConsolePlugin}; -use bevy_mod_scripting::{BMSPlugin, ScriptFunctionsPlugin}; -use bevy_mod_scripting_core::{ - asset::ScriptAsset, - bindings::{ - function::namespace::{GlobalNamespace, NamespaceBuilder}, - script_value::ScriptValue, - AllocatorDiagnosticPlugin, CoreScriptGlobalsPlugin, - }, - callback_labels, - commands::AddStaticScript, - event::ScriptCallbackEvent, - handler::event_handler, - script::ScriptComponent, -}; -use bevy_mod_scripting_lua::LuaScriptingPlugin; -use bevy_mod_scripting_rhai::RhaiScriptingPlugin; +use bevy_mod_scripting::{core::bindings::AllocatorDiagnosticPlugin, prelude::*}; use clap::Parser; // CONSOLE SETUP @@ -54,7 +39,10 @@ fn console_app(app: &mut App) -> &mut App { fn run_script_cmd( mut log: ConsoleCommand, mut commands: Commands, - mut loaded_scripts: ResMut, + asset_server: Res, + script_comps: Query<(Entity, &ScriptComponent)>, + mut static_lua_scripts: Local>, + mut static_rhai_scripts: Local>, ) { if let Some(Ok(command)) = log.take() { match command { @@ -63,32 +51,61 @@ fn run_script_cmd( use_static_script, } => { // create an entity with the script component - bevy::log::info!( - "Starting game of life spawning entity with the game_of_life.{} script", - language - ); + bevy::log::info!("Using game of life script game_of_life.{}", language); let script_path = format!("scripts/game_of_life.{}", language); if !use_static_script { bevy::log::info!("Spawning an entity with ScriptComponent"); - commands.spawn(ScriptComponent::new(vec![script_path])); + commands.spawn(ScriptComponent::new(vec![asset_server.load(script_path)])); } else { bevy::log::info!("Using static script instead of spawning an entity"); - commands.queue(AddStaticScript::new(script_path)) + let handle = asset_server.load(script_path); + if language == "lua" { + static_lua_scripts.push(handle.id()); + } else { + static_rhai_scripts.push(handle.id()); + } + commands.queue(AddStaticScript::new(handle)) } } GameOfLifeCommand::Stop => { // we can simply drop the handle, or manually delete, I'll just drop the handle bevy::log::info!("Stopping game of life by dropping the handles to all scripts"); + for (id, script_component) in &script_comps { + for script in &script_component.0 { + match script + .path() + .and_then(|p| p.get_full_extension()) + .unwrap_or_default() + .as_str() + { + "lua" => { + commands + .entity(id) + .queue(DeleteScript::::new(script.id())); + } + "rhai" => { + #[cfg(feature = "rhai")] + commands + .entity(id) + .queue(DeleteScript::::new(script.id())); + } + ext => { + warn!("Can't delete script with extension {ext:?}."); + } + } + } + commands.entity(id).despawn(); + } - // I am not mapping the handles to the script names, so I'll just clear the entire list - loaded_scripts.0.clear(); + for script in static_lua_scripts.drain(..) { + commands.queue(DeleteScript::::new(script)); + } - // you could also do - // commands.queue(DeleteScript::::new( - // "scripts/game_of_life.lua".into(), - // )); - // as this will retain your script asset and handle + #[cfg(feature = "rhai")] + for script in static_rhai_scripts.drain(..) { + commands.queue(DeleteScript::::new(script)); + } } } } @@ -115,12 +132,12 @@ pub enum GameOfLifeCommand { // ------------- GAME OF LIFE fn game_of_life_app(app: &mut App) -> &mut App { app.insert_resource(Time::::from_seconds(UPDATE_FREQUENCY.into())) + // .add_plugins(BMSPlugin.set(LuaScriptingPlugin::default().enable_context_sharing())) .add_plugins(BMSPlugin) .register_type::() .register_type::() .init_resource::() - .init_resource::() - .add_systems(Startup, (init_game_of_life_state, load_script_assets)) + .add_systems(Startup, init_game_of_life_state) .add_systems(Update, (sync_window_size, send_on_click)) .add_systems( FixedUpdate, @@ -129,8 +146,10 @@ fn game_of_life_app(app: &mut App) -> &mut App { send_on_update.after(update_rendered_state), ( event_handler::, + #[cfg(feature = "rhai")] event_handler::, event_handler::, + #[cfg(feature = "rhai")] event_handler::, ) .after(send_on_update), @@ -145,9 +164,6 @@ pub struct LifeState { pub cells: Vec, } -#[derive(Debug, Resource, Default)] -pub struct LoadedScripts(pub Vec>); - #[derive(Reflect, Resource)] #[reflect(Resource)] pub struct Settings { @@ -170,17 +186,6 @@ impl Default for Settings { } } -/// Prepares any scripts by loading them and storing the handles. -pub fn load_script_assets( - asset_server: Res, - mut loaded_scripts: ResMut, -) { - loaded_scripts.0.extend(vec![ - asset_server.load("scripts/game_of_life.lua"), - asset_server.load("scripts/game_of_life.rhai"), - ]); -} - pub fn register_script_functions(app: &mut App) -> &mut App { let world = app.world_mut(); NamespaceBuilder::::new_unregistered(world) @@ -291,7 +296,7 @@ callback_labels!( /// Sends events allowing scripts to drive update logic pub fn send_on_update(mut events: EventWriter) { - events.send(ScriptCallbackEvent::new_for_all(OnUpdate, vec![])); + events.send(ScriptCallbackEvent::new_for_all_scripts(OnUpdate, vec![])); } pub fn send_on_click( @@ -304,7 +309,7 @@ pub fn send_on_click( let pos = window.unwrap().cursor_position().unwrap_or_default(); let x = pos.x as u32; let y = pos.y as u32; - events.send(ScriptCallbackEvent::new_for_all( + events.send(ScriptCallbackEvent::new_for_all_scripts( OnClick, vec![ ScriptValue::Integer(x as i64), diff --git a/examples/run-script.rs b/examples/run-script.rs new file mode 100644 index 0000000000..63098a69f6 --- /dev/null +++ b/examples/run-script.rs @@ -0,0 +1,66 @@ +use bevy::prelude::*; +use bevy_mod_scripting::prelude::*; + +fn main() { + // Collect command-line arguments, skipping the program name. + let mut args: Vec = std::env::args().skip(1).collect(); + + if args.is_empty() { + eprintln!("Usage: run-script ..."); + std::process::exit(1); + } + + App::new() + .add_plugins(DefaultPlugins) + .add_plugins(BMSPlugin) + .add_plugins(add_logging) + .add_systems( + Startup, + move |asset_server: Res, mut commands: Commands| { + let mut handles = vec![]; + for file_name in args.drain(..) { + handles.push(asset_server.load::(file_name)); + } + commands.spawn(ScriptComponent(handles)); + }, + ) + .add_systems(Update, info_on_asset_event::()) + .run(); +} + +fn add_logging(app: &mut App) { + let world = app.world_mut(); + NamespaceBuilder::::new_unregistered(world) + .register("info", |s: String| { + bevy::log::info!("{}", s); + }) + .register("warn", |s: String| { + bevy::log::warn!("{}", s); + }) + .register("error", |s: String| { + bevy::log::error!("{}", s); + }) + .register("debug", |s: String| { + bevy::log::debug!("{}", s); + }) + .register("trace", |s: String| { + bevy::log::trace!("{}", s); + }); +} + +pub fn info_on_asset_event() -> impl FnMut(EventReader>) { + // The events need to be consumed, so that there are no false positives on subsequent + // calls of the run condition. Simply checking `is_empty` would not be enough. + // PERF: note that `count` is efficient (not actually looping/iterating), + // due to Bevy having a specialized implementation for events. + move |mut reader: EventReader>| { + for event in reader.read() { + match event { + AssetEvent::Modified { .. } => (), + _ => { + info!("ASSET EVENT {:?}", &event); + } + } + } + } +} diff --git a/src/lib.rs b/src/lib.rs index a880540a76..421a3bdb81 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,8 @@ pub mod core { pub use bevy_mod_scripting_core::*; } +pub mod prelude; + #[cfg(feature = "lua")] pub mod lua { pub use bevy_mod_scripting_lua::*; diff --git a/src/prelude.rs b/src/prelude.rs new file mode 100644 index 0000000000..a9eb6990c3 --- /dev/null +++ b/src/prelude.rs @@ -0,0 +1,20 @@ +pub use crate::{BMSPlugin, ScriptFunctionsPlugin}; +pub use bevy_mod_scripting_core::{ + asset::{Language, ScriptAsset}, + bindings::{ + function::namespace::{GlobalNamespace, NamespaceBuilder}, + script_value::ScriptValue, + CoreScriptGlobalsPlugin, + }, + callback_labels, + commands::{AddStaticScript, DeleteScript}, + event::ScriptCallbackEvent, + handler::event_handler, + script::{ScriptComponent, ScriptId}, + ConfigureScriptAssetSettings, ConfigureScriptPlugin, IntoScriptPluginParams, +}; + +#[cfg(feature = "lua")] +pub use bevy_mod_scripting_lua::LuaScriptingPlugin; +#[cfg(feature = "rhai")] +pub use bevy_mod_scripting_rhai::RhaiScriptingPlugin; diff --git a/tests/script_tests.rs b/tests/script_tests.rs index cbb451abd6..039a965dee 100644 --- a/tests/script_tests.rs +++ b/tests/script_tests.rs @@ -3,11 +3,9 @@ use std::path::PathBuf; use libtest_mimic::{Arguments, Failed, Trial}; -use script_integration_test_harness::{ - execute_lua_integration_test, execute_rhai_integration_test, -}; +use script_integration_test_harness::{execute_integration_test, scenario::Scenario}; -use test_utils::{discover_all_tests, Test, TestKind}; +use test_utils::{discover_all_tests, Test}; trait TestExecutor { fn execute(self) -> Result<(), Failed>; @@ -16,12 +14,20 @@ trait TestExecutor { impl TestExecutor for Test { fn execute(self) -> Result<(), Failed> { - println!("Running test: {:?}", self.path); + let script_asset_path = self.script_asset_path; + let scenario_path = self.scenario_path.ok_or_else(|| { + Failed::from("Test does not have a scenario.txt file near to use for test".to_string()) + })?; + println!( + "Running test: {}, with scenario: {}", + script_asset_path.display(), + scenario_path.display() + ); - match self.kind { - TestKind::Lua => execute_lua_integration_test(&self.path.to_string_lossy())?, - TestKind::Rhai => execute_rhai_integration_test(&self.path.to_string_lossy())?, - } + let scenario = Scenario::from_scenario_file(&script_asset_path, &scenario_path) + .map_err(|e| format!("{e:?}"))?; // print whole error from anyhow including source and backtrace + + execute_integration_test(scenario)?; Ok(()) } @@ -30,7 +36,7 @@ impl TestExecutor for Test { format!( "script_test - {} - {}", self.kind, - self.path + self.script_asset_path .to_string_lossy() .split(&format!("tests{}data", std::path::MAIN_SEPARATOR)) .last() @@ -46,7 +52,7 @@ fn main() { let args = Arguments::from_args(); let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - let tests = discover_all_tests(manifest_dir, |p| p.path.starts_with("tests")) + let tests = discover_all_tests(manifest_dir, |p| p.script_asset_path.starts_with("tests")) .into_iter() .map(|t| Trial::test(t.name(), move || t.execute())) .collect::>();