Skip to content

Commit

Permalink
Added a few more features
Browse files Browse the repository at this point in the history
QueenOfSquiggles committed Nov 30, 2023
1 parent 1daf474 commit ba441d1
Showing 14 changed files with 764 additions and 56 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
62 changes: 62 additions & 0 deletions assets/icons/gui_interact.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
44 changes: 25 additions & 19 deletions squiggles_core.gdextension
Original file line number Diff line number Diff line change
@@ -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"
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"
17 changes: 17 additions & 0 deletions src/editor_plugin.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
use godot::{engine::*, prelude::*};
use once_cell::sync::Lazy;

use crate::game_globals::CoreGlobals;

pub static SINGLETON_CORE_GLOBALS: Lazy<StringName> = 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());
}
133 changes: 133 additions & 0 deletions src/game_globals.rs
Original file line number Diff line number Diff line change
@@ -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<SquigglesCoreConfig>,
#[base]
base: Base<Object>,
}

#[godot_api]
impl IObject for CoreGlobals {
fn init(base: Base<Self::Base>) -> Self {
// let mut zelf = Self { config: None, base };
let mut possible_config: Option<Gd<SquigglesCoreConfig>> = 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<SquigglesCoreConfig>, Gd<Resource>> =
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<T: GodotConvert + FromGodot + ToGodot>(
name: &str,
default: T,
) -> Result<T, ConvertError> {
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<CoreGlobals> {
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<CoreGlobals>, 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();
}
}
121 changes: 121 additions & 0 deletions src/game_settings/graphics.rs
Original file line number Diff line number Diff line change
@@ -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<Resource>,
}

#[godot_api]
impl IResource for GameGraphicsSettings {
fn init(base: Base<Self::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,
);
}
}
39 changes: 39 additions & 0 deletions src/game_settings/mod.rs
Original file line number Diff line number Diff line change
@@ -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<graphics::GameGraphicsSettings>,

#[base]
base: Base<Resource>,
}
#[godot_api]
impl IResource for SquigglesCoreConfig {
fn init(base: Base<Self::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();
}
}
44 changes: 44 additions & 0 deletions src/godot_replacements/custom_world_environment.rs
Original file line number Diff line number Diff line change
@@ -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<WorldEnvironment>,
}

#[godot_api]
impl IWorldEnvironment for WorldEnvironmentSettingsCompliant {
fn ready(&mut self) {
// TODO: omfg if <unwrap> 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 {}
1 change: 1 addition & 0 deletions src/godot_replacements/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod custom_world_environment;
37 changes: 2 additions & 35 deletions src/interaction.rs
Original file line number Diff line number Diff line change
@@ -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<StringName> = Lazy::new(|| StringName::from("on_select"));
static METHOD_DESELECT: Lazy<StringName> = Lazy::new(|| StringName::from("on_deselect"));
@@ -20,6 +18,7 @@ static SIGNAL_ON_DESELECTED: Lazy<StringName> = Lazy::new(|| StringName::from("o
struct InteractRaycast3D {
#[export]
filter_groups: PackedStringArray,
#[var]
target: Option<Gd<Node3D>>,
#[base]
base: Base<RayCast3D>,
@@ -30,6 +29,7 @@ struct InteractRaycast3D {
struct InteractArea3D {
#[export]
filter_groups: PackedStringArray,
#[var]
target: Option<Gd<Node3D>>,
#[base]
base: Base<Area3D>,
@@ -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> = 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);
// }
}
}
26 changes: 25 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
165 changes: 165 additions & 0 deletions src/serialization.rs
Original file line number Diff line number Diff line change
@@ -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<GString, Gd<SaveDataBuilder>>,
base: Base<Object>,
}

#[godot_api]
impl IObject for SaveDataBuilder {
fn init(base: Base<Self::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<SaveDataBuilder> {
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<SaveDataBuilder> {
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<Gd<SaveDataBuilder>> {
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<T: FromGodot + ToGodot>(
&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);
}
128 changes: 128 additions & 0 deletions src/utility_nodes/gui_interact.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
use godot::{
engine::{AudioStream, Control},
prelude::*,
};

type Sfx = Option<Gd<AudioStream>>;
#[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<Gd<AudioStreamPlayer>>,
#[base]
base: Base<Node>,
}

#[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<Gd<Control>, _> = 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<Gd<Control>, _> = 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<Gd<Control>, _> = 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<AudioStream>) {
if let Some(player) = &mut self.audio_player {
player.stop();
player.set_stream(stream);
player.play();
}
}
fn try_connect(&self, node: &mut Gd<Control>, 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
}
}
1 change: 1 addition & 0 deletions src/utility_nodes/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod gui_interact;

0 comments on commit ba441d1

Please sign in to comment.