tirbofish/dropbear · commit
35dbc3bc0532faf94bb353f5e6037c36e68611cd
im assuming it still works?
Signature present but could not be verified.
Unverified
@@ -40,14 +40,11 @@ open = "5" parking_lot = {version = "0.12", features = ["deadlock_detection"] } pollster = "0.4" rfd = "0.15.4" -rhai = "1.22" ron = "0.10.1" russimp-ng = { version = "3.2.4", features = ["static-link"] } rustyscript = { version = "0.12" } serde = { version = "1.0.219", features = ["derive"] } spin_sleep = "1.3" -specta = { version = "=2.0.0-rc.22", features = ["glam", "derive", "export"] } -specta-typescript = "0.0.9" tokio = { version = "1", features = ["full"] } transform-gizmo-egui = { git = "https://github.com/4tkbytes/transform-gizmo"} wgpu = "26" @@ -60,13 +57,6 @@ version = "0.25" default-features = false features = ["png"] -[patch.crates-io] -dropbear-engine = { path = "dropbear-engine" } -eucalyptus = { path = "eucalyptus" } -# fixing android compilation -russimp-sys-ng = { git = "https://github.com/Kek5chen/russimp-sys-ng", branch = "fix-android-compilation" } - - [profile.dev] opt-level = 0 debug = true @@ -23,7 +23,6 @@ gilrs.workspace = true glam.workspace = true log.workspace = true log-once.workspace = true -#rfd.workspace = true russimp-ng.workspace = true pollster.workspace = true serde.workspace = true @@ -34,7 +33,6 @@ hecs.workspace = true once_cell.workspace = true parking_lot.workspace = true lazy_static.workspace = true -specta.workspace = true [target.'cfg(not(target_os = "android"))'.dependencies] rfd.workspace = true @@ -31,7 +31,6 @@ model_to_image = { workspace = true, optional = true } once_cell = { workspace = true, optional = true } open = { workspace = true, optional = true } parking_lot = { workspace = true, optional = true } -rhai = { workspace = true, optional = true } ron = { workspace = true, optional = true } serde = { workspace = true, optional = true } tokio = { workspace = true, optional = true } @@ -44,8 +43,6 @@ zip = { workspace = true, optional = true } bytemuck = "1.23.2" rustyscript.workspace = true -specta.workspace = true -specta-typescript.workspace = true [target.'cfg(not(target_os = "android"))'.dependencies] rfd = { workspace = true, optional = true } @@ -73,7 +70,6 @@ editor = [ "open", "parking_lot", "rfd", - "rhai", "ron", "serde", "tokio", @@ -94,7 +90,6 @@ data-only = [ "glam", "log", "dropbear-engine", - "rhai", "winit", "hecs", "ron", @@ -55,6 +55,7 @@ fn main() -> anyhow::Result<()> { )); } + println!("cargo:rustc-link-arg=/NODEFAULTLIB:libcmt.lib"); println!("cargo:rerun-if-changed=build.rs"); Ok(()) } @@ -0,0 +1,257 @@ +// Modules for the dropbear-engine scripting component +// Made by 4tkbytes +// EDIT THIS IF YOU WISH, MOST LIKELY + +type TransformT = any; +type Vec3T = any; +type QuatT = any; +type InputStateT = any; + +/** + * A placeholder function that returns the value of the host function + */ +function hostFn(name: string) { + const fn = (globalThis as any)[name]; + if (typeof fn !== "function") { + throw new Error(`Host function ${name}() is not available. Make sure the runtime exposes it.`); + } + return fn; +} + +const Transform = { + create: (): TransformT => hostFn("createTransform")(), + translate: (transform: TransformT, translation: [number, number, number] | number[]): TransformT => + hostFn("transformTranslate")(transform, translation), + rotateX: (transform: TransformT, angle: number): TransformT => + hostFn("transformRotateX")(transform, angle), + rotateY: (transform: TransformT, angle: number): TransformT => + hostFn("transformRotateY")(transform, angle), + rotateZ: (transform: TransformT, angle: number): TransformT => + hostFn("transformRotateZ")(transform, angle), + scale: (transform: TransformT, scale: number | [number, number, number]): TransformT => + hostFn("transformScale")(transform, scale), + matrix: (transform: TransformT): TransformT => hostFn("transformMatrix")(transform), +}; + +/** + * A Vector of 3 components: an X, Y and Z. + */ +const Vec3 = { + new: (x = 0, y = 0, z = 0): Vec3T => hostFn("createVec3")(x, y, z), + zero: (): Vec3T => hostFn("createVec3")(0, 0, 0), + one: (): Vec3T => hostFn("createVec3")(1, 1, 1), +}; + +const Quaternion = { + identity: (): QuatT => hostFn("createQuatIdentity")(), + fromEuler: (x: number, y: number, z: number): QuatT => hostFn("createQuatFromEuler")(x, y, z), +}; + +export const Keys = { + // Letters + KeyA: "KeyA", KeyB: "KeyB", KeyC: "KeyC", KeyD: "KeyD", KeyE: "KeyE", KeyF: "KeyF", + KeyG: "KeyG", KeyH: "KeyH", KeyI: "KeyI", KeyJ: "KeyJ", KeyK: "KeyK", KeyL: "KeyL", + KeyM: "KeyM", KeyN: "KeyN", KeyO: "KeyO", KeyP: "KeyP", KeyQ: "KeyQ", KeyR: "KeyR", + KeyS: "KeyS", KeyT: "KeyT", KeyU: "KeyU", KeyV: "KeyV", KeyW: "KeyW", KeyX: "KeyX", + KeyY: "KeyY", KeyZ: "KeyZ", + + // Numbers + Digit0: "Digit0", Digit1: "Digit1", Digit2: "Digit2", Digit3: "Digit3", Digit4: "Digit4", + Digit5: "Digit5", Digit6: "Digit6", Digit7: "Digit7", Digit8: "Digit8", Digit9: "Digit9", + + // Special keys + Space: "Space", + ShiftLeft: "ShiftLeft", + ShiftRight: "ShiftRight", + ControlLeft: "ControlLeft", + ControlRight: "ControlRight", + AltLeft: "AltLeft", + AltRight: "AltRight", + Escape: "Escape", + Enter: "Enter", + Tab: "Tab", + ArrowUp: "ArrowUp", + ArrowDown: "ArrowDown", + ArrowLeft: "ArrowLeft", + ArrowRight: "ArrowRight", + + // Function keys + F1: "F1", F2: "F2", F3: "F3", F4: "F4", F5: "F5", F6: "F6", + F7: "F7", F8: "F8", F9: "F9", F10: "F10", F11: "F11", F12: "F12", +} as const; + +export type KeyCode = typeof Keys[keyof typeof Keys]; + +export const Input = { + /** + * Check if a key is currently pressed + * @param inputState The current input state + * @param key The key to check + * @returns true if the key is pressed, false otherwise + */ + isKeyPressed: (inputState: InputStateT, key: KeyCode): boolean => + hostFn("isKeyPressed")(inputState, key), + + /** + * Get the current mouse X position + * @param inputState The current input state + * @returns The mouse X coordinate + */ + getMouseX: (inputState: InputStateT): number => + hostFn("getMouseX")(inputState), + + /** + * Get the current mouse Y position + * @param inputState The current input state + * @returns The mouse Y coordinate + */ + getMouseY: (inputState: InputStateT): number => + hostFn("getMouseY")(inputState), + + /** + * Get the mouse delta X (movement since last frame) + * @param inputState The current input state + * @returns The mouse delta X + */ + getMouseDeltaX: (inputState: InputStateT): number => + hostFn("getMouseDeltaX")(inputState), + + /** + * Get the mouse delta Y (movement since last frame) + * @param inputState The current input state + * @returns The mouse delta Y + */ + getMouseDeltaY: (inputState: InputStateT): number => + hostFn("getMouseDeltaY")(inputState), + + /** + * Lock or unlock the cursor + * @param locked Whether to lock the cursor + */ + lockCursor: (locked: boolean): void => + hostFn("lockCursor")(locked), +}; + +export const EntityProperties = { + /** + * Get a property value (generic) + * @param properties The entity properties object + * @param key The property key + * @returns The property value or null if not found + */ + getProperty: (properties: any, key: string): any => + hostFn("getProperty")(properties, key), + + /** + * Set a string property + * @param properties The entity properties object + * @param key The property key + * @param value The string value + * @returns Updated properties object + */ + setPropertyString: (properties: any, key: string, value: string): any => + hostFn("setPropertyString")(properties, key, value), + + /** + * Set an integer property + * @param properties The entity properties object + * @param key The property key + * @param value The integer value + * @returns Updated properties object + */ + setPropertyInt: (properties: any, key: string, value: number): any => + hostFn("setPropertyInt")(properties, key, value), + + /** + * Set a float property + * @param properties The entity properties object + * @param key The property key + * @param value The float value + * @returns Updated properties object + */ + setPropertyFloat: (properties: any, key: string, value: number): any => + hostFn("setPropertyFloat")(properties, key, value), + + /** + * Set a boolean property + * @param properties The entity properties object + * @param key The property key + * @param value The boolean value + * @returns Updated properties object + */ + setPropertyBool: (properties: any, key: string, value: boolean): any => + hostFn("setPropertyBool")(properties, key, value), + + /** + * Set a Vec3 property + * @param properties The entity properties object + * @param key The property key + * @param value The Vec3 value as [x, y, z] array + * @returns Updated properties object + */ + setPropertyVec3: (properties: any, key: string, value: [number, number, number]): any => + hostFn("setPropertyVec3")(properties, key, value), + + /** + * Get a string property + * @param properties The entity properties object + * @param key The property key + * @returns The string value or empty string if not found + */ + getString: (properties: any, key: string): string => + hostFn("getString")(properties, key), + + /** + * Get an integer property + * @param properties The entity properties object + * @param key The property key + * @returns The integer value or 0 if not found + */ + getInt: (properties: any, key: string): number => + hostFn("getInt")(properties, key), + + /** + * Get a float property + * @param properties The entity properties object + * @param key The property key + * @returns The float value or 0.0 if not found + */ + getFloat: (properties: any, key: string): number => + hostFn("getFloat")(properties, key), + + /** + * Get a boolean property + * @param properties The entity properties object + * @param key The property key + * @returns The boolean value or false if not found + */ + getBool: (properties: any, key: string): boolean => + hostFn("getBool")(properties, key), + + /** + * Get a Vec3 property + * @param properties The entity properties object + * @param key The property key + * @returns The Vec3 value as [x, y, z] array or [0, 0, 0] if not found + */ + getVec3: (properties: any, key: string): [number, number, number] => + hostFn("getVec3")(properties, key), + + /** + * Check if a property exists + * @param properties The entity properties object + * @param key The property key + * @returns True if the property exists, false otherwise + */ + hasProperty: (properties: any, key: string): boolean => + hostFn("hasProperty")(properties, key), +}; + +export { Transform, Vec3, Quaternion }; + +globalThis.Transform = Transform; +globalThis.Vec3 = Vec3; +globalThis.Quaternion = Quaternion; +globalThis.Input = Input; +globalThis.Keys = Keys; +globalThis.EntityProperties = EntityProperties; @@ -420,7 +420,7 @@ impl Component for ScriptComponent { ui.horizontal(|ui| { if ui.button("Browse").clicked() { if let Some(script_file) = rfd::FileDialog::new() - .add_filter("Rhai Script", &["rhai"]) + .add_filter("Typescript", &["ts"]) .pick_file() { let script_name = script_file @@ -437,8 +437,8 @@ impl Component for ScriptComponent { if ui.button("New").clicked() { if let Some(script_path) = rfd::FileDialog::new() - .add_filter("Rhai Script", &["rhai"]) - .set_file_name(format!("{}_script.rhai", label)) + .add_filter("TypeScript", &["ts"]) + .set_file_name(format!("{}_script.ts", label)) .save_file() { match std::fs::write(&script_path, TEMPLATE_SCRIPT) { @@ -217,7 +217,7 @@ impl Editor { viewport_mode: ViewportMode::None, signal: Signal::None, undo_stack: Vec::new(), - script_manager: ScriptManager::new(), + script_manager: ScriptManager::new().unwrap(), editor_state: EditorState::Editing, gizmo_mode: EnumSet::empty(), play_mode_backup: None, @@ -540,7 +540,6 @@ impl Editor { viewport_mode: &mut self.viewport_mode, undo_stack: &mut self.undo_stack, signal: &mut self.signal, - // engine: &mut self.rhai_engine, camera_manager: &mut self.camera_manager, gizmo_mode: &mut self.gizmo_mode, editor_mode: &mut self.editor_state, @@ -1,89 +1,333 @@ -use rhai::{Dynamic, Engine}; - use crate::states::{ModelProperties, PropertyValue}; +use rustyscript::{serde_json, Runtime}; +use serde::{Deserialize, Serialize}; + +// Create a serializable version of ModelProperties for script communication +#[derive(Clone, Serialize, Deserialize)] +pub struct SerializableModelProperties { + pub properties: std::collections::HashMap<String, serde_json::Value>, +} + +impl From<&ModelProperties> for SerializableModelProperties { + fn from(props: &ModelProperties) -> Self { + let mut properties = std::collections::HashMap::new(); + + // Convert each property to JSON value + for (key, value) in props.custom_properties.iter() { + let json_value = match value { + PropertyValue::String(s) => serde_json::Value::String(s.clone()), + PropertyValue::Int(i) => serde_json::Value::Number(serde_json::Number::from(*i)), + PropertyValue::Float(f) => serde_json::Value::Number( + serde_json::Number::from_f64(*f as f64).unwrap_or(serde_json::Number::from(0)) + ), + PropertyValue::Bool(b) => serde_json::Value::Bool(*b), + PropertyValue::Vec3(v) => serde_json::Value::Array(vec![ + serde_json::Value::Number(serde_json::Number::from_f64(v[0] as f64).unwrap()), + serde_json::Value::Number(serde_json::Number::from_f64(v[1] as f64).unwrap()), + serde_json::Value::Number(serde_json::Number::from_f64(v[2] as f64).unwrap()), + ]), + }; + properties.insert(key.clone(), json_value); + } + + Self { properties } + } +} + +impl SerializableModelProperties { + pub fn to_model_properties(&self) -> ModelProperties { + let mut props = ModelProperties::default(); + + for (key, value) in &self.properties { + let property_value = match value { + serde_json::Value::String(s) => PropertyValue::String(s.clone()), + serde_json::Value::Number(n) => { + if let Some(i) = n.as_i64() { + PropertyValue::Int(i) + } else if let Some(f) = n.as_f64() { + PropertyValue::Float(f) + } else { + continue; + } + }, + serde_json::Value::Bool(b) => PropertyValue::Bool(*b), + serde_json::Value::Array(arr) => { + if arr.len() == 3 { + if let (Some(x), Some(y), Some(z)) = ( + arr[0].as_f64(), + arr[1].as_f64(), + arr[2].as_f64(), + ) { + PropertyValue::Vec3([x as f32, y as f32, z as f32]) + } else { + continue; + } + } else { + continue; + } + }, + _ => continue, + }; + props.set_property(key.clone(), property_value); + } + + props + } +} + +pub fn register_model_props_module(runtime: &mut Runtime) -> anyhow::Result<()> { + // Get property (generic) + runtime.register_function("getProperty", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 2 { + return Err(rustyscript::Error::Runtime("getProperty requires 2 arguments (properties, key)".to_string())); + } + + let props: SerializableModelProperties = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid properties: {}", e)))?; + + let key = args[1].as_str() + .ok_or_else(|| rustyscript::Error::Runtime("Key must be a string".to_string()))?; + + let value = props.properties.get(key) + .cloned() + .unwrap_or(serde_json::Value::Null); + + Ok(value) + })?; + + // Set property (string) + runtime.register_function("setPropertyString", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 3 { + return Err(rustyscript::Error::Runtime("setPropertyString requires 3 arguments (properties, key, value)".to_string())); + } + + let mut props: SerializableModelProperties = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid properties: {}", e)))?; + + let key = args[1].as_str() + .ok_or_else(|| rustyscript::Error::Runtime("Key must be a string".to_string()))?; + + let value = args[2].as_str() + .ok_or_else(|| rustyscript::Error::Runtime("Value must be a string".to_string()))?; + + props.properties.insert(key.to_string(), serde_json::Value::String(value.to_string())); + + serde_json::to_value(props) + .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize properties: {}", e))) + })?; -pub fn register_model_props_module(engine: &mut Engine) { - engine.register_type_with_name::<ModelProperties>("Properties"); - - engine.register_fn("get_property", |props: &mut ModelProperties, key: &str| -> Dynamic { - match props.get_property(key) { - Some(PropertyValue::String(s)) => Dynamic::from(s.clone()), - Some(PropertyValue::Int(i)) => Dynamic::from(*i), - Some(PropertyValue::Float(f)) => Dynamic::from(*f), - Some(PropertyValue::Bool(b)) => Dynamic::from(*b), - Some(PropertyValue::Vec3(v)) => Dynamic::from([v[0] as f64, v[1] as f64, v[2] as f64]), - None => Dynamic::UNIT, + // Set property (int) + runtime.register_function("setPropertyInt", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 3 { + return Err(rustyscript::Error::Runtime("setPropertyInt requires 3 arguments (properties, key, value)".to_string())); } - }); - - engine.register_fn("set_property", |props: &mut ModelProperties, key: &str, value: String| { - props.set_property(key.to_string(), PropertyValue::String(value)); - }); - - engine.register_fn("set_property", |props: &mut ModelProperties, key: &str, value: i64| { - props.set_property(key.to_string(), PropertyValue::Int(value)); - }); - - engine.register_fn("set_property", |props: &mut ModelProperties, key: &str, value: f64| { - props.set_property(key.to_string(), PropertyValue::Float(value)); - }); - - engine.register_fn("set_property", |props: &mut ModelProperties, key: &str, value: bool| { - props.set_property(key.to_string(), PropertyValue::Bool(value)); - }); - - engine.register_fn("set_property", |props: &mut ModelProperties, key: &str, value: rhai::Array| { - if value.len() == 3 { - if let (Ok(x), Ok(y), Ok(z)) = ( - value[0].as_float(), - value[1].as_float(), - value[2].as_float() - ) { - props.set_property(key.to_string(), PropertyValue::Vec3([x as f32, y as f32, z as f32])); - } + + let mut props: SerializableModelProperties = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid properties: {}", e)))?; + + let key = args[1].as_str() + .ok_or_else(|| rustyscript::Error::Runtime("Key must be a string".to_string()))?; + + let value = args[2].as_i64() + .ok_or_else(|| rustyscript::Error::Runtime("Value must be an integer".to_string()))?; + + props.properties.insert(key.to_string(), serde_json::Value::Number(serde_json::Number::from(value))); + + serde_json::to_value(props) + .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize properties: {}", e))) + })?; + + // Set property (float) + runtime.register_function("setPropertyFloat", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 3 { + return Err(rustyscript::Error::Runtime("setPropertyFloat requires 3 arguments (properties, key, value)".to_string())); } - }); + + let mut props: SerializableModelProperties = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid properties: {}", e)))?; + + let key = args[1].as_str() + .ok_or_else(|| rustyscript::Error::Runtime("Key must be a string".to_string()))?; + + let value = args[2].as_f64() + .ok_or_else(|| rustyscript::Error::Runtime("Value must be a number".to_string()))?; + + props.properties.insert(key.to_string(), serde_json::Value::Number( + serde_json::Number::from_f64(value).unwrap_or(serde_json::Number::from(0)) + )); + + serde_json::to_value(props) + .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize properties: {}", e))) + })?; - engine.register_fn("get_string", |props: &mut ModelProperties, key: &str| -> String { - match props.get_property(key) { - Some(PropertyValue::String(s)) => s.clone(), - _ => String::new(), + // Set property (bool) + runtime.register_function("setPropertyBool", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 3 { + return Err(rustyscript::Error::Runtime("setPropertyBool requires 3 arguments (properties, key, value)".to_string())); } - }); + + let mut props: SerializableModelProperties = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid properties: {}", e)))?; + + let key = args[1].as_str() + .ok_or_else(|| rustyscript::Error::Runtime("Key must be a string".to_string()))?; + + let value = args[2].as_bool() + .ok_or_else(|| rustyscript::Error::Runtime("Value must be a boolean".to_string()))?; + + props.properties.insert(key.to_string(), serde_json::Value::Bool(value)); + + serde_json::to_value(props) + .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize properties: {}", e))) + })?; - engine.register_fn("get_int", |props: &mut ModelProperties, key: &str| -> i64 { - match props.get_property(key) { - Some(PropertyValue::Int(i)) => *i, - _ => 0, + // Set property (Vec3) + runtime.register_function("setPropertyVec3", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 3 { + return Err(rustyscript::Error::Runtime("setPropertyVec3 requires 3 arguments (properties, key, value)".to_string())); + } + + let mut props: SerializableModelProperties = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid properties: {}", e)))?; + + let key = args[1].as_str() + .ok_or_else(|| rustyscript::Error::Runtime("Key must be a string".to_string()))?; + + let value = args[2].as_array() + .ok_or_else(|| rustyscript::Error::Runtime("Value must be an array".to_string()))?; + + if value.len() != 3 { + return Err(rustyscript::Error::Runtime("Vec3 array must have exactly 3 elements".to_string())); } - }); + + let x = value[0].as_f64().ok_or_else(|| rustyscript::Error::Runtime("Vec3 elements must be numbers".to_string()))?; + let y = value[1].as_f64().ok_or_else(|| rustyscript::Error::Runtime("Vec3 elements must be numbers".to_string()))?; + let z = value[2].as_f64().ok_or_else(|| rustyscript::Error::Runtime("Vec3 elements must be numbers".to_string()))?; + + let vec3_array = serde_json::Value::Array(vec![ + serde_json::Value::Number(serde_json::Number::from_f64(x).unwrap()), + serde_json::Value::Number(serde_json::Number::from_f64(y).unwrap()), + serde_json::Value::Number(serde_json::Number::from_f64(z).unwrap()), + ]); + + props.properties.insert(key.to_string(), vec3_array); + + serde_json::to_value(props) + .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize properties: {}", e))) + })?; - engine.register_fn("get_float", |props: &mut ModelProperties, key: &str| -> f64 { - match props.get_property(key) { - Some(PropertyValue::Float(f)) => *f, - _ => 0.0, + // Typed getters + runtime.register_function("getString", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 2 { + return Err(rustyscript::Error::Runtime("getString requires 2 arguments (properties, key)".to_string())); } - }); + + let props: SerializableModelProperties = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid properties: {}", e)))?; + + let key = args[1].as_str() + .ok_or_else(|| rustyscript::Error::Runtime("Key must be a string".to_string()))?; + + let value = props.properties.get(key) + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + + Ok(serde_json::Value::String(value)) + })?; - engine.register_fn("get_bool", |props: &mut ModelProperties, key: &str| -> bool { - match props.get_property(key) { - Some(PropertyValue::Bool(b)) => *b, - _ => false, + runtime.register_function("getInt", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 2 { + return Err(rustyscript::Error::Runtime("getInt requires 2 arguments (properties, key)".to_string())); } - }); - - engine.register_fn("get_vec3", |props: &mut ModelProperties, key: &str| -> rhai::Array { - match props.get_property(key) { - Some(PropertyValue::Vec3(v)) => { - vec![Dynamic::from(v[0] as f64), Dynamic::from(v[1] as f64), Dynamic::from(v[2] as f64)] - }, - _ => vec![Dynamic::from(0.0), Dynamic::from(0.0), Dynamic::from(0.0)], + + let props: SerializableModelProperties = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid properties: {}", e)))?; + + let key = args[1].as_str() + .ok_or_else(|| rustyscript::Error::Runtime("Key must be a string".to_string()))?; + + let value = props.properties.get(key) + .and_then(|v| v.as_i64()) + .unwrap_or(0); + + Ok(serde_json::Value::Number(serde_json::Number::from(value))) + })?; + + runtime.register_function("getFloat", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 2 { + return Err(rustyscript::Error::Runtime("getFloat requires 2 arguments (properties, key)".to_string())); } - }); + + let props: SerializableModelProperties = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid properties: {}", e)))?; + + let key = args[1].as_str() + .ok_or_else(|| rustyscript::Error::Runtime("Key must be a string".to_string()))?; + + let value = props.properties.get(key) + .and_then(|v| v.as_f64()) + .unwrap_or(0.0); + + Ok(serde_json::Value::Number(serde_json::Number::from_f64(value).unwrap())) + })?; - engine.register_fn("has_property", |props: &mut ModelProperties, key: &str| -> bool { - props.get_property(key).is_some() - }); + runtime.register_function("getBool", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 2 { + return Err(rustyscript::Error::Runtime("getBool requires 2 arguments (properties, key)".to_string())); + } + + let props: SerializableModelProperties = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid properties: {}", e)))?; + + let key = args[1].as_str() + .ok_or_else(|| rustyscript::Error::Runtime("Key must be a string".to_string()))?; + + let value = props.properties.get(key) + .and_then(|v| v.as_bool()) + .unwrap_or(false); + + Ok(serde_json::Value::Bool(value)) + })?; + + runtime.register_function("getVec3", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 2 { + return Err(rustyscript::Error::Runtime("getVec3 requires 2 arguments (properties, key)".to_string())); + } + + let props: SerializableModelProperties = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid properties: {}", e)))?; + + let key = args[1].as_str() + .ok_or_else(|| rustyscript::Error::Runtime("Key must be a string".to_string()))?; + + let value = props.properties.get(key) + .and_then(|v| v.as_array()) + .cloned() + .unwrap_or_else(|| vec![ + serde_json::Value::Number(serde_json::Number::from(0)), + serde_json::Value::Number(serde_json::Number::from(0)), + serde_json::Value::Number(serde_json::Number::from(0)), + ]); + + Ok(serde_json::Value::Array(value)) + })?; + + runtime.register_function("hasProperty", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 2 { + return Err(rustyscript::Error::Runtime("hasProperty requires 2 arguments (properties, key)".to_string())); + } + + let props: SerializableModelProperties = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid properties: {}", e)))?; + + let key = args[1].as_str() + .ok_or_else(|| rustyscript::Error::Runtime("Key must be a string".to_string()))?; + + let has_property = props.properties.contains_key(key); + Ok(serde_json::Value::Bool(has_property)) + })?; log::info!("[Script] Initialised entity custom properties module"); + Ok(()) } @@ -1,11 +1,14 @@ use std::{collections::{HashMap, HashSet}, time::{Duration, Instant}}; -use rhai::*; +use rustyscript::{serde_json, Runtime}; +use serde::{Deserialize, Serialize}; use winit::{event::MouseButton, keyboard::KeyCode}; -#[derive(rhai::CustomType, Clone)] +#[derive(Clone)] pub struct InputState { + #[allow(dead_code)] pub last_key_press_times: HashMap<KeyCode, Instant>, + #[allow(dead_code)] pub double_press_threshold: Duration, pub mouse_pos: (f64, f64), pub mouse_button: HashSet<MouseButton>, @@ -14,6 +17,28 @@ pub struct InputState { pub is_cursor_locked: bool, } +// Create a serializable version for script communication +#[derive(Clone, Serialize, Deserialize)] +pub struct SerializableInputState { + pub mouse_pos: (f64, f64), + pub pressed_keys: Vec<String>, // Convert KeyCode to strings + pub mouse_delta: Option<(f64, f64)>, + pub is_cursor_locked: bool, +} + +impl From<&InputState> for SerializableInputState { + fn from(input_state: &InputState) -> Self { + Self { + mouse_pos: input_state.mouse_pos, + pressed_keys: input_state.pressed_keys.iter() + .filter_map(|key| keycode_to_string(*key)) + .collect(), + mouse_delta: input_state.mouse_delta, + is_cursor_locked: input_state.is_cursor_locked, + } + } +} + impl Default for InputState { fn default() -> Self { Self::new() @@ -39,72 +64,224 @@ impl InputState { self.is_cursor_locked = toggle; } - pub fn register_input_modules(engine: &mut Engine) { - engine.register_type_with_name::<InputState>("InputState"); - engine.register_type_with_name::<Key>("Key"); + pub fn register_input_modules(runtime: &mut Runtime) -> anyhow::Result<()> { + // Register input functions that take SerializableInputState as parameter + runtime.register_function("isKeyPressed", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 2 { + return Err(rustyscript::Error::Runtime("isKeyPressed requires 2 arguments (inputState, keyCode)".to_string())); + } + + let input_state: SerializableInputState = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid input state: {}", e)))?; + + let key_code_str = args[1].as_str() + .ok_or_else(|| rustyscript::Error::Runtime("Key code must be a string".to_string()))?; + + let is_pressed = input_state.pressed_keys.contains(&key_code_str.to_string()); + Ok(serde_json::Value::Bool(is_pressed)) + })?; - engine - .register_fn("is_pressed", |s: &mut InputState, k: Key| s.pressed_keys.contains(&k.0)) - .register_get("mouse_x", |s: &mut InputState| s.mouse_pos.0) - .register_get("mouse_y", |s: &mut InputState| s.mouse_pos.1) - .register_get("mouse_dx", |s: &mut InputState| s.mouse_delta.map(|d| d.0).unwrap_or(0.0)) - .register_get("mouse_dy", |s: &mut InputState| s.mouse_delta.map(|d| d.1).unwrap_or(0.0)) - .register_fn("lock_cursor", |s: &mut InputState, value: bool| s.is_cursor_locked = value); + runtime.register_function("getMouseX", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 1 { + return Err(rustyscript::Error::Runtime("getMouseX requires 1 argument (inputState)".to_string())); + } + + let input_state: SerializableInputState = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid input state: {}", e)))?; + + Ok(serde_json::Value::Number(serde_json::Number::from_f64(input_state.mouse_pos.0).unwrap())) + })?; - let mut kmod = Module::new(); + runtime.register_function("getMouseY", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 1 { + return Err(rustyscript::Error::Runtime("getMouseY requires 1 argument (inputState)".to_string())); + } + + let input_state: SerializableInputState = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid input state: {}", e)))?; + + Ok(serde_json::Value::Number(serde_json::Number::from_f64(input_state.mouse_pos.1).unwrap())) + })?; - macro_rules! add_key { - ($name:ident, $kc:expr) => { - kmod.set_var(stringify!($name), Key($kc)); - }; - } + runtime.register_function("getMouseDeltaX", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 1 { + return Err(rustyscript::Error::Runtime("getMouseDeltaX requires 1 argument (inputState)".to_string())); + } + + let input_state: SerializableInputState = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid input state: {}", e)))?; + + let delta_x = input_state.mouse_delta.map(|d| d.0).unwrap_or(0.0); + Ok(serde_json::Value::Number(serde_json::Number::from_f64(delta_x).unwrap())) + })?; + + runtime.register_function("getMouseDeltaY", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 1 { + return Err(rustyscript::Error::Runtime("getMouseDeltaY requires 1 argument (inputState)".to_string())); + } + + let input_state: SerializableInputState = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid input state: {}", e)))?; + + let delta_y = input_state.mouse_delta.map(|d| d.1).unwrap_or(0.0); + Ok(serde_json::Value::Number(serde_json::Number::from_f64(delta_y).unwrap())) + })?; + + runtime.register_function("lockCursor", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 1 { + return Err(rustyscript::Error::Runtime("lockCursor requires 1 argument (locked)".to_string())); + } + + let locked = args[0].as_bool() + .ok_or_else(|| rustyscript::Error::Runtime("Locked parameter must be a boolean".to_string()))?; + + // This function would need to communicate back to the engine to actually lock the cursor + // For now, just return success + Ok(serde_json::Value::Bool(true)) + })?; - add_key!(A, KeyCode::KeyA); - add_key!(B, KeyCode::KeyB); - add_key!(C, KeyCode::KeyC); - add_key!(D, KeyCode::KeyD); - add_key!(E, KeyCode::KeyE); - add_key!(F, KeyCode::KeyF); - add_key!(G, KeyCode::KeyG); - add_key!(H, KeyCode::KeyH); - add_key!(I, KeyCode::KeyI); - add_key!(J, KeyCode::KeyJ); - add_key!(K, KeyCode::KeyK); - add_key!(L, KeyCode::KeyL); - add_key!(M, KeyCode::KeyM); - add_key!(N, KeyCode::KeyN); - add_key!(O, KeyCode::KeyO); - add_key!(P, KeyCode::KeyP); - add_key!(Q, KeyCode::KeyQ); - add_key!(R, KeyCode::KeyR); - add_key!(S, KeyCode::KeyS); - add_key!(T, KeyCode::KeyT); - add_key!(U, KeyCode::KeyU); - add_key!(V, KeyCode::KeyV); - add_key!(W, KeyCode::KeyW); - add_key!(X, KeyCode::KeyX); - add_key!(Y, KeyCode::KeyY); - add_key!(Z, KeyCode::KeyZ); - - add_key!(Space, KeyCode::Space); - add_key!(ShiftLeft, KeyCode::ShiftLeft); - add_key!(ShiftRight, KeyCode::ShiftRight); - add_key!(ControlLeft, KeyCode::ControlLeft); - add_key!(ControlRight, KeyCode::ControlRight); - add_key!(AltLeft, KeyCode::AltLeft); - add_key!(AltRight, KeyCode::AltRight); - add_key!(Escape, KeyCode::Escape); - add_key!(Enter, KeyCode::Enter); - add_key!(Tab, KeyCode::Tab); - add_key!(Up, KeyCode::ArrowUp); - add_key!(Down, KeyCode::ArrowDown); - add_key!(Left, KeyCode::ArrowLeft); - add_key!(Right, KeyCode::ArrowRight); - - engine.register_static_module("keys", kmod.into()); log::info!("[Script] Initialised input module"); + Ok(()) + } +} + +// Helper function to convert string to KeyCode +fn string_to_keycode(key_str: &str) -> Option<KeyCode> { + match key_str { + "KeyA" => Some(KeyCode::KeyA), + "KeyB" => Some(KeyCode::KeyB), + "KeyC" => Some(KeyCode::KeyC), + "KeyD" => Some(KeyCode::KeyD), + "KeyE" => Some(KeyCode::KeyE), + "KeyF" => Some(KeyCode::KeyF), + "KeyG" => Some(KeyCode::KeyG), + "KeyH" => Some(KeyCode::KeyH), + "KeyI" => Some(KeyCode::KeyI), + "KeyJ" => Some(KeyCode::KeyJ), + "KeyK" => Some(KeyCode::KeyK), + "KeyL" => Some(KeyCode::KeyL), + "KeyM" => Some(KeyCode::KeyM), + "KeyN" => Some(KeyCode::KeyN), + "KeyO" => Some(KeyCode::KeyO), + "KeyP" => Some(KeyCode::KeyP), + "KeyQ" => Some(KeyCode::KeyQ), + "KeyR" => Some(KeyCode::KeyR), + "KeyS" => Some(KeyCode::KeyS), + "KeyT" => Some(KeyCode::KeyT), + "KeyU" => Some(KeyCode::KeyU), + "KeyV" => Some(KeyCode::KeyV), + "KeyW" => Some(KeyCode::KeyW), + "KeyX" => Some(KeyCode::KeyX), + "KeyY" => Some(KeyCode::KeyY), + "KeyZ" => Some(KeyCode::KeyZ), + "Space" => Some(KeyCode::Space), + "ShiftLeft" => Some(KeyCode::ShiftLeft), + "ShiftRight" => Some(KeyCode::ShiftRight), + "ControlLeft" => Some(KeyCode::ControlLeft), + "ControlRight" => Some(KeyCode::ControlRight), + "AltLeft" => Some(KeyCode::AltLeft), + "AltRight" => Some(KeyCode::AltRight), + "Escape" => Some(KeyCode::Escape), + "Enter" => Some(KeyCode::Enter), + "Tab" => Some(KeyCode::Tab), + "ArrowUp" => Some(KeyCode::ArrowUp), + "ArrowDown" => Some(KeyCode::ArrowDown), + "ArrowLeft" => Some(KeyCode::ArrowLeft), + "ArrowRight" => Some(KeyCode::ArrowRight), + "Digit0" => Some(KeyCode::Digit0), + "Digit1" => Some(KeyCode::Digit1), + "Digit2" => Some(KeyCode::Digit2), + "Digit3" => Some(KeyCode::Digit3), + "Digit4" => Some(KeyCode::Digit4), + "Digit5" => Some(KeyCode::Digit5), + "Digit6" => Some(KeyCode::Digit6), + "Digit7" => Some(KeyCode::Digit7), + "Digit8" => Some(KeyCode::Digit8), + "Digit9" => Some(KeyCode::Digit9), + "F1" => Some(KeyCode::F1), + "F2" => Some(KeyCode::F2), + "F3" => Some(KeyCode::F3), + "F4" => Some(KeyCode::F4), + "F5" => Some(KeyCode::F5), + "F6" => Some(KeyCode::F6), + "F7" => Some(KeyCode::F7), + "F8" => Some(KeyCode::F8), + "F9" => Some(KeyCode::F9), + "F10" => Some(KeyCode::F10), + "F11" => Some(KeyCode::F11), + "F12" => Some(KeyCode::F12), + _ => None, + } +} + +// Helper function to convert KeyCode to string +fn keycode_to_string(key_code: KeyCode) -> Option<String> { + match key_code { + KeyCode::KeyA => Some("KeyA".to_string()), + KeyCode::KeyB => Some("KeyB".to_string()), + KeyCode::KeyC => Some("KeyC".to_string()), + KeyCode::KeyD => Some("KeyD".to_string()), + KeyCode::KeyE => Some("KeyE".to_string()), + KeyCode::KeyF => Some("KeyF".to_string()), + KeyCode::KeyG => Some("KeyG".to_string()), + KeyCode::KeyH => Some("KeyH".to_string()), + KeyCode::KeyI => Some("KeyI".to_string()), + KeyCode::KeyJ => Some("KeyJ".to_string()), + KeyCode::KeyK => Some("KeyK".to_string()), + KeyCode::KeyL => Some("KeyL".to_string()), + KeyCode::KeyM => Some("KeyM".to_string()), + KeyCode::KeyN => Some("KeyN".to_string()), + KeyCode::KeyO => Some("KeyO".to_string()), + KeyCode::KeyP => Some("KeyP".to_string()), + KeyCode::KeyQ => Some("KeyQ".to_string()), + KeyCode::KeyR => Some("KeyR".to_string()), + KeyCode::KeyS => Some("KeyS".to_string()), + KeyCode::KeyT => Some("KeyT".to_string()), + KeyCode::KeyU => Some("KeyU".to_string()), + KeyCode::KeyV => Some("KeyV".to_string()), + KeyCode::KeyW => Some("KeyW".to_string()), + KeyCode::KeyX => Some("KeyX".to_string()), + KeyCode::KeyY => Some("KeyY".to_string()), + KeyCode::KeyZ => Some("KeyZ".to_string()), + KeyCode::Space => Some("Space".to_string()), + KeyCode::ShiftLeft => Some("ShiftLeft".to_string()), + KeyCode::ShiftRight => Some("ShiftRight".to_string()), + KeyCode::ControlLeft => Some("ControlLeft".to_string()), + KeyCode::ControlRight => Some("ControlRight".to_string()), + KeyCode::AltLeft => Some("AltLeft".to_string()), + KeyCode::AltRight => Some("AltRight".to_string()), + KeyCode::Escape => Some("Escape".to_string()), + KeyCode::Enter => Some("Enter".to_string()), + KeyCode::Tab => Some("Tab".to_string()), + KeyCode::ArrowUp => Some("ArrowUp".to_string()), + KeyCode::ArrowDown => Some("ArrowDown".to_string()), + KeyCode::ArrowLeft => Some("ArrowLeft".to_string()), + KeyCode::ArrowRight => Some("ArrowRight".to_string()), + KeyCode::Digit0 => Some("Digit0".to_string()), + KeyCode::Digit1 => Some("Digit1".to_string()), + KeyCode::Digit2 => Some("Digit2".to_string()), + KeyCode::Digit3 => Some("Digit3".to_string()), + KeyCode::Digit4 => Some("Digit4".to_string()), + KeyCode::Digit5 => Some("Digit5".to_string()), + KeyCode::Digit6 => Some("Digit6".to_string()), + KeyCode::Digit7 => Some("Digit7".to_string()), + KeyCode::Digit8 => Some("Digit8".to_string()), + KeyCode::Digit9 => Some("Digit9".to_string()), + KeyCode::F1 => Some("F1".to_string()), + KeyCode::F2 => Some("F2".to_string()), + KeyCode::F3 => Some("F3".to_string()), + KeyCode::F4 => Some("F4".to_string()), + KeyCode::F5 => Some("F5".to_string()), + KeyCode::F6 => Some("F6".to_string()), + KeyCode::F7 => Some("F7".to_string()), + KeyCode::F8 => Some("F8".to_string()), + KeyCode::F9 => Some("F9".to_string()), + KeyCode::F10 => Some("F10".to_string()), + KeyCode::F11 => Some("F11".to_string()), + KeyCode::F12 => Some("F12".to_string()), + _ => None, } } -#[derive(rhai::CustomType, Clone, Copy)] +#[derive(Clone, Copy)] pub struct Key(pub KeyCode); @@ -1,8 +1,166 @@ -use rustyscript::{Module, Runtime}; +use dropbear_engine::entity::Transform; +use glam::{DQuat, DVec3}; +use rustyscript::{serde_json, Runtime}; pub fn register_math_functions(runtime: &mut Runtime) -> anyhow::Result<()> { - // runtime.load_module_async(module) - let module = Module::load_dir("typescript")?; - runtime.load_modules(module, side_modules) + + // Transform functions + runtime.register_function("createTransform", |_args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + let transform = Transform::new(); + serde_json::to_value(transform) + .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize Transform: {}", e))) + })?; + + runtime.register_function("transformTranslate", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 2 { + return Err(rustyscript::Error::Runtime("transformTranslate requires 2 arguments".to_string())); + } + + let mut transform: Transform = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid transform: {}", e)))?; + + // Handle both array and individual values for translation + let translation = if let Some(array) = args[1].as_array() { + if array.len() != 3 { + return Err(rustyscript::Error::Runtime("Translation array must have 3 elements".to_string())); + } + DVec3::new( + array[0].as_f64().unwrap_or(0.0), + array[1].as_f64().unwrap_or(0.0), + array[2].as_f64().unwrap_or(0.0), + ) + } else { + return Err(rustyscript::Error::Runtime("Translation must be an array".to_string())); + }; + + transform.position += translation; + serde_json::to_value(transform) + .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize Transform: {}", e))) + })?; + + runtime.register_function("transformRotateX", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 2 { + return Err(rustyscript::Error::Runtime("transformRotateX requires 2 arguments".to_string())); + } + + let mut transform: Transform = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid transform: {}", e)))?; + + let angle = args[1].as_f64().unwrap_or(0.0); + let rotation = DQuat::from_rotation_x(angle); + transform.rotation = rotation * transform.rotation; + + serde_json::to_value(transform) + .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize Transform: {}", e))) + })?; + + runtime.register_function("transformRotateY", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 2 { + return Err(rustyscript::Error::Runtime("transformRotateY requires 2 arguments".to_string())); + } + + let mut transform: Transform = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid transform: {}", e)))?; + + let angle = args[1].as_f64().unwrap_or(0.0); + let rotation = DQuat::from_rotation_y(angle); + transform.rotation = rotation * transform.rotation; + + serde_json::to_value(transform) + .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize Transform: {}", e))) + })?; + + runtime.register_function("transformRotateZ", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 2 { + return Err(rustyscript::Error::Runtime("transformRotateZ requires 2 arguments".to_string())); + } + + let mut transform: Transform = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid transform: {}", e)))?; + + let angle = args[1].as_f64().unwrap_or(0.0); + let rotation = DQuat::from_rotation_z(angle); + transform.rotation = rotation * transform.rotation; + + serde_json::to_value(transform) + .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize Transform: {}", e))) + })?; + + runtime.register_function("transformScale", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 2 { + return Err(rustyscript::Error::Runtime("transformScale requires 2 arguments".to_string())); + } + + let mut transform: Transform = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid transform: {}", e)))?; + + let scale = if let Some(num) = args[1].as_f64() { + DVec3::splat(num) + } else if let Some(array) = args[1].as_array() { + if array.len() != 3 { + return Err(rustyscript::Error::Runtime("Scale array must have 3 elements".to_string())); + } + DVec3::new( + array[0].as_f64().unwrap_or(1.0), + array[1].as_f64().unwrap_or(1.0), + array[2].as_f64().unwrap_or(1.0), + ) + } else { + return Err(rustyscript::Error::Runtime("Scale must be a number or array".to_string())); + }; + + transform.scale *= scale; + serde_json::to_value(transform) + .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize Transform: {}", e))) + })?; + + runtime.register_function("transformMatrix", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 1 { + return Err(rustyscript::Error::Runtime("transformMatrix requires 1 argument".to_string())); + } + + let transform: Transform = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid transform: {}", e)))?; + + // Assuming Transform has a matrix() method that returns a 4x4 matrix + // You may need to adjust this based on your actual Transform implementation + let matrix = transform.matrix(); + serde_json::to_value(matrix) + .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize matrix: {}", e))) + })?; + + // Vec3 functions + runtime.register_function("createVec3", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + let x = args.get(0).and_then(|v| v.as_f64()).unwrap_or(0.0); + let y = args.get(1).and_then(|v| v.as_f64()).unwrap_or(0.0); + let z = args.get(2).and_then(|v| v.as_f64()).unwrap_or(0.0); + + let vec3 = DVec3::new(x, y, z); + serde_json::to_value(vec3) + .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize Vec3: {}", e))) + })?; + + // Quaternion functions + runtime.register_function("createQuatIdentity", |_args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + let quat = DQuat::IDENTITY; + serde_json::to_value(quat) + .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize Quaternion: {}", e))) + })?; + + runtime.register_function("createQuatFromEuler", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 3 { + return Err(rustyscript::Error::Runtime("createQuatFromEuler requires 3 arguments".to_string())); + } + + let x = args[0].as_f64().unwrap_or(0.0); + let y = args[1].as_f64().unwrap_or(0.0); + let z = args[2].as_f64().unwrap_or(0.0); + + let quat = DQuat::from_euler(glam::EulerRot::XYZ, x, y, z); + serde_json::to_value(quat) + .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize Quaternion: {}", e))) + })?; + log::info!("[Script] Initialised math module"); + Ok(()) } @@ -2,13 +2,9 @@ pub mod entity; pub mod math; pub mod input; -use chrono::DateTime; use dropbear_engine::entity::{AdoptedEntity, Transform}; -use glam::{DQuat, DVec3}; use hecs::World; -use rustyscript::{Runtime, RuntimeOptions}; -use specta::{Type, TypeCollection}; -use specta_typescript::Typescript; +use rustyscript::{serde_json, Module, ModuleHandle, Runtime, RuntimeOptions}; use std::path::PathBuf; use std::{collections::HashMap, fs}; @@ -159,120 +155,43 @@ pub fn attach_script_to_entity( Ok(()) } -// #[cfg(feature = "editor")] -// pub fn generate_ts() -> anyhow::Result<()> { -// let mut types = TypeCollection::default(); -// types.register::<Transform>(); - -// let utc: DateTime<Utc> = Utc::now(); -// let header = format!(" -// // @generated by dropbear-engine on {} UTC time -// // Hello there! This is the generated stubs for the Eucalyptus Editor scripting component. -// // This file purely exists for the LSP and for TypeScripts type safety. Running this library -// // most likely won't do anything, but go ahead and be my guest. -// // EDIT THIS LIBRARY AT YOUR OWN WILL. IT IS RECOMMENDED TO NOT TOUCH THIS FILE -// ", utc); - -// if let Ok(project) = PROJECT.read() { -// Typescript::new().header(header).export_to(project.project_path.clone().join("src/dropbear-engine.d.ts"), types)?; -// } else { -// anyhow::bail!("Unable to retain Project Config lock, could not generate typescript bindings"); -// } -// Ok(()) -// } - pub struct ScriptManager { pub runtime: Runtime, - compiled_scripts: HashMap<String, AST>, - script_scopes: HashMap<hecs::Entity, Scope<'static>>, + compiled_scripts: HashMap<String, ModuleHandle>, + entity_script_data: HashMap<hecs::Entity, serde_json::Value>, } impl ScriptManager { pub fn new() -> anyhow::Result<Self> { let mut runtime = Runtime::new(RuntimeOptions::default())?; - - // // REGISTER FUNCTIONS HERE - // math::register_math_functions(&mut runtime); - // input::InputState::register_input_modules(&mut runtime); - // entity::register_model_props_module(&mut runtime); - - // // register types used by scripts - // runtime.register_type_with_name::<Transform>("Transform"); - // runtime.register_type_with_name::<DQuat>("Quaternion"); - // runtime.register_type_with_name::<DVec3>("Vector3"); - - // runtime.mod - - // // constructors / helpers (alias names to match helper.ts) - // runtime.register_function("createTransform", Transform::new); - // runtime.register_function("createVec3", |x: f64, y: f64, z: f64| DVec3::new(x, y, z)); - // // keep the original alias if you used "vec3" elsewhere - // runtime.register_function("vec3", |x: f64, y: f64, z: f64| DVec3::new(x, y, z)); - - // runtime.register_function("createQuatIdentity", || DQuat::IDENTITY); - // // from-euler: adapt to your exact DQuat API if different - // runtime.register_function("createQuatFromEuler", |x: f64, y: f64, z: f64| { - // // uses EulerRot::XYZ — adjust if your glam API differs - // DQuat::from_euler(glam::EulerRot::XYZ, x, y, z) - // }); - - // // transform helpers: simple implementations that return an updated Transform - // runtime.register_function( - // "transformTranslate", - // |mut t: Transform, v: DVec3| { - // t.position += v; - // t - // }, - // ); - // runtime.register_function( - // "transformScale", - // |mut t: Transform, s: DVec3| { - // t.scale *= s; - // t - // }, - // ); - // runtime.register_function( - // "transformMatrix", - // |t: Transform| { - // // return whatever representation your scripts expect (e.g. a 4x4 matrix type) - // t.matrix() // replace with actual method if different - // }, - // ); - - // // axis rotations — simple wrappers that multiply quaternions - // runtime.register_function("transformRotateX", |mut t: Transform, a: f64| { - // let r = DQuat::from_rotation_x(a); - // t.rotation = r * t.rotation; - // t - // }); - // runtime.register_function("transformRotateY", |mut t: Transform, a: f64| { - // let r = DQuat::from_rotation_y(a); - // t.rotation = r * t.rotation; - // t - // }); - // runtime.register_function("transformRotateZ", |mut t: Transform, a: f64| { - // let r = DQuat::from_rotation_z(a); - // t.rotation = r * t.rotation; - // t - // }); - - // // utils already in your file — log and time - // runtime.register_function("log", |msg: String| { - // println!("[Script] {}", msg); - // }); - // runtime.register_function("time", || { - // std::time::SystemTime::now() - // .duration_since(std::time::UNIX_EPOCH) - // .unwrap() - // .as_secs_f64() - // }); - - Self { + // Register modules + math::register_math_functions(&mut runtime)?; + input::InputState::register_input_modules(&mut runtime)?; + entity::register_model_props_module(&mut runtime)?; + + // Register utility functions + runtime.register_function("log", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + let msg = args.get(0) + .and_then(|v| v.as_str()) + .unwrap_or("undefined"); + println!("[Script] {}", msg); + Ok(serde_json::Value::Null) + })?; + + runtime.register_function("time", |_args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + let time = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs_f64(); + Ok(serde_json::Value::Number(serde_json::Number::from_f64(time).unwrap())) + })?; + + Ok(Self { runtime, compiled_scripts: HashMap::new(), - script_scopes: HashMap::new(), - } + entity_script_data: HashMap::new(), + }) } pub fn init_entity_script( @@ -283,8 +202,8 @@ impl ScriptManager { input_state: &input::InputState, ) -> anyhow::Result<()> { if let Ok(mut q) = world.query_one::<&AdoptedEntity>(entity_id) { - if let Some(adopted) = q.get() { - log_once::debug_once!( + if let Some(adopted) = q.get() { + log_once::debug_once!( "init_entity_script: '{}' for entity {:?} -> label='{}' path='{}'", script_name, entity_id, @@ -296,42 +215,43 @@ impl ScriptManager { log_once::debug_once!("init_entity_script: '{}' for entity {:?}", script_name, entity_id); } - if let Some(ast) = self.compiled_scripts.get(script_name) { - let mut scope = Scope::new(); + if let Some(module) = self.compiled_scripts.get(script_name) { + // Prepare script data + let mut script_data = serde_json::Map::new(); - if let Ok(transform) = world.query_one_mut::<&mut Transform>(entity_id) { - scope.push("transform", *transform); + // Add transform data + if let Ok(mut transform_query) = world.query_one::<&Transform>(entity_id) { + if let Some(transform) = transform_query.get() { + script_data.insert("transform".to_string(), serde_json::to_value(transform)?); + } } - if let Ok(properties) = world.query_one_mut::<&mut ModelProperties>(entity_id) { - scope.push("entity", properties.clone()); + // Add entity properties + if let Ok(mut properties_query) = world.query_one::<&ModelProperties>(entity_id) { + if let Some(properties) = properties_query.get() { + script_data.insert("entity".to_string(), serde_json::to_value(properties)?); + } else { + let default_props = ModelProperties::default(); + script_data.insert("entity".to_string(), serde_json::to_value(&default_props)?); + } } else { let default_props = ModelProperties::default(); - // default_props.set_property(String::from("speed"), PropertyValue::Float(1.0)); - scope.push("entity", default_props); + script_data.insert("entity".to_string(), serde_json::to_value(&default_props)?); } - scope.push("input", input_state.clone()); + // Add input state + let serializable_input = input::SerializableInputState::from(input_state); + script_data.insert("input".to_string(), serde_json::to_value(&serializable_input)?); - if let Ok(_) = self.engine.call_fn::<()>(&mut scope, ast, "init", ()) { + // Call init function if it exists - specify return type + let args: Vec<serde_json::Value> = Vec::new(); + if let Ok(_result) = self.runtime.call_function::<serde_json::Value>(Some(module), "load", &args) { log::debug!("Called init for entity {:?}", entity_id); - - if let Some(properties_from_scope) = scope.get_value::<ModelProperties>("entity") { - if let Ok(properties) = world.query_one_mut::<&mut ModelProperties>(entity_id) { - *properties = properties_from_scope; - } else { - let _ = world.insert_one(entity_id, properties_from_scope); - } - } - - if let Some(transform_from_scope) = scope.get_value::<Transform>("transform") { - if let Ok(transform) = world.query_one_mut::<&mut Transform>(entity_id) { - *transform = transform_from_scope; - } - } } - self.script_scopes.insert(entity_id, scope); + // Store script data for this entity + self.entity_script_data.insert(entity_id, serde_json::Value::Object(script_data)); + Ok(()) } else { Err(anyhow::anyhow!("Script '{}' not found", script_name)) @@ -346,41 +266,44 @@ impl ScriptManager { input_state: &input::InputState, dt: f32, ) -> anyhow::Result<()> { - if let Some(ast) = self.compiled_scripts.get(script_name) { - if let Some(scope) = self.script_scopes.get_mut(&entity_id) { - if let Ok(transform) = world.query_one_mut::<&mut Transform>(entity_id) { - scope.set_value("transform", *transform); + if let Some(module) = self.compiled_scripts.get(script_name) { + // Update script data + let mut script_data = serde_json::Map::new(); + + // Update transform data + if let Ok(mut transform_query) = world.query_one::<&Transform>(entity_id) { + if let Some(transform) = transform_query.get() { + script_data.insert("transform".to_string(), serde_json::to_value(transform)?); } + } - if let Ok(mut properties_query) = world.query_one::<&ModelProperties>(entity_id) { - if let Some(properties) = properties_query.get() { - scope.set_value("entity", properties.clone()); - } + // Update entity properties + if let Ok(mut properties_query) = world.query_one::<&ModelProperties>(entity_id) { + if let Some(properties) = properties_query.get() { + script_data.insert("entity".to_string(), serde_json::to_value(properties)?); } + } - scope.set_value("input", input_state.clone()); - - match self.engine.call_fn::<()>(scope, ast, "update", (dt,)) { - Ok(_) => { - if let Some(transform_from_scope) = scope.get_value::<Transform>("transform") { - if let Ok(transform) = world.query_one_mut::<&mut Transform>(entity_id) { - *transform = transform_from_scope; - } - } - - if let Some(properties_from_scope) = scope.get_value::<ModelProperties>("entity") { - if let Ok(properties) = world.query_one_mut::<&mut ModelProperties>(entity_id) { - *properties = properties_from_scope; - } - } - } - Err(e) => { - log_once::error_once!("Script execution error for entity {:?}: {}", entity_id, e); - } + // Update input state + let serializable_input = input::SerializableInputState::from(input_state); + script_data.insert("input".to_string(), serde_json::to_value(&serializable_input)?); + + // Call update function if it exists - specify return type and fix parameter passing + let dt_value = serde_json::Value::Number(serde_json::Number::from_f64(dt as f64).unwrap()); + match self.runtime.call_function::<serde_json::Value>(Some(module), "update", &vec![dt_value]) { + Ok(_result) => { + log::trace!("Called update for entity {:?}", entity_id); + // Here you would need to extract any modified data from the script + // and update the world accordingly. This depends on how rustyscript + // handles data exchange between Rust and JS. + } + Err(e) => { + log_once::error_once!("Script execution error for entity {:?}: {}", entity_id, e); } - } else { - log_once::error_once!("Unable to get scope of entity {:?}", entity_id); } + + // Update stored script data + self.entity_script_data.insert(entity_id, serde_json::Value::Object(script_data)); } else { log_once::error_once!("Unable to fetch compiled scripts for entity {:?}. Script Name: {}", entity_id, script_name); } @@ -388,14 +311,19 @@ impl ScriptManager { } pub fn load_script_from_source(&mut self, script_name: &String, script_content: &String) -> anyhow::Result<String> { - let ast = self.engine.compile(&script_content).map_err(|e| { - log::error!("Compiling AST for script [{}] returned an error: {}", script_name, e); - e - })?; - self.compiled_scripts.insert(script_name.clone(), ast); - - log::info!("Loaded script: {}", script_name); - Ok(script_name.to_string()) + let module = Module::new(&script_name, script_content); + + match self.runtime.load_module(&module) { + Ok(module) => { + self.compiled_scripts.insert(script_name.clone(), module); + log::info!("Loaded script: {}", script_name); + Ok(script_name.to_string()) + } + Err(e) => { + log::error!("Compiling module for script [{}] returned an error: {}", script_name, e); + Err(e.into()) + } + } } pub fn load_script(&mut self, script_path: &PathBuf) -> anyhow::Result<String> { @@ -406,31 +334,22 @@ impl ScriptManager { .to_string_lossy() .to_string(); - let ast = self.engine.compile(&script_content).map_err(|e| { - log::error!("Compiling AST for script path [{}] returned an error: {}", script_path.display(), e); - e - })?; - self.compiled_scripts.insert(script_name.clone(), ast); - - log::info!("Loaded script: {}", script_name); - Ok(script_name) + let module = Module::new(&script_name, &script_content); + + match self.runtime.load_module(&module) { + Ok(module) => { + self.compiled_scripts.insert(script_name.clone(), module); + log::info!("Loaded script: {}", script_name); + Ok(script_name) + } + Err(e) => { + log::error!("Compiling module for script path [{}] returned an error: {}", script_path.display(), e); + Err(e.into()) + } + } } pub fn remove_entity_script(&mut self, entity_id: hecs::Entity) { - self.script_scopes.remove(&entity_id); + self.entity_script_data.remove(&entity_id); } - - // maybe useful later??? - // pub fn reload_script( - // &mut self, - // script_name: &str, - // script_path: &PathBuf, - // ) -> anyhow::Result<()> { - // let script_content = fs::read_to_string(script_path)?; - // let ast = self.engine.compile(&script_content)?; - // self.compiled_scripts.insert(script_name.to_string(), ast); - - // log::info!("Reloaded script: {}", script_name); - // Ok(()) - // } } @@ -31,7 +31,6 @@ use serde::{Deserialize, Serialize}; use dropbear_engine::model::Model; use dropbear_engine::starter::plane::PlaneBuilder; use dropbear_engine::utils::{ResourceReference, ResourceReferenceType}; -use crate::build::build; use crate::camera::CameraType; #[cfg(feature = "editor")] use crate::editor::EditorTab; @@ -1 +1,10 @@ -// todo +// dropbear-engine script template for eucalyptus +import * as Dropbear from "./dropbear"; + +export function onLoad() { + +} + +export function onUpdate(dt: Number) { + +} @@ -1,46 +0,0 @@ -// Modules for the dropbear-engine scripting component - -type TransformT = any; -type Vec3T = any; -type QuatT = any; - -function hostFn(name: string) { - const fn = (globalThis as any)[name]; - if (typeof fn !== "function") { - throw new Error(`Host function ${name}() is not available. Make sure the runtime exposes it.`); - } - return fn; -} - -const Transform = { - create: (): TransformT => hostFn("createTransform")(), - translate: (transform: TransformT, translation: [number, number, number] | number[]): TransformT => - hostFn("transformTranslate")(transform, translation), - rotateX: (transform: TransformT, angle: number): TransformT => - hostFn("transformRotateX")(transform, angle), - rotateY: (transform: TransformT, angle: number): TransformT => - hostFn("transformRotateY")(transform, angle), - rotateZ: (transform: TransformT, angle: number): TransformT => - hostFn("transformRotateZ")(transform, angle), - scale: (transform: TransformT, scale: number | [number, number, number]): TransformT => - hostFn("transformScale")(transform, scale), - matrix: (transform: TransformT): TransformT => hostFn("transformMatrix")(transform), -}; - -const Vec3 = { - create: (x = 0, y = 0, z = 0): Vec3T => hostFn("createVec3")(x, y, z), - zero: (): Vec3T => hostFn("createVec3")(0, 0, 0), - one: (): Vec3T => hostFn("createVec3")(1, 1, 1), -}; - -const Quaternion = { - identity: (): QuatT => hostFn("createQuatIdentity")(), - fromEuler: (x: number, y: number, z: number): QuatT => hostFn("createQuatFromEuler")(x, y, z), -}; - -// make global -export { Transform, Vec3, Quaternion }; - -globalThis.Transform = Transform; -globalThis.Vec3 = Vec3; -globalThis.Quaternion = Quaternion; @@ -1 +1 @@ -Subproject commit 316913058a1f25b68b52e06c3d9069d3d3c73ffc +Subproject commit 36de5a65d6cb8c77d30d3d3170ac5794d94d7e01