From ba441d12fae90d58eb3a14b217f7a2704100d725 Mon Sep 17 00:00:00 2001 From: QueenOfSquiggles <8940604+QueenOfSquiggles@users.noreply.github.com> Date: Thu, 30 Nov 2023 13:11:42 -0600 Subject: [PATCH] Added a few more features --- Cargo.toml | 2 +- assets/icons/gui_interact.svg | 62 +++++++ squiggles_core.gdextension | 44 +++-- src/editor_plugin.rs | 17 ++ src/game_globals.rs | 133 ++++++++++++++ src/game_settings/graphics.rs | 121 +++++++++++++ src/game_settings/mod.rs | 39 +++++ .../custom_world_environment.rs | 44 +++++ src/godot_replacements/mod.rs | 1 + src/interaction.rs | 37 +--- src/lib.rs | 26 ++- src/serialization.rs | 165 ++++++++++++++++++ src/utility_nodes/gui_interact.rs | 128 ++++++++++++++ src/utility_nodes/mod.rs | 1 + 14 files changed, 764 insertions(+), 56 deletions(-) create mode 100644 assets/icons/gui_interact.svg create mode 100644 src/editor_plugin.rs create mode 100644 src/game_globals.rs create mode 100644 src/game_settings/graphics.rs create mode 100644 src/game_settings/mod.rs create mode 100644 src/godot_replacements/custom_world_environment.rs create mode 100644 src/godot_replacements/mod.rs create mode 100644 src/serialization.rs create mode 100644 src/utility_nodes/gui_interact.rs create mode 100644 src/utility_nodes/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 300c6da..f0a8811 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,5 +7,5 @@ edition = "2021" crate-type = ["cdylib"] # Compile this crate to a dynamic C library. [dependencies] -godot = { git = "https://github.com/godot-rust/gdext", rev="d3745f8" } +godot = { git = "https://github.com/godot-rust/gdext", rev="05a1b09" } once_cell = "1.18.0" diff --git a/assets/icons/gui_interact.svg b/assets/icons/gui_interact.svg new file mode 100644 index 0000000..2c5be59 --- /dev/null +++ b/assets/icons/gui_interact.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + diff --git a/squiggles_core.gdextension b/squiggles_core.gdextension index bab373f..15247dc 100644 --- a/squiggles_core.gdextension +++ b/squiggles_core.gdextension @@ -3,33 +3,39 @@ entry_symbol = "gdext_rust_init" compatibility_minimum = 4.1 [libraries] -linux.debug.x86_64 = "res://squiggles_core/target/debug/libsquiggles_core.so" -windows.debug.x86_64 = "res://squiggles_core/target/debug/squiggles_core.dll" -macos.debug = "res://squiggles_core/target/debug/libsquiggles_core.dylib" -macos.debug.arm64 = "res://squiggles_core/target/debug/libsquiggles_core.dylib" - -linux.release.x86_64 = "res://squiggles_core/target/release/libsquiggles_core.so" -windows.release.x86_64 = "res://squiggles_core/target/release/squiggles_core.dll" -macos.release = "res://squiggles_core/target/release/libsquiggles_core.dylib" -macos.release.arm64 = "res://squiggles_core/target/release/libsquiggles_core.dylib" +;; Debug Binaries +linux.debug.x86_64 = "res://addons/squiggles_core/target/debug/libsquiggles_core.so" +windows.debug.x86_64 = "res://addons/squiggles_core/target/debug/squiggles_core.dll" +macos.debug = "res://addons/squiggles_core/target/debug/libsquiggles_core.dylib" +macos.debug.arm64 = "res://addons/squiggles_core/target/debug/libsquiggles_core.dylib" + +;; Release Binaries +linux.release.x86_64 = "res://addons/squiggles_core/target/release/libsquiggles_core.so" +windows.release.x86_64 = "res://addons/squiggles_core/target/release/squiggles_core.dll" +macos.release = "res://addons/squiggles_core/target/release/libsquiggles_core.dylib" +macos.release.arm64 = "res://addons/squiggles_core/target/release/libsquiggles_core.dylib" [icons] ;; camera.rs -CameraBrain3D = "res://squiggles_core/assets/icons/camera_brain.svg" -VirtualCamera3D = "res://squiggles_core/assets/icons/virtual_camera.svg" +CameraBrain3D = "res://addons/squiggles_core/assets/icons/camera_brain.svg" +VirtualCamera3D = "res://addons/squiggles_core/assets/icons/virtual_camera.svg" ;; interaction.rs -InteractRaycast3D = "res://squiggles_core/assets/icons/interaction_raycast3d.svg" -InteractArea3D = "res://squiggles_core/assets/icons/interaction_area3d.svg" -InteractionObjectArea3D = "res://squiggles_core/assets/icons/interact_object_area3d.svg" -InteractionObjectStaticBody3D = "res://squiggles_core/assets/icons/interact_object_static3d.svg" -InteractionObjectCharacterBody3D = "res://squiggles_core/assets/icons/interact_object_character3d.svg" +InteractRaycast3D = "res://addons/squiggles_core/assets/icons/interaction_raycast3d.svg" +InteractArea3D = "res://addons/squiggles_core/assets/icons/interaction_area3d.svg" +InteractionObjectArea3D = "res://addons/squiggles_core/assets/icons/interact_object_area3d.svg" +InteractionObjectStaticBody3D = "res://addons/squiggles_core/assets/icons/interact_object_static3d.svg" +InteractionObjectCharacterBody3D = "res://addons/squiggles_core/assets/icons/interact_object_character3d.svg" ;; state_machine.rs -FiniteStateMachine = "res://squiggles_core/assets/icons/fsm.svg" -FiniteState = "res://squiggles_core/assets/icons/fsm_state.svg" -FiniteSubStateMachine = "res://squiggles_core/assets/icons/fsm_sub_state.svg" \ No newline at end of file +FiniteStateMachine = "res://addons/squiggles_core/assets/icons/fsm.svg" +FiniteState = "res://addons/squiggles_core/assets/icons/fsm_state.svg" +FiniteSubStateMachine = "res://addons/squiggles_core/assets/icons/fsm_sub_state.svg" + +;; utility_nodes/gui_interact.rs + +GuiInteract = "res://addons/squiggles_core/assets/icons/gui_interact.svg" \ No newline at end of file diff --git a/src/editor_plugin.rs b/src/editor_plugin.rs new file mode 100644 index 0000000..40c7874 --- /dev/null +++ b/src/editor_plugin.rs @@ -0,0 +1,17 @@ +use godot::{engine::*, prelude::*}; +use once_cell::sync::Lazy; + +use crate::game_globals::CoreGlobals; + +pub static SINGLETON_CORE_GLOBALS: Lazy = Lazy::new(|| StringName::from("CoreGlobals")); + +pub fn register_engine_elements() { + Engine::singleton().register_singleton( + SINGLETON_CORE_GLOBALS.clone(), + CoreGlobals::alloc_gd().upcast(), + ); +} + +pub fn unregister_engine_elements() { + Engine::singleton().unregister_singleton(SINGLETON_CORE_GLOBALS.clone()); +} diff --git a/src/game_globals.rs b/src/game_globals.rs new file mode 100644 index 0000000..11465ab --- /dev/null +++ b/src/game_globals.rs @@ -0,0 +1,133 @@ +use godot::{ + builtin::meta::{ConvertError, GodotConvert}, + engine::*, + prelude::*, +}; + +use crate::{ + editor_plugin::SINGLETON_CORE_GLOBALS, game_settings::SquigglesCoreConfig, + serialization::SquigglesSerialized, +}; + +const PROJECT_SETTINGS_NAMESPACE: &str = "addons/squiggles_core/"; +const S_LOADERS: &str = "loaders"; +const S_GAME_SETTINGS: &str = "game_settings"; + +fn get_setting_name(name: &str) -> GString { + (String::from(PROJECT_SETTINGS_NAMESPACE) + name).to_godot() +} + +#[derive(GodotClass)] +#[class(tool, base=Object)] +pub struct CoreGlobals { + #[var] + config: Gd, + #[base] + base: Base, +} + +#[godot_api] +impl IObject for CoreGlobals { + fn init(base: Base) -> Self { + // let mut zelf = Self { config: None, base }; + let mut possible_config: Option> = None; + match Self::get_or_init_default(S_LOADERS, PackedStringArray::new()) { + Err(err) => godot_warn!("Conversion Error: {}", err.to_string()), + Ok(loaders) => { + for item in loaders.as_slice().iter() { + godot_print!("Found loader entry: {}", item); + } + } + } + // try load configuration file + if let Ok(config_path) = + Self::get_or_init_default(S_GAME_SETTINGS, "res://squiggles_config.tres".to_godot()) + { + if let Some(config_resource) = ResourceLoader::singleton().load(config_path.clone()) { + let opt_res: Result, Gd> = + config_resource.try_cast(); + if let Ok(valid_resource) = opt_res { + possible_config = Some(valid_resource); + } + } else { + let msg = format!("Expected an instance of `SquigglesCoreConfig` resource to be at path: \"{}\". Either create the resource at that location, or update the `{}` setting in your project settings.", config_path, S_GAME_SETTINGS); + godot_error!("{}", msg); + godot_print!("{}", msg); + } + } + let mut zelf = Self { + config: possible_config.unwrap_or(SquigglesCoreConfig::new_gd()), + base, + }; + zelf.reload_globals(); + + zelf + } +} + +#[godot_api] +impl CoreGlobals { + #[signal] + fn global_serialize() {} + + #[signal] + fn global_deserialize() {} + + #[func] + fn get_setting(&self, name: String, default_value: Variant) -> Variant { + let result = Self::get_or_init_default(name.as_str(), default_value); + match result { + Ok(value) => value, + Err(_) => Variant::nil(), + } + } + #[func] + fn save_globals(&mut self) { + self.serialize(); + } + + #[func] + fn reload_globals(&mut self) { + self.deserialize(); + } + + // internal specialized functions + + pub fn get_or_init_default( + name: &str, + default: T, + ) -> Result { + let mut project = ProjectSettings::singleton(); + let value_volatile = project.get_setting(get_setting_name(name)); + + if value_volatile.is_nil() || value_volatile.get_type() != default.to_variant().get_type() { + project.set_setting(get_setting_name(name), default.to_variant()); + Ok(default) + } else { + // no longer volatile + T::try_from_variant(&value_volatile) + } + } + + pub fn singleton() -> Gd { + let Some(vol) = Engine::singleton().get_singleton(SINGLETON_CORE_GLOBALS.clone()) else { + panic!("Failed to find engine singleton for CoreGlobals. You must access this after it is registered!"); + }; + let res_core: Result, Gd<_>> = vol.try_cast(); + let Ok(core) = res_core else { + panic!("Failed to cast engine singleton for CoreGlobals. This should never happen!"); + }; + core + } +} + +impl SquigglesSerialized for CoreGlobals { + fn serialize(&mut self) { + // I'm comfy using unwrap because this struct should never be constructed outside of the init function, which assigns the + self.config.bind_mut().serialize(); + } + + fn deserialize(&mut self) { + self.config.bind_mut().deserialize(); + } +} diff --git a/src/game_settings/graphics.rs b/src/game_settings/graphics.rs new file mode 100644 index 0000000..e70fa06 --- /dev/null +++ b/src/game_settings/graphics.rs @@ -0,0 +1,121 @@ +use godot::{engine::display_server::WindowMode, prelude::*}; + +use crate::serialization::{SaveDataBuilder, SquigglesSerialized}; + +#[derive(GodotClass)] +#[class(tool, base=Resource)] +pub struct GameGraphicsSettings { + #[export] + use_ssao: bool, + #[export] + use_bloom: bool, + #[export] + use_sdfgi: bool, + #[export] + use_ssil: bool, + #[export] + use_ssr: bool, + #[export] + value_brightness: f32, + #[export] + value_contrast: f32, + #[export] + value_saturation: f32, + #[export] + value_exposure: f32, + #[export(enum=(Windowed=0, Minimized=1, Maximized=2, Fullscreen=3, ExclusiveFullscreen=4))] + window_fullscreen_mode: i32, + #[base] + base: Base, +} + +#[godot_api] +impl IResource for GameGraphicsSettings { + fn init(base: Base) -> Self { + Self { + use_ssao: true, + base, + use_bloom: true, + use_sdfgi: true, + use_ssil: false, + use_ssr: false, + value_brightness: 1.0, + value_contrast: 1.0, + value_saturation: 1.0, + value_exposure: 1.0, + window_fullscreen_mode: WindowMode::WINDOW_MODE_MAXIMIZED.ord(), + } + } +} + +#[godot_api] +impl GameGraphicsSettings {} + +const GRAPHICS_SAVE_PATH: &str = "user://core/graphics.json"; + +impl SquigglesSerialized for GameGraphicsSettings { + fn serialize(&mut self) { + let mut save = SaveDataBuilder::alloc_gd(); + let mut bind = save.bind_mut(); + bind.set_value("use_ssao".to_godot(), self.use_ssao.to_variant()); + bind.set_value("use_bloom".to_godot(), self.use_bloom.to_variant()); + bind.set_value("use_sdfgi".to_godot(), self.use_sdfgi.to_variant()); + bind.set_value("use_ssil".to_godot(), self.use_ssil.to_variant()); + bind.set_value("use_ssr".to_godot(), self.use_ssr.to_variant()); + bind.set_value( + "value_brightness".to_godot(), + self.value_brightness.to_variant(), + ); + bind.set_value( + "value_contrast".to_godot(), + self.value_contrast.to_variant(), + ); + bind.set_value( + "value_saturation".to_godot(), + self.value_saturation.to_variant(), + ); + bind.set_value( + "value_exposure".to_godot(), + self.value_exposure.to_variant(), + ); + bind.set_value( + "window_fullscreen_mode".to_godot(), + self.window_fullscreen_mode.to_variant(), + ); + bind.save(GRAPHICS_SAVE_PATH.into_godot()); + } + + fn deserialize(&mut self) { + let Some(mut load) = SaveDataBuilder::try_load_file(GRAPHICS_SAVE_PATH.into_godot()) else { + return; + }; + // use_ssao: bool, + // use_bloom: bool, + // use_sdfgi: bool, + // use_ssil: bool, + // use_ssr: bool, + // value_brightness: f32, + // value_contrast: f32, + // value_saturation: f32, + // value_exposure: f32, + // window_fullscreen_mode: i32, + let mut bind = load.bind_mut(); + self.use_ssao = bind.internal_get_value("use_ssao".to_godot(), self.use_ssao.to_godot()); + self.use_bloom = bind.internal_get_value("use_bloom".to_godot(), self.use_bloom.to_godot()); + self.use_sdfgi = bind.internal_get_value("use_sdfgi".to_godot(), self.use_sdfgi.to_godot()); + self.use_ssil = bind.internal_get_value("use_ssil".to_godot(), self.use_ssil.to_godot()); + self.use_ssr = bind.internal_get_value("use_ssr".to_godot(), self.use_ssr.to_godot()); + self.value_brightness = + bind.internal_get_value("value_brightness".to_godot(), self.value_brightness); + self.value_contrast = + bind.internal_get_value("value_contrast".to_godot(), self.value_contrast); + self.value_saturation = + bind.internal_get_value("value_saturation".to_godot(), self.value_saturation); + self.value_exposure = + bind.internal_get_value("value_exposure".to_godot(), self.value_exposure); + self.window_fullscreen_mode = bind.internal_get_value( + "window_fullscreen_mode".to_godot(), + self.window_fullscreen_mode, + ); + } +} diff --git a/src/game_settings/mod.rs b/src/game_settings/mod.rs new file mode 100644 index 0000000..25b65a3 --- /dev/null +++ b/src/game_settings/mod.rs @@ -0,0 +1,39 @@ +use godot::prelude::*; + +use crate::serialization::SquigglesSerialized; + +use self::graphics::GameGraphicsSettings; + +pub mod graphics; + +#[derive(GodotClass)] +#[class(tool, base=Resource)] +pub struct SquigglesCoreConfig { + #[export] + graphics: Gd, + + #[base] + base: Base, +} +#[godot_api] +impl IResource for SquigglesCoreConfig { + fn init(base: Base) -> Self { + Self { + base, + graphics: GameGraphicsSettings::new_gd(), + } + } +} + +#[godot_api] +impl SquigglesCoreConfig {} + +impl SquigglesSerialized for SquigglesCoreConfig { + fn serialize(&mut self) { + self.graphics.bind_mut().serialize(); + } + + fn deserialize(&mut self) { + self.graphics.bind_mut().deserialize(); + } +} diff --git a/src/godot_replacements/custom_world_environment.rs b/src/godot_replacements/custom_world_environment.rs new file mode 100644 index 0000000..a4c80ce --- /dev/null +++ b/src/godot_replacements/custom_world_environment.rs @@ -0,0 +1,44 @@ +use godot::{ + engine::{Environment, IWorldEnvironment, WorldEnvironment}, + prelude::*, +}; + +use crate::game_globals::CoreGlobals; + +#[derive(GodotClass)] +#[class(init, base=WorldEnvironment)] +struct WorldEnvironmentSettingsCompliant { + #[export] + force_override: bool, + #[base] + base: Base, +} + +#[godot_api] +impl IWorldEnvironment for WorldEnvironmentSettingsCompliant { + fn ready(&mut self) { + // TODO: omfg if else {...}; is so lovely on the eyes. I wanna convert some of the more deeply nested fuctions into this pattern if possible. + let option_env = self.base.get_environment(); + let mut env = Environment::new(); + if option_env.is_some() && !self.force_override { + // if there is an existing environment and we are not forcing an override, let that be the environment + return; + } + if let Some(n_env) = option_env { + env = n_env; + } + + let gd_gfx = CoreGlobals::singleton() + .bind() + .get_config() + .bind() + .get_graphics(); + let gfx = gd_gfx.bind(); + env.set_glow_enabled(gfx.get_use_bloom()); + + self.base.set_environment(env); + } +} + +#[godot_api] +impl WorldEnvironmentSettingsCompliant {} diff --git a/src/godot_replacements/mod.rs b/src/godot_replacements/mod.rs new file mode 100644 index 0000000..8ad540e --- /dev/null +++ b/src/godot_replacements/mod.rs @@ -0,0 +1 @@ +pub mod custom_world_environment; diff --git a/src/interaction.rs b/src/interaction.rs index 6a70776..0c81029 100644 --- a/src/interaction.rs +++ b/src/interaction.rs @@ -3,8 +3,6 @@ use godot::engine::{Area3D, CharacterBody3D, IArea3D, IRayCast3D, RayCast3D, Sta use godot::prelude::*; use once_cell::sync::Lazy; -// TODO: add signals to the interactobjects for on_select and on_deselect so users can just hook into signals without needing to implement other stuff if they so choose - // these are accessed by calling .clone(). Normally I'd dislike this, but StringName is ref-counted so duplicating it is almost completely free static METHOD_SELECT: Lazy = Lazy::new(|| StringName::from("on_select")); static METHOD_DESELECT: Lazy = Lazy::new(|| StringName::from("on_deselect")); @@ -20,6 +18,7 @@ static SIGNAL_ON_DESELECTED: Lazy = Lazy::new(|| StringName::from("o struct InteractRaycast3D { #[export] filter_groups: PackedStringArray, + #[var] target: Option>, #[base] base: Base, @@ -30,6 +29,7 @@ struct InteractRaycast3D { struct InteractArea3D { #[export] filter_groups: PackedStringArray, + #[var] target: Option>, #[base] base: Base, @@ -323,36 +323,3 @@ impl InteractionObjectCharacterBody3D { GString::from("No name given") } } - -#[cfg(test)] -mod tests { - // use super::*; - - #[test] - fn test_raycast_detect() { - // Error thrown because this requires the godot engine for testing - // basically, testing requires a scene tree, which requires godot. So as a library we can't really test directly. Unless we want to test something that doesn't rely on Godot's engine stuff. - // - - - - - - - - // use godot::{ - // engine::{CollisionShape3D, SphereShape3D}, - // obj::UserClass, - // }; - // let mut ray: Gd = InteractRaycast3D::alloc_gd(); - // let mut object = InteractionObjectArea3D::alloc_gd(); - // ray.set_collide_with_bodies(true); - // ray.set_global_position(Vector3::new(0.0, 0.0, -1.0)); - // ray.set_target_position(Vector3::new(0.0, 0.0, 5.0)); - // object.set_global_position(Vector3::new(0.0, 0.0, 1.0)); - // let mut obj_coll = CollisionShape3D::new_alloc(); - // obj_coll.set_shape(SphereShape3D::new().upcast()); - // object.add_child(obj_coll.upcast()); - - // let scene = SceneTree::new_alloc(); - // if let Some(mut root) = scene.get_root() { - // root.add_child(ray.clone().upcast()); - // root.add_child(object.clone().upcast()); - // ray.force_raycast_update(); - // assert_eq!(ray.is_colliding(), true); - // } - } -} diff --git a/src/lib.rs b/src/lib.rs index 0035cc6..7b91b55 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,34 @@ +// crate-wide warnings alterations +#![allow(dead_code)] // a lot of elements are considered unused because Godot grabs it over FFI. + use godot::prelude::*; +// module specifications pub mod camera; +pub mod editor_plugin; pub mod error_handling; +pub mod game_globals; +pub mod game_settings; +pub mod godot_replacements; pub mod interaction; +pub mod serialization; pub mod state_machine; +pub mod utility_nodes; + +// extension loading struct SquigglesCore; #[gdextension] -unsafe impl ExtensionLibrary for SquigglesCore {} +unsafe impl ExtensionLibrary for SquigglesCore { + fn on_level_init(level: InitLevel) { + if level == InitLevel::Scene { + editor_plugin::register_engine_elements(); + } + } + + fn on_level_deinit(level: InitLevel) { + if level == InitLevel::Scene { + editor_plugin::unregister_engine_elements(); + } + } +} diff --git a/src/serialization.rs b/src/serialization.rs new file mode 100644 index 0000000..30d261c --- /dev/null +++ b/src/serialization.rs @@ -0,0 +1,165 @@ +use std::collections::HashMap; + +use godot::{ + engine::{file_access::ModeFlags, DirAccess, FileAccess, Json, ProjectSettings}, + prelude::*, +}; + +const INTERNAL_PREFIX: &str = "__internal__"; + +#[derive(GodotClass)] +#[class(base=Object)] +pub struct SaveDataBuilder { + data: Dictionary, + child_builders: HashMap>, + base: Base, +} + +#[godot_api] +impl IObject for SaveDataBuilder { + fn init(base: Base) -> Self { + Self { + data: Dictionary::new(), + child_builders: HashMap::new(), + base, + } + } +} +#[godot_api] +pub impl SaveDataBuilder { + #[func] + pub fn set_value(&mut self, key: GString, value: Variant) { + self.data.set(key, value); + } + + #[func] + pub fn get_value(&mut self, key: GString) -> Variant { + self.data.get_or_nil(key) + } + + #[func] + pub fn get_value_or_default(&mut self, key: GString, default: Variant) -> Variant { + if let Some(value) = self.data.get(key) { + return value; + } + default + } + + #[func] + pub fn get_child_builder(&mut self, key: GString) -> Gd { + let mut n_builder = SaveDataBuilder::alloc_gd(); + if let Some(child) = self.child_builders.get_mut(&key) { + n_builder = child.clone(); + } else { + self.child_builders.insert(key, n_builder.clone()); + } + n_builder + } + + #[func] + pub fn save(&mut self, file_path: GString) -> bool { + let abs_path = ProjectSettings::singleton().globalize_path(file_path.clone()); + if let Some(base_dir) = std::path::Path::new(abs_path.to_string().as_str()).parent() { + if let Some(valid_base_dir) = base_dir.to_str() { + if !DirAccess::dir_exists_absolute(GString::from(valid_base_dir)) { + DirAccess::make_dir_recursive_absolute(GString::from(valid_base_dir)); + } + } + } + let Some(mut file) = FileAccess::open(file_path.clone(), ModeFlags::WRITE) else { + godot_warn!("Failed to access file {}", file_path); + return false; + }; + let text = Json::stringify(self.get_as_dict().to_variant()); + file.store_string(text); + true + } + + #[func] + pub fn load(&mut self, file_path: GString) -> bool { + let Some(file) = FileAccess::open(file_path.clone(), ModeFlags::READ) else { + godot_warn!("Failed to access file {}", file_path); + return false; + }; + let opt_cast = Json::parse_string(file.get_as_text()); + if let Ok(data) = Dictionary::try_from_variant(&opt_cast) { + for entry in data.iter_shared() { + let skey = GString::from_variant(&entry.0); + if skey.to_string().starts_with(INTERNAL_PREFIX) { + } else { + self.data.set(skey, entry.1); + } + } + }; + + true + } + + #[func] + pub fn load_from(dict: Dictionary) -> Gd { + let mut data = SaveDataBuilder::alloc_gd(); + for entry in dict.iter_shared() { + let skey = GString::from_variant(&entry.0); + if skey.to_string().starts_with(INTERNAL_PREFIX) { + // attempts to construct an internal save data builder (which is effecitively a sub-layer in the JSON) + if let Ok(dict) = Dictionary::try_from_variant(&entry.1) { + let child = SaveDataBuilder::load_from(dict); + let i_key = skey.to_string().replace(INTERNAL_PREFIX, ""); + data.bind_mut() + .child_builders + .insert(i_key.to_godot(), child); + } else { + godot_warn!("Found SaveDataBuilder entry that is corrupted. Please ensure this JSON data is correct. Key=\"{}\"; expected dictionary value. Found: {}", skey, entry.1); + } + } else { + // loads a simple data value + data.bind_mut().data.set(skey, entry.1); + } + } + data + } + + pub fn try_load_file(file_path: GString) -> Option> { + let mut result = SaveDataBuilder::alloc_gd(); + if !result.bind_mut().load(file_path) { + None + } else { + Some(result) + } + } + + #[func] + pub fn get_as_dict(&mut self) -> Dictionary { + let mut dict = Dictionary::new(); + for entry in self.data.iter_shared() { + dict.insert(entry.0, entry.1); + } + for entry in self.child_builders.iter_mut() { + let n_key: GString = + (String::from(INTERNAL_PREFIX) + &entry.0.to_string().to_owned()).to_godot(); + dict.insert(n_key, entry.1.bind_mut().get_as_dict()); + } + dict + } + + pub fn internal_get_value( + &mut self, + key: GString, + default_value: T, + ) -> T { + let value = self.get_value(key); + if value.is_nil() { + return default_value; + } + let Ok(valid) = T::try_from_variant(&value) else { + return default_value; + }; + valid + } +} + +/// Unfortunately this only can be used internally, but it grants access to serialization functions for all serializable functions +pub trait SquigglesSerialized { + fn serialize(&mut self); + fn deserialize(&mut self); +} diff --git a/src/utility_nodes/gui_interact.rs b/src/utility_nodes/gui_interact.rs new file mode 100644 index 0000000..2ad940d --- /dev/null +++ b/src/utility_nodes/gui_interact.rs @@ -0,0 +1,128 @@ +use godot::{ + engine::{AudioStream, Control}, + prelude::*, +}; + +type Sfx = Option>; +#[derive(GodotClass)] +#[class(init, base=Node)] +struct GuiInteract { + #[export] + auto_focus: bool, + #[export] + hover_sfx: Sfx, + #[export] + interact_sfx: Sfx, + #[export] + appear_sfx: Sfx, + #[export] + disappear_sfx: Sfx, + + audio_player: Option>, + #[base] + base: Base, +} + +#[godot_api] +impl INode for GuiInteract { + fn ready(&mut self) { + // load audio stream player + let player = AudioStreamPlayer::new_alloc(); + self.audio_player = Some(player.clone()); + self.base.add_child(player.upcast()); + // TODO: use a global setting to assign the audio bus and volume + + // get parent + let Some(node_parent) = self.base.get_parent() else { + return; + }; + let rcast: Result, _> = node_parent.try_cast(); + let Ok(mut control_parent) = rcast else { + return; + }; + + // grab focus + if self.auto_focus { + control_parent.grab_focus(); + } + + // connect signals + self.try_connect(&mut control_parent, "pressed", "on_interact"); + self.try_connect( + &mut control_parent, + "visibility_changed", + "on_visiblity_changed", + ); + self.try_connect(&mut control_parent, "focus_entered", "on_hover"); + self.try_connect(&mut control_parent, "mouse_entered", "on_mouse_enter"); + } +} + +#[godot_api] +impl GuiInteract { + #[func] + fn on_visiblity_changed(&mut self) { + if let Some(node_parent) = self.base.get_parent() { + let rcast: Result, _> = node_parent.try_cast(); + if let Ok(parent) = rcast { + if parent.is_visible() { + if let Some(sfx) = &self.appear_sfx { + self.try_play_sfx(sfx.clone()); + } + } else if let Some(sfx) = &self.disappear_sfx { + self.try_play_sfx(sfx.clone()); + } + } + } + } + + #[func] + fn on_mouse_enter(&mut self) { + // get parent + let Some(node_parent) = self.base.get_parent() else { + return; + }; + let rcast: Result, _> = node_parent.try_cast(); + let Ok(mut control_parent) = rcast else { + return; + }; + + // grab focus + if self.auto_focus { + control_parent.grab_focus(); + } + } + + #[func] + fn on_hover(&mut self) { + if let Some(sfx) = &self.hover_sfx { + self.try_play_sfx(sfx.clone()); + } + } + #[func] + fn on_interact(&mut self) { + if let Some(sfx) = &self.interact_sfx { + self.try_play_sfx(sfx.clone()); + } + } + + fn try_play_sfx(&mut self, stream: Gd) { + if let Some(player) = &mut self.audio_player { + player.stop(); + player.set_stream(stream); + player.play(); + } + } + fn try_connect(&self, node: &mut Gd, signal_name: &str, callable: &str) -> bool { + let signal = StringName::from(signal_name); + if !node.has_signal(signal.clone()) { + return false; + } + node.connect( + signal, + Callable::from_object_method(&self.to_gd(), callable), + ); + + true + } +} diff --git a/src/utility_nodes/mod.rs b/src/utility_nodes/mod.rs new file mode 100644 index 0000000..4345e1d --- /dev/null +++ b/src/utility_nodes/mod.rs @@ -0,0 +1 @@ +pub mod gui_interact;