Skip to content

feat(bms,ladfile_builder): introduce app global instance registry and export them in ladfile_builder #340

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Mar 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 24 additions & 15 deletions .github/workflows/bevy_mod_scripting.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,20 +85,20 @@ jobs:
matrix:
run_args: ${{fromJson(needs.generate-job-matrix.outputs.matrix)}}
steps:
- name: Free Disk Space (Ubuntu)
if: runner.os == 'Linux'
uses: jlumbroso/free-disk-space@main
with:
tool-cache: false
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: true
swap-storage: true
# - if: runner.os == 'linux'
# run: |
# sudo rm -rf /usr/share/dotnet; sudo rm -rf /opt/ghc; sudo rm -rf "/usr/local/share/boost"; sudo rm -rf "$AGENT_TOOLSDIRECTORY"
# - name: Free Disk Space (Ubuntu)
# if: runner.os == 'Linux'
# uses: jlumbroso/free-disk-space@main
# with:
# tool-cache: false
# android: true
# dotnet: true
# haskell: true
# large-packages: true
# docker-images: true
# swap-storage: true
# # - if: runner.os == 'linux'
# # run: |
# # sudo rm -rf /usr/share/dotnet; sudo rm -rf /opt/ghc; sudo rm -rf "/usr/local/share/boost"; sudo rm -rf "$AGENT_TOOLSDIRECTORY"
- name: Checkout
if: ${{ needs.check-needs-run.outputs.any-changes == 'true' }}
uses: actions/checkout@v4
Expand All @@ -120,10 +120,19 @@ jobs:
run: |
cargo xtask init

- name: Setup GPU Drivers
if: ${{ needs.check-needs-run.outputs.any-changes == 'true' && matrix.run_args.requires_gpu }}
run: |
sudo add-apt-repository ppa:kisak/turtle -y
sudo apt-get install --no-install-recommends libxkbcommon-x11-0 xvfb libgl1-mesa-dri libxcb-xfixes0-dev mesa-vulkan-drivers
- name: Check
if: ${{ needs.check-needs-run.outputs.any-changes == 'true' }}
if: ${{ needs.check-needs-run.outputs.any-changes == 'true' && !matrix.run_args.requires_gpu }}
run: |
${{ matrix.run_args.command }}
- name: Check With virtual X11 server
if: ${{ needs.check-needs-run.outputs.any-changes == 'true' && matrix.run_args.requires_gpu }}
run: |
xvfb-run ${{ matrix.run_args.command }}

- name: Upload coverage artifact
if: ${{ needs.check-needs-run.outputs.any-changes == 'true' && matrix.run_args.generates_coverage }}
Expand Down
40 changes: 40 additions & 0 deletions crates/bevy_mod_scripting_core/src/bindings/globals/core.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//! Core globals exposed by the BMS framework

use bevy::{app::Plugin, ecs::reflect::AppTypeRegistry};

use super::AppScriptGlobalsRegistry;

/// A plugin introducing core globals for the BMS framework
pub struct CoreScriptGlobalsPlugin;

impl Plugin for CoreScriptGlobalsPlugin {
fn build(&self, app: &mut bevy::app::App) {
let global_registry = app
.world_mut()
.get_resource_or_init::<AppScriptGlobalsRegistry>()
.clone();
let type_registry = app
.world_mut()
.get_resource_or_init::<AppTypeRegistry>()
.clone();
let mut global_registry = global_registry.write();
let type_registry = type_registry.read();

// find all reflectable types without generics
for registration in type_registry.iter() {
if !registration.type_info().generics().is_empty() {
continue;
}

if let Some(global_name) = registration.type_info().type_path_table().ident() {
let documentation = "A reference to the type, allowing you to call static methods.";
global_registry.register_static_documented_dynamic(
registration.type_id(),
None,
global_name.into(),
documentation.into(),
)
}
}
}
}
266 changes: 266 additions & 0 deletions crates/bevy_mod_scripting_core/src/bindings/globals/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
//! Contains abstractions for exposing "globals" to scripts, in a language-agnostic way.

use super::{
function::arg_meta::{ScriptReturn, TypedScriptReturn},
script_value::ScriptValue,
WorldGuard,
};
use crate::{docgen::typed_through::ThroughTypeInfo, error::InteropError};
use bevy::{ecs::system::Resource, utils::hashbrown::HashMap};
use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use std::{any::TypeId, borrow::Cow, sync::Arc};

pub mod core;

/// A send + sync wrapper around the [`ScriptGlobalsRegistry`].
#[derive(Default, Resource, Clone)]
pub struct AppScriptGlobalsRegistry(Arc<RwLock<ScriptGlobalsRegistry>>);

impl AppScriptGlobalsRegistry {
/// Returns a reference to the inner [`ScriptGlobalsRegistry`].
pub fn read(&self) -> RwLockReadGuard<ScriptGlobalsRegistry> {
self.0.read()
}

/// Returns a mutable reference to the inner [`ScriptGlobalsRegistry`].
pub fn write(&self) -> RwLockWriteGuard<ScriptGlobalsRegistry> {
self.0.write()
}
}

/// A function that creates a global variable.
pub type ScriptGlobalMakerFn<T> =
dyn Fn(WorldGuard) -> Result<T, InteropError> + 'static + Send + Sync;

/// A global variable that can be exposed to scripts.
pub struct ScriptGlobal {
/// The function that creates the global variable.
/// if not present, this is assumed to be a static global, one that
/// cannot be instantiated, but carries type information.
pub maker: Option<Arc<ScriptGlobalMakerFn<ScriptValue>>>,
/// The documentation for the global variable.
pub documentation: Option<Cow<'static, str>>,
/// The type ID of the global variable.
pub type_id: TypeId,
/// Rich type information the global variable.
pub type_information: Option<ThroughTypeInfo>,
}

/// A registry of global variables that can be exposed to scripts.
#[derive(Default)]
pub struct ScriptGlobalsRegistry {
globals: HashMap<Cow<'static, str>, ScriptGlobal>,
}

impl ScriptGlobalsRegistry {
/// Gets the global with the given name
pub fn get(&self, name: &str) -> Option<&ScriptGlobal> {
self.globals.get(name)
}

/// Gets the global with the given name mutably
pub fn get_mut(&mut self, name: &str) -> Option<&mut ScriptGlobal> {
self.globals.get_mut(name)
}

/// Counts the number of globals in the registry
pub fn len(&self) -> usize {
self.globals.len()
}

/// Checks if the registry is empty
pub fn is_empty(&self) -> bool {
self.len() == 0
}

/// Iterates over the globals in the registry
pub fn iter(&self) -> impl Iterator<Item = (&Cow<'static, str>, &ScriptGlobal)> {
self.globals.iter()
}

/// Iterates over the globals in the registry mutably
pub fn iter_mut(&mut self) -> impl Iterator<Item = (&Cow<'static, str>, &mut ScriptGlobal)> {
self.globals.iter_mut()
}

fn type_erase_maker<
T: ScriptReturn,
F: Fn(WorldGuard) -> Result<T, InteropError> + Send + Sync + 'static,
>(
maker: F,
) -> Arc<ScriptGlobalMakerFn<ScriptValue>> {
Arc::new(move |world| T::into_script(maker(world.clone())?, world))
}

/// Inserts a global into the registry, returns the previous value if it existed
pub fn register<
T: ScriptReturn + 'static,
F: Fn(WorldGuard) -> Result<T, InteropError> + 'static + Send + Sync,
>(
&mut self,
name: Cow<'static, str>,
maker: F,
) -> Option<ScriptGlobal> {
self.globals.insert(
name,
ScriptGlobal {
maker: Some(Self::type_erase_maker(maker)),
documentation: None,
type_id: TypeId::of::<T>(),
type_information: None,
},
)
}

/// 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.
pub fn register_documented<
T: TypedScriptReturn + 'static,
F: Fn(WorldGuard) -> Result<T, InteropError> + 'static + Send + Sync,
>(
&mut self,
name: Cow<'static, str>,
maker: F,
documentation: Cow<'static, str>,
) -> Option<ScriptGlobal> {
self.globals.insert(
name,
ScriptGlobal {
maker: Some(Self::type_erase_maker(maker)),
documentation: Some(documentation),
type_id: TypeId::of::<T>(),
type_information: Some(T::through_type_info()),
},
)
}

/// Registers a static global into the registry.
pub fn register_static<T: 'static>(&mut self, name: Cow<'static, str>) {
self.globals.insert(
name,
ScriptGlobal {
maker: None,
documentation: None,
type_id: TypeId::of::<T>(),
type_information: None,
},
);
}

/// Registers a static global into the registry.
///
/// This is a version of [`Self::register_static`] which stores rich type information regarding the global.
pub fn register_static_documented<T: TypedScriptReturn + 'static>(
&mut self,
name: Cow<'static, str>,
documentation: Cow<'static, str>,
) {
self.globals.insert(
name,
ScriptGlobal {
maker: None,
documentation: Some(documentation),
type_id: TypeId::of::<T>(),
type_information: Some(T::through_type_info()),
},
);
}

/// Registers a static global into the registry.
///
/// This is a version of [`Self::register_static_documented`] which does not require compile time type knowledge.
pub fn register_static_documented_dynamic(
&mut self,
type_id: TypeId,
type_information: Option<ThroughTypeInfo>,
name: Cow<'static, str>,
documentation: Cow<'static, str>,
) {
self.globals.insert(
name,
ScriptGlobal {
maker: None,
documentation: Some(documentation),
type_id,
type_information,
},
);
}
}

#[cfg(test)]
mod test {
use bevy::ecs::world::World;

use super::*;

#[test]
fn test_script_globals_registry() {
let mut registry = ScriptGlobalsRegistry::default();

let maker = |_: WorldGuard| Ok(ScriptValue::from(42));
let maker2 = |_: WorldGuard| Ok(ScriptValue::from(43));

assert_eq!(registry.len(), 0);
assert!(registry.is_empty());

assert!(registry.register(Cow::Borrowed("foo"), maker).is_none());
assert_eq!(registry.len(), 1);

assert_eq!(
(registry.get("foo").unwrap().maker.clone().unwrap())(WorldGuard::new(
&mut World::new()
))
.unwrap(),
ScriptValue::from(42)
);

assert!(registry.register(Cow::Borrowed("foo"), maker2).is_some());
assert_eq!(registry.len(), 1);

assert_eq!(
(registry.get("foo").unwrap().maker.clone().unwrap())(WorldGuard::new(
&mut World::new()
))
.unwrap(),
ScriptValue::from(43)
);
}

#[test]
fn test_documentation_is_stored() {
let mut registry = ScriptGlobalsRegistry::default();

let maker = |_: WorldGuard| Ok(ScriptValue::from(42));

assert!(registry
.register_documented(Cow::Borrowed("foo"), maker, Cow::Borrowed("This is a test"))
.is_none());

let global = registry.get("foo").unwrap();
assert_eq!(global.documentation.as_deref(), Some("This is a test"));
}

#[test]
fn test_static_globals() {
let mut registry = ScriptGlobalsRegistry::default();

registry.register_static::<i32>(Cow::Borrowed("foo"));

let global = registry.get("foo").unwrap();
assert!(global.maker.is_none());
assert_eq!(global.type_id, TypeId::of::<i32>());

// the same but documented
registry.register_static_documented::<i32>(
Cow::Borrowed("bar"),
Cow::Borrowed("This is a test"),
);

let global = registry.get("bar").unwrap();
assert!(global.maker.is_none());
assert_eq!(global.type_id, TypeId::of::<i32>());
assert_eq!(global.documentation.as_deref(), Some("This is a test"));
}
}
1 change: 1 addition & 0 deletions crates/bevy_mod_scripting_core/src/bindings/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
pub mod access_map;
pub mod allocator;
pub mod function;
pub mod globals;
pub mod pretty_print;
pub mod query;
pub mod reference;
Expand Down
Loading
Loading