diff --git a/README.md b/README.md index 42b0c51..40cab51 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,6 @@ The following features are currently available. More will be listed in the `In D Features that will certainly be expanded upon: -- Provide more submodules in the `serde_gd` module to support iterable `Gd` collections. - Make the `gdron` and `gdbin` formats interchangeable for release mode with a custom `EditorExportPlugin`. - Ensure everything works smoothly in compiled games, especially pointers to `.tres` resources after they are changed into `.res` format. @@ -111,7 +110,9 @@ Both file are recognizable by Godot editor, can be loaded through it and attache ## Bundled resources What if we have a Resource which contains another resource, which we would want to save as a bundled resource? There are two modules that handle this case: - `gd_props::serde_gd::gd_option` - for `Option>` fields, -- `gd_props::serde_gd::gd` - for `Gd` fields. +- `gd_props::serde_gd::gd` - for `Gd` fields, +- `gd_props::serde_gd::gd_resvec` - for vector collections of `Gd` using `GdResVec` (see `Supplementary types` section), +- `gd_props::serde_gd::gd_hashmap` - for `HashMap` fields. There are some requirements for this to work: - `T` needs to be User-defined `GodotClass` inheriting from `Resource`, @@ -161,8 +162,10 @@ Upon saving, we receive file as below: ## External Resources If you desire to preserve a sub-resource as an External Resource, akin to regular resource saving in Godot, `gd-props` provides two additional modules: -- `gd_props::serde_gd::ext_option` - designed for `Option>` fields. -- `gd_props::serde_gd::ext` - designed for `Gd` fields. +- `gd_props::serde_gd::ext_option` - designed for `Option>` fields, +- `gd_props::serde_gd::ext` - designed for `Gd` fields, +- `gd_props::serde_gd::ext_resvec` - for vector collections of `Gd` using `GdResVec` (see `Supplementary types` section), +- `gd_props::serde_gd::ext_hashmap` - for `HashMap` fields. To enable this functionality, a few requirements must be met: @@ -262,4 +265,19 @@ unsafe impl ExtensionLibrary for MyExtension { } } } -``` \ No newline at end of file +``` + +## Supplementary types + +For now, this crate introduces only one utility type: `GdResVec`, a vector-like collection of `Gd` pointers to classes +inheriting from `Resource`. + +If you need to hold a collection of subresources in one field, deciding whether to use Godot's `Array`, which allows you +to export the collection to the Godot editor, or `Vec`, which is easier to work with in Rust, can be challenging. + +To alleviate this hurdle and avoid the declaration of additional `serde_gd` modules, the `GdResVec` has been created, +combining the best of both worlds: + +- It holds the pointers in a `Vec`, dereferences to it in Rust, and can be used as a regular vector. +- It is transferred to the Godot side as a typed `Array`, making it easily accessible there by GDScript methods. +- It implements Property and Export, allowing it to be exported. \ No newline at end of file diff --git a/gd-props-defs/src/gd_meta.rs b/gd-props-defs/src/gd_meta.rs index a526d22..d0e89cc 100644 --- a/gd-props-defs/src/gd_meta.rs +++ b/gd-props-defs/src/gd_meta.rs @@ -2,7 +2,7 @@ use std::io::BufRead; use godot::builtin::GString; use godot::engine::file_access::ModeFlags; -use godot::engine::{FileAccess, GFile, Resource, ResourceLoader}; +use godot::engine::{FileAccess, GFile, Resource, ResourceLoader, ResourceUid}; use godot::obj::Gd; use serde::{Deserialize, Serialize}; @@ -121,10 +121,22 @@ impl GdMetaExt { None } fn try_load_from_uid(&self, resource_loader: &mut Gd) -> Option> { - resource_loader.load(GString::from(&self.uid)) + let resource_uid = ResourceUid::singleton(); + let id = resource_uid.text_to_id(GString::from(&self.uid)); + if resource_uid.has_id(id) { + resource_loader + .load_ex(GString::from(&self.uid)) + .type_hint(GString::from(&self.gd_class)) + .done() + } else { + None + } } fn try_load_from_path(&self, resource_loader: &mut Gd) -> Option> { - resource_loader.load(GString::from(&self.path)) + resource_loader + .load_ex(GString::from(&self.path)) + .type_hint(GString::from(&self.gd_class)) + .done() } } diff --git a/gd-props-defs/src/gdprop_io.rs b/gd-props-defs/src/gdprop_io.rs index a9ee050..d6e9f48 100644 --- a/gd-props-defs/src/gdprop_io.rs +++ b/gd-props-defs/src/gdprop_io.rs @@ -171,7 +171,7 @@ where GdPropFormat::GdRon => T::load_ron(path), GdPropFormat::GdBin => T::load_bin(path), GdPropFormat::None => { - godot_warn!("Unrecognized format for: {}", &path); + godot_warn!("unrecognized format for: {}", &path); godot::engine::global::Error::ERR_FILE_UNRECOGNIZED.to_variant() } } diff --git a/gd-props-defs/src/lib.rs b/gd-props-defs/src/lib.rs index da41b68..9ca2cbd 100644 --- a/gd-props-defs/src/lib.rs +++ b/gd-props-defs/src/lib.rs @@ -2,8 +2,15 @@ pub mod errors; pub(crate) mod gd_meta; pub(crate) mod gdprop; pub(crate) mod gdprop_io; + +/// Module containing serialization and deserialization methods for Godot objects and their collections. pub mod serde_gd; +/// Supplementary types for easier handling of pointers of [Resource](godot::enigne::Resource)-inheriting +/// [GodotClass](godot::obj::GodotClass)es. +pub mod types; + +/// Traits containing logic of `gd-props` custom resource formats. pub mod traits { pub use super::gdprop::GdProp; pub use super::gdprop_io::{GdPropLoader, GdPropSaver}; diff --git a/gd-props-defs/src/serde_gd.rs b/gd-props-defs/src/serde_gd.rs index ec7516e..918d6c5 100644 --- a/gd-props-defs/src/serde_gd.rs +++ b/gd-props-defs/src/serde_gd.rs @@ -1,9 +1,7 @@ -//! Module containing additional serialization and deserialization methods for Godot objects. +use godot::engine::Resource; +use godot::obj::dom::UserDomain; +use godot::obj::{Gd, GodotClass, Inherits}; -use godot::{ - obj::dom::UserDomain, - prelude::{Gd, GodotClass, Inherits, Resource}, -}; use serde::{Serialize, Serializer}; pub(crate) struct GodotPointerSerWrapper< @@ -21,30 +19,11 @@ where self.0.bind().serialize(serializer) } } - -// pub(crate) struct GodotPointerDeWrapper + Inherits + Deserialize<'de>>( -// Gd, -// ); - -// impl<'de, T> Deserialize<'de> for GodotPointerDeWrapper -// where -// T: GodotClass + Inherits + Deserialize<'de>, -// { -// fn deserialize(deserializer: D) -> Result -// where -// D: Deserializer<'de>, -// { -// let obj: T = Deserialize::deserialize(deserializer)?; -// Ok(GodotPointerDeWrapper(Gd::new(obj))) -// } -// } - -/// Module that can be used to serialize and deserialize objects castable -/// to [Resouce](godot::engine::Resource) on basis of their [Gd]. -/// -/// Its main use is to derive [serde::Serialize] and [serde::Deserialize] on -/// resources containing pointers to other resources, while -/// keeping data from every one. +/// Module that can be used to serialize and deserialize objects castable to [Resouce](godot::engine::Resource) on basis +/// of their [Gd]. +/// +/// Its main use is to derive [serde::Serialize] and [serde::Deserialize] on resources containing pointers to other +/// resources, while keeping data from every one. /// /// ## Example /// @@ -75,10 +54,10 @@ where /// } /// ``` pub mod gd { - use godot::{ - obj::dom::UserDomain, - prelude::{Gd, GodotClass, Inherits, Resource}, - }; + use godot::engine::Resource; + use godot::obj::dom::UserDomain; + use godot::obj::{Gd, GodotClass, Inherits}; + use serde::{de, ser, Deserialize, Serialize}; pub fn deserialize<'de, D, T>(deserializer: D) -> Result, D::Error> @@ -86,8 +65,8 @@ pub mod gd { D: de::Deserializer<'de>, T: GodotClass + Inherits + Deserialize<'de>, { - let obj: T = de::Deserialize::deserialize(deserializer)?; - Ok(Gd::from_object(obj)) + let res = T::deserialize(deserializer)?; + Ok(Gd::from_object(res)) } pub fn serialize(pointer: &Gd, serializer: S) -> Result @@ -99,12 +78,11 @@ pub mod gd { } } -/// Module that can be used to serialize and deserialize objects castable -/// to [Resouce](godot::engine::Resource) on basis of their [Option]<[Gd]>. +/// Module that can be used to serialize and deserialize objects castable to [Resouce](godot::engine::Resource) on basis +/// of their [Option]<[Gd]>. /// -/// Its main use is to derive [serde::Serialize] and [serde::Deserialize] on -/// resources containing optional pointers to other resources, while -/// keeping data from every one (if attached). +/// Its main use is to derive [serde::Serialize] and [serde::Deserialize] on resources containing optional pointers +/// to other resources, while keeping data from every one (if attached). /// /// ## Example /// @@ -133,10 +111,10 @@ pub mod gd { /// ``` pub mod gd_option { - use godot::{ - obj::dom::UserDomain, - prelude::{godot_error, Gd, GodotClass, Inherits, Resource}, - }; + use godot::engine::Resource; + use godot::obj::dom::UserDomain; + use godot::obj::{Gd, GodotClass, Inherits}; + use serde::{de, ser, Deserialize, Serialize}; use super::GodotPointerSerWrapper; @@ -146,13 +124,9 @@ pub mod gd_option { D: de::Deserializer<'de>, T: GodotClass + Inherits + Deserialize<'de>, { - match Option::::deserialize(deserializer) { - Ok(Some(obj)) => Ok(Some(Gd::from_object(obj))), - Ok(None) => Ok(None), - Err(e) => { - godot_error!("{:?}", e); - Err(e) - } + match Option::::deserialize(deserializer)? { + Some(obj) => Ok(Some(Gd::from_object(obj))), + None => Ok(None), } } @@ -173,29 +147,137 @@ pub mod gd_option { } } -pub mod gd_vec { - use godot::{ - obj::dom::UserDomain, - prelude::{Gd, GodotClass, Inherits, Resource}, - }; +/// Module that can be used to serialize and deserialize objects castable to [`Resource`](godot::engine::Resource) on basis +/// of their pointers contained within [`HashMap`](std::collections::HashMap) as bundled resources. +/// +/// Its main use is to derive [serde::Serialize] and [serde::Deserialize] on resources containing optional pointers +/// to other resources, while keeping data from every one (if attached). +/// +/// ## Example +/// +/// ```no_run +/// use godot::prelude::*; +/// use godot::engine::{Resource, IResource}; +/// use godot::builtin::Array; +/// use serde::{Serialize, Deserialize}; +/// use std::collections::HashMap; +/// +/// #[derive(GodotClass, Serialize, Deserialize)] +/// #[class(base=Resource, init)] +/// struct InnerResource {} +/// +/// #[godot_api] +/// impl InnerResource {} +/// +/// #[derive(GodotClass, Serialize, Deserialize)] +/// #[class(init, base=Resource)] +/// struct OuterResource { +/// #[serde(with="gd_props::serde_gd::gd_hashmap")] +/// inner: HashMap> +/// } +/// +/// #[godot_api] +/// impl OuterResource {} +/// ``` +pub mod gd_hashmap { + use std::collections::HashMap; + use std::hash::Hash; + + use godot::engine::Resource; + use godot::obj::dom::UserDomain; + use godot::obj::{Gd, GodotClass, Inherits}; + + use serde::{Deserialize, Serialize}; + + use super::GodotPointerSerWrapper; + + pub fn serialize(map: &HashMap>, serializer: S) -> Result + where + S: serde::ser::Serializer, + T: GodotClass + Inherits + Serialize, + K: Hash + Eq + PartialEq + Serialize + Clone, + { + // Serialize each Gd using the GodotPointerWrapper + let mut wrapper_map: HashMap> = HashMap::new(); + for (k, gd) in map { + wrapper_map.insert(k.clone(), GodotPointerSerWrapper(gd.clone())); + } + wrapper_map.serialize(serializer) + } + + pub fn deserialize<'de, D, T, K>(deserializer: D) -> Result>, D::Error> + where + D: serde::de::Deserializer<'de>, + T: GodotClass + Inherits + Deserialize<'de>, + K: Hash + Eq + PartialEq + Deserialize<'de> + Clone, + { + // Deserialize a vector of GodotPointerWrapper and then extract the inner Gd values + let mut wrapper_map: HashMap = HashMap::deserialize(deserializer)?; + let mut gd_map = HashMap::new(); + for (k, obj) in wrapper_map.drain() { + gd_map.insert(k, Gd::::from_object(obj)); + } + Ok(gd_map) + } +} + +/// Module that can be used to serialize and deserialize objects castable to [Resouce](godot::engine::Resource) on basis +/// of their pointers contained within [GdResVec](crate::types::GdResVec) collection. +/// +/// Its main use is to derive [serde::Serialize] and [serde::Deserialize] on resources containing optional pointers +/// to other resources, while keeping data from every one. +/// +/// ## Example +/// +/// ```no_run +/// use godot::prelude::*; +/// use godot::engine::{Resource, IResource}; +/// use godot::builtin::Array; +/// use serde::{Serialize, Deserialize}; +/// use gd_props::types::GdResVec; +/// +/// #[derive(GodotClass, Serialize, Deserialize)] +/// #[class(base=Resource, init)] +/// struct InnerResource {} +/// +/// #[godot_api] +/// impl InnerResource {} +/// +/// #[derive(GodotClass, Serialize, Deserialize)] +/// #[class(init, base=Resource)] +/// struct OuterResource { +/// #[export] +/// #[serde(with="gd_props::serde_gd::gd_resvec")] +/// inner: GdResVec +/// } +/// +/// #[godot_api] +/// impl OuterResource {} +/// ``` +pub mod gd_resvec { + use godot::engine::Resource; + use godot::obj::dom::UserDomain; + use godot::obj::{Gd, GodotClass, Inherits}; + use serde::{Deserialize, Serialize}; use super::GodotPointerSerWrapper; + use crate::types::GdResVec; - pub fn serialize(vec: &[Gd], serializer: S) -> Result + pub fn serialize(resvec: &GdResVec, serializer: S) -> Result where S: serde::ser::Serializer, T: GodotClass + Inherits + Serialize, { // Serialize each Gd using the GodotPointerWrapper - let wrapper_vec: Vec<_> = vec + let wrapper_vec: Vec<_> = resvec .iter() .map(|gd| GodotPointerSerWrapper(gd.clone())) .collect(); wrapper_vec.serialize(serializer) } - pub fn deserialize<'de, D, T>(deserializer: D) -> Result>, D::Error> + pub fn deserialize<'de, D, T>(deserializer: D) -> Result, D::Error> where D: serde::de::Deserializer<'de>, T: GodotClass + Inherits + Deserialize<'de>, @@ -206,22 +288,19 @@ pub mod gd_vec { .into_iter() .map(|obj| Gd::::from_object(obj)) .collect(); - Ok(gd_vec) + Ok(GdResVec::from_vec(gd_vec)) } } -/// Module that can be used to serialize and deserialize External Resources -/// kept within your custom [Resource]. +/// Module that can be used to serialize and deserialize External Resources kept within your custom [Resource]. /// -/// External Resource which [Gd] is contained within the annotated field don't need -/// to implement [serde::Serialize] and [serde::Deserialize] - no regular -/// serialization/deserialization is made there. Instead, the resource class, UID and path -/// is saved upon serialization as and upon deserialization, the -/// resource is loaded using [ResourceLoader](godot::engine::ResourceLoader) singleton -/// on its basis. +/// External Resource which [Gd] is contained within the annotated field don't need to implement [serde::Serialize] and +/// [serde::Deserialize] - no regular serialization/deserialization is made there. Instead, the resource class, UID and path +/// is saved upon serialization as and upon deserialization, the resource is loaded using +/// [ResourceLoader](godot::engine::ResourceLoader) singleton on its basis. /// -/// The External Resource can be both godot built-in resource and other rust-defined -/// custom [Resource]. +/// The External Resource can be both godot built-in resource and other rust-defined custom [Resource]. Only runtime +/// requirement is that the [ResourceFormatLoader](godot::engine::ResourceFormatLoader) is registered in global `ResourceLoader`. /// /// ## Example /// @@ -249,7 +328,9 @@ pub mod gd_vec { /// ``` pub mod ext { use crate::gd_meta::{GdExtResource, GdMetaExt}; - use godot::prelude::{Gd, GodotClass, Inherits, Resource}; + use godot::engine::Resource; + use godot::obj::{Gd, GodotClass, Inherits}; + use serde::{de, ser, Deserialize, Serialize}; pub fn deserialize<'de, D, T>(deserializer: D) -> Result, D::Error> @@ -289,18 +370,15 @@ pub mod ext { } } -/// Module that can be used to serialize and deserialize optional External Resources -/// kept within your custom [Resource]. +/// Module that can be used to serialize and deserialize optional External Resources kept within your custom [Resource]. /// -/// External Resource which [Option]<[Gd]> is contained within the annotated field don't need -/// to implement [serde::Serialize] and [serde::Deserialize] - no regular -/// serialization/deserialization is made there. Instead, the resource class, UID and path -/// is saved upon serialization and upon deserialization, the -/// resource is loaded using [ResourceLoader](godot::engine::ResourceLoader) singleton +/// External Resource which [Option]<[Gd]> is contained within the annotated field don't need to implement [serde::Serialize] +/// and [serde::Deserialize] - no regular serialization/deserialization is made there. Instead, the resource class, UID and path +/// is saved upon serialization and upon deserialization, the resource is loaded using [ResourceLoader](godot::engine::ResourceLoader) singleton /// on basis of this data. /// -/// The External Resource can be both godot built-in resource and other rust-defined -/// custom [Resource]. +/// The External Resource can be both godot built-in resource and other rust-defined custom [Resource]. Only runtime +/// requirement is that the [ResourceFormatLoader](godot::engine::ResourceFormatLoader) is registered in global `ResourceLoader`. /// /// ## Example /// @@ -329,7 +407,8 @@ pub mod ext { /// ``` pub mod ext_option { - use godot::prelude::{Gd, GodotClass, Inherits, Resource}; + use godot::engine::Resource; + use godot::obj::{Gd, GodotClass, Inherits}; use serde::{de, ser, Deserialize, Serialize}; use crate::gd_meta::GdExtResource; @@ -363,21 +442,161 @@ pub mod ext_option { } } -#[doc(hidden)] -pub mod ext_vec { +/// Module that can be used to serialize and deserialize optional External Resources kept within your custom [Resource] +/// in a [HashMap](std::collections::HashMap). +/// +/// External Resource which pointers are contained within the annotated `HashMap` field don't need to implement [serde::Serialize] +/// and [serde::Deserialize] - no regular serialization/deserialization is made there. Instead, the resource class, UID and path +/// is saved upon serialization and upon deserialization, the resource is loaded using [`ResourceLoader`](godot::engine::ResourceLoader) singleton +/// on basis of this data. +/// +/// The External Resource can be both godot built-in resource and other rust-defined custom [Resource]. Only runtime +/// requirement is that the [ResourceFormatLoader](godot::engine::ResourceFormatLoader) is registered in global `ResourceLoader`. +/// +/// ## Example +/// +/// ```no_run +/// use godot::prelude::*; +/// use godot::engine::{Resource, IResource}; +/// use godot::builtin::Array; +/// use serde::{Serialize, Deserialize}; +/// use std::collections::HashMap; +/// +/// #[derive(GodotClass, Serialize, Deserialize)] +/// #[class(base=Resource, init)] +/// struct InnerResource {} +/// +/// #[godot_api] +/// impl InnerResource {} +/// +/// #[derive(GodotClass, Serialize, Deserialize)] +/// #[class(init, base=Resource)] +/// struct OuterResource { +/// #[serde(with="gd_props::serde_gd::ext_hashmap")] +/// inner: HashMap> +/// } +/// +/// #[godot_api] +/// impl OuterResource {} +/// ``` +pub mod ext_hashmap { + + use std::collections::HashMap; + use std::hash::Hash; + + use crate::gd_meta::{GdExtResource, GdMetaExt}; + use godot::engine::Resource; + use godot::obj::{Gd, GodotClass, Inherits}; + use serde::{de, ser, Deserialize, Serialize}; + + pub fn deserialize<'de, D, T, K>(deserializer: D) -> Result>, D::Error> + where + D: de::Deserializer<'de>, + T: GodotClass + Inherits, + K: Hash + Eq + PartialEq + Deserialize<'de> + Clone, + { + let map: HashMap = Deserialize::deserialize(deserializer)?; + + let mut result = HashMap::new(); + + for (k, element) in map { + if let GdExtResource::ExtResource(meta) = element { + let obj = meta + .try_load() + .ok_or_else(|| de::Error::custom("cannot load resource"))?; + result.insert(k, obj.cast::()); + } else { + return Err(de::Error::custom("no meta found")); + } + } + + Ok(result) + } + + pub fn serialize(map: &HashMap>, serializer: S) -> Result + where + S: ser::Serializer, + T: GodotClass + Inherits, + K: Hash + Eq + PartialEq + Serialize + Clone, + { + let mut loader = godot::engine::ResourceLoader::singleton(); + let res_uid = godot::engine::ResourceUid::singleton(); + + let external: HashMap = + HashMap::from_iter(map.iter().map(|(k, element)| { + let upcasted = element.clone().upcast::(); + let path = upcasted.get_path(); + let gd_class = upcasted.get_class().to_string(); + let uuid = loader.get_resource_uid(path.clone()); + + ( + k.clone(), + GdExtResource::ExtResource(GdMetaExt { + gd_class, + uid: res_uid.id_to_text(uuid).to_string(), + path: path.to_string(), + }), + ) + })); + + external.serialize(serializer) + } +} + +/// Module that can be used to serialize and deserialize optional External Resources kept within your custom [Resource] +/// in a [GdResVec](crate::types::GdResVec). +/// +/// External Resource which pointers are contained within the annotated `GdResVec` field don't need to implement [serde::Serialize] +/// and [serde::Deserialize] - no regular serialization/deserialization is made there. Instead, the resource class, UID and path +/// is saved upon serialization and upon deserialization, the resource is loaded using [`ResourceLoader`](godot::engine::ResourceLoader) singleton +/// on basis of this data. +/// +/// The External Resource can be both godot built-in resource and other rust-defined custom [Resource]. Only runtime +/// requirement is that the [ResourceFormatLoader](godot::engine::ResourceFormatLoader) is registered in global `ResourceLoader`. +/// +/// ## Example +/// +/// ```no_run +/// use godot::prelude::*; +/// use godot::engine::{Resource, IResource}; +/// use godot::builtin::Array; +/// use serde::{Serialize, Deserialize}; +/// use std::collections::HashMap; +/// use gd_props::types::GdResVec; +/// +/// #[derive(GodotClass, Serialize, Deserialize)] +/// #[class(base=Resource, init)] +/// struct InnerResource {} +/// +/// #[godot_api] +/// impl InnerResource {} +/// +/// #[derive(GodotClass, Serialize, Deserialize)] +/// #[class(init, base=Resource)] +/// struct OuterResource { +/// #[serde(with="gd_props::serde_gd::ext_resvec")] +/// inner: GdResVec +/// } +/// +/// #[godot_api] +/// impl OuterResource {} +/// ``` +pub mod ext_resvec { use crate::gd_meta::{GdExtResource, GdMetaExt}; - use godot::prelude::{Gd, GodotClass, Inherits, Resource}; + use crate::types::GdResVec; + use godot::engine::Resource; + use godot::obj::{GodotClass, Inherits}; use serde::{de, ser, Deserialize, Serialize}; - pub fn deserialize<'de, D, T>(deserializer: D) -> Result>, D::Error> + pub fn deserialize<'de, D, T>(deserializer: D) -> Result, D::Error> where D: de::Deserializer<'de>, T: GodotClass + Inherits, { let vec: Vec = Deserialize::deserialize(deserializer)?; - let mut result = Vec::new(); + let mut result = GdResVec::default(); for element in vec { if let GdExtResource::ExtResource(meta) = element { @@ -393,16 +612,17 @@ pub mod ext_vec { Ok(result) } - pub fn serialize(vec: &[Gd], serializer: S) -> Result + pub fn serialize(vec: &GdResVec, serializer: S) -> Result where S: ser::Serializer, T: GodotClass + Inherits, { + let mut loader = godot::engine::ResourceLoader::singleton(); + let res_uid = godot::engine::ResourceUid::singleton(); + let serialized: Vec = vec .iter() .map(|element| { - let mut loader = godot::engine::ResourceLoader::singleton(); - let res_uid = godot::engine::ResourceUid::singleton(); let upcasted = element.clone().upcast::(); let path = upcasted.get_path(); let gd_class = upcasted.get_class().to_string(); diff --git a/gd-props-defs/src/types.rs b/gd-props-defs/src/types.rs new file mode 100644 index 0000000..9a8a2df --- /dev/null +++ b/gd-props-defs/src/types.rs @@ -0,0 +1,233 @@ +use std::ops::{Deref, DerefMut}; + +use godot::bind::property::{Export, Property, PropertyHintInfo, TypeStringHint}; +use godot::builtin::meta::{FromGodot, GodotConvert, ToGodot}; +use godot::builtin::{Array, Variant, VariantArray}; +use godot::engine::Resource; +use godot::obj::{Gd, Inherits}; + +/// Collection of pointers to instances of [Resource]-inheriting structs. +/// +/// Basic collection which acts like [Vec] of `Gd` on the Rust side, and [Array] on the Godot side. Its Godot +/// representation makes it easily exportable on [GodotClass](godot::obj::GodotClass) structs, while its Rust representation +/// makes it easier to work in Rust code. +/// +/// ## Serde +/// There are dedicated modules implementing serde for fields of these types: +/// - as _bundled resources_: [gd_props::serde_gd::gd_resvec](crate::serde_gd::gd_resvec), +/// - as _external resources_: [gd_props::serde_gd::ext_resvec](crate::serde_gd::ext_resvec) +/// +/// ## Examples +/// +/// An instance of `GdResVec` can be created in a few ways: +/// +/// ```no_run +/// use godot::prelude::*; +/// use gd_props::types::GdResVec; +/// +/// // Create empty vector, then push pointers into it +/// let mut resvec = GdResVec::default(); +/// resvec.push(Gd::::default()); +/// resvec.push(Gd::::default()); +/// assert_eq!(resvec.len(), 2); +/// +/// // Create from existing vector +/// let vector = vec![Gd::::default(), Gd::::default()]; +/// let from_vec = GdResVec::from_vec(vector); +/// assert_eq!(from_vec.len(), 2); +/// +/// // Create from typed Godot array +/// let mut typed_arr: Array> = Array::new(); +/// typed_arr.push(Gd::::default()); +/// typed_arr.push(Gd::::default()); +/// let from_typed_arr = GdResVec::from_array(typed_arr); +/// assert_eq!(from_typed_arr.len(), 2); +/// +/// // Create from variant array +/// let mut var_arr = VariantArray::new(); +/// var_arr.push(Gd::::default().to_variant()); +/// var_arr.push(Gd::::default().to_variant()); +/// let from_var_arr = GdResVec::::from_variant_array(var_arr); +/// assert_eq!(from_var_arr.len(), 2); +/// ``` +/// +/// Declaration of serializable [GodotClass](godot::obj::GodotClass) with `GdResVec` field. +/// +/// ```no_run +/// use godot::prelude::*; +/// use gd_props::types::GdResVec; +/// use gd_props::GdProp; +/// use serde::{Serialize, Deserialize}; +/// +/// # mod resource { +/// # use gd_props::GdProp; +/// # use godot::prelude::GodotClass; +/// # use serde::{Serialize, Deserialize}; +/// # #[derive(GodotClass, GdProp, Serialize, Deserialize)] +/// # #[class(init, base=Resource)] +/// # pub struct MyResource; +/// # } +/// # use resource::*; +/// +/// #[derive(GodotClass, Serialize, Deserialize, GdProp)] +/// #[class(init, base=Resource)] +/// struct BundledResVecResource { +/// #[export] +/// #[serde(with="gd_props::serde_gd::gd_resvec")] +/// resvec: GdResVec +/// } +/// +/// #[derive(GodotClass, Serialize, Deserialize, GdProp)] +/// #[class(init, base=Resource)] +/// struct ExternalResVecResource { +/// #[export] +/// #[serde(with="gd_props::serde_gd::ext_resvec")] +/// resvec: GdResVec +/// } +/// ``` +pub struct GdResVec +where + T: Inherits, +{ + vec: Vec>, + empty_last: bool, +} + +impl Default for GdResVec +where + T: Inherits, +{ + fn default() -> Self { + Self { + vec: Vec::new(), + empty_last: false, + } + } +} + +impl GdResVec +where + T: Inherits, +{ + pub fn from_vec(vec: Vec>) -> Self { + Self { + vec, + ..Default::default() + } + } + + pub fn from_array(arr: Array>) -> Self { + let mut vec = Vec::new(); + for gd in arr.iter_shared() { + vec.push(gd); + } + Self { + vec, + ..Default::default() + } + } + + pub fn as_array(&self) -> Array> { + let mut array: Array> = Array::new(); + for gd in self.vec.iter() { + array.push(gd.clone()) + } + array + } + + pub fn from_variant_array(arr: VariantArray) -> Self { + let mut vec = Vec::new(); + let mut empty_last = false; + for variant in arr.iter_shared() { + if let Ok(gd) = Gd::::try_from_variant(&variant) { + vec.push(gd); + } else { + empty_last = true; + } + } + Self { vec, empty_last } + } + + pub fn as_variant_array(&self) -> VariantArray { + let mut array = godot::builtin::VariantArray::new(); + for gd in self.vec.iter() { + array.push(gd.clone().to_variant()); + } + if self.empty_last { + array.push(Variant::nil()); + } + array + } +} + +impl Deref for GdResVec +where + T: Inherits, +{ + type Target = Vec>; + + fn deref(&self) -> &Self::Target { + &self.vec + } +} + +impl DerefMut for GdResVec +where + T: Inherits, +{ + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.vec + } +} + +impl GodotConvert for GdResVec +where + T: Inherits, +{ + type Via = godot::builtin::Array>; +} + +impl ToGodot for GdResVec +where + T: Inherits, +{ + fn to_godot(&self) -> Self::Via { + self.as_array() + } +} + +impl FromGodot for GdResVec +where + T: Inherits, +{ + fn try_from_godot(via: Self::Via) -> Result { + Ok(Self::from_array(via)) + } +} + +impl Property for GdResVec +where + T: Inherits, +{ + type Intermediate = VariantArray; + + fn get_property(&self) -> Self::Intermediate { + self.as_variant_array() + } + + fn set_property(&mut self, value: Self::Intermediate) { + *self = Self::from_variant_array(value); + } +} + +impl Export for GdResVec +where + T: Inherits, +{ + fn default_export_info() -> godot::bind::property::PropertyHintInfo { + PropertyHintInfo { + hint: godot::engine::global::PropertyHint::PROPERTY_HINT_TYPE_STRING, + hint_string: Gd::::type_string().into(), + } + } +} diff --git a/gd-props-macros/src/gdprop.rs b/gd-props-macros/src/gdprop.rs index 7ebb9cc..e952c61 100644 --- a/gd-props-macros/src/gdprop.rs +++ b/gd-props-macros/src/gdprop.rs @@ -100,7 +100,7 @@ pub fn derive_loader(decl: Declaration) -> Result { use ::gd_props::traits::GdProp; match self._int_get_type(path.clone()) { - Err(error) => ::godot::prelude::godot_error!("Error getting '{}' resource type during load: {}", path, error), + Err(error) => ::godot::prelude::godot_error!("error getting '{}' resource type during load: {}", path, error), Ok(struct_name) => { #( if struct_name.eq(#registers::HEAD_IDENT) { @@ -115,10 +115,7 @@ pub fn derive_loader(decl: Declaration) -> Result { fn get_resource_uid(&self, path: ::godot::builtin::GString) -> i64 { match self._int_get_uid(path.clone()) { Ok(uid) => uid, - Err(error) => { - ::godot::prelude::godot_error!("Error getting uid from resource: '{}', '{}", path, error); - -1 - } + Err(error) => -1 } } } diff --git a/gd-props/src/lib.rs b/gd-props/src/lib.rs index 890697f..975e8fb 100644 --- a/gd-props/src/lib.rs +++ b/gd-props/src/lib.rs @@ -33,3 +33,4 @@ pub mod traits { pub use gd_props_defs::errors; pub use gd_props_defs::serde_gd; +pub use gd_props_defs::types; diff --git a/tests/godot/ext_test/test_ext.gdron b/tests/godot/ext_test/test_ext.gdron new file mode 100644 index 0000000..6d0c8c4 --- /dev/null +++ b/tests/godot/ext_test/test_ext.gdron @@ -0,0 +1,13 @@ +(gd_class:"WithExtGd",uid:"uid://bgx7ek0b2rb26") +( + second: ExtResource(( + gd_class: "TestGodotResource", + uid: "uid://1s8om1og5j3x", + path: "res://ext_test/test_godot_res.tres", + )), + first: ExtResource(( + gd_class: "TestResource", + uid: "uid://ct0lwwygpjcdp", + path: "res://ext_test/test_resource.gdron", + )), +) \ No newline at end of file diff --git a/tests/godot/ext_test/test_godot_res.tres b/tests/godot/ext_test/test_godot_res.tres new file mode 100644 index 0000000..04e4f1e --- /dev/null +++ b/tests/godot/ext_test/test_godot_res.tres @@ -0,0 +1,5 @@ +[gd_resource type="TestGodotResource" format=3 uid="uid://1s8om1og5j3x"] + +[resource] +int = -127 +str = "YCHGDVJRMF" diff --git a/tests/godot/ext_test/test_resource.gdbin b/tests/godot/ext_test/test_resource.gdbin new file mode 100644 index 0000000..cc6f128 Binary files /dev/null and b/tests/godot/ext_test/test_resource.gdbin differ diff --git a/tests/godot/ext_test/test_resource.gdron b/tests/godot/ext_test/test_resource.gdron new file mode 100644 index 0000000..8b4d33d --- /dev/null +++ b/tests/godot/ext_test/test_resource.gdron @@ -0,0 +1,31 @@ +(gd_class:"TestResource",uid:"uid://ct0lwwygpjcdp") +( + set: [ + ( + int: 149, + character: 'X', + ), + ( + int: 66, + character: 'Z', + ), + ], + vec: [ + ( + int: -80, + character: 'U', + ), + ( + int: 203, + character: 'S', + ), + ( + int: 4, + character: 'Z', + ), + ( + int: 68, + character: 'K', + ), + ], +) \ No newline at end of file diff --git a/tests/godot/project.godot b/tests/godot/project.godot index a9b1d05..359f380 100644 --- a/tests/godot/project.godot +++ b/tests/godot/project.godot @@ -10,7 +10,7 @@ config_version=5 [application] -config/name="Godot-IO tests" +config/name="gd-props tests" run/main_scene="res://tests.tscn" config/features=PackedStringArray("4.2", "GL Compatibility") config/icon="res://icon.svg" diff --git a/tests/rust/src/itest/gdron.rs b/tests/rust/src/itest/gdron.rs index 266616b..5e34222 100644 --- a/tests/rust/src/itest/gdron.rs +++ b/tests/rust/src/itest/gdron.rs @@ -1,12 +1,15 @@ use gd_rehearse::itest::gditest; use godot::builtin::meta::FromGodot; use godot::builtin::GString; -use godot::engine::{DirAccess, IResourceFormatLoader, IResourceFormatSaver}; +use godot::engine::global::Error; +use godot::engine::{ + try_load, DirAccess, IResourceFormatLoader, IResourceFormatSaver, ResourceLoader, ResourceSaver, +}; use godot::obj::{Gd, UserClass}; use crate::remove_file; use crate::structs::prop_handlers::{PropLoader, PropSaver}; -use crate::structs::resource::TestResource; +use crate::structs::resource::{TestGodotResource, TestResource, WithBundledGd, WithExtGd}; #[gditest] fn can_save() { @@ -104,3 +107,107 @@ fn uid_is_stable() { remove_file(path, file); } + +#[gditest] +fn can_save_bundled() { + let path = "res://"; + let file = "test.gdron"; + let file_path = &format!("{}{}", path, file); + + let with_bundled = WithBundledGd::new_gd(); + + assert_eq!( + ResourceSaver::singleton() + .save_ex(with_bundled.upcast()) + .path(file_path.into()) + .done(), + Error::OK + ); + remove_file(path, file); +} + +#[gditest] +fn can_load_bundled() { + let path = "res://"; + let file = "test.gdron"; + let file_path = &format!("{}{}", path, file); + + let with_bundled = WithBundledGd::new_gd(); + + assert_eq!( + ResourceSaver::singleton() + .save_ex(with_bundled.clone().upcast()) + .path(file_path.into()) + .done(), + Error::OK + ); + + let load_res = try_load::(file_path); + assert!(load_res.is_some()); + let res = load_res.unwrap(); + assert!(TestResource::check_set_eq( + res.bind().first.bind().get_set(), + with_bundled.bind().first.bind().get_set() + )); + assert!(TestResource::check_vec_eq( + res.bind().first.bind().get_vec(), + with_bundled.bind().first.bind().get_vec() + )); + + if let (Some(from_bundled), Some(from_loaded)) = + (&with_bundled.bind().second, &res.bind().second) + { + assert!(TestResource::check_set_eq( + from_bundled.bind().get_set(), + from_loaded.bind().get_set() + )); + assert!(TestResource::check_vec_eq( + from_bundled.bind().get_vec(), + from_loaded.bind().get_vec() + )); + } else { + panic!("There need to be resources here!"); + }; +} + +#[gditest] +fn can_save_external() { + let path = "res://"; + let file = "test.gdron"; + let file_path = &format!("{}{}", path, file); + + let mut loader = ResourceLoader::singleton(); + let godot_res = loader + .load("res://ext_test/test_godot_res.tres".into()) + .unwrap() + .cast::(); + let res = loader + .load("res://ext_test/test_resource.gdron".into()) + .unwrap() + .cast::(); + let with_ext = Gd::::from_object(WithExtGd { + second: Some(godot_res), + first: res, + }); + + assert_eq!( + ResourceSaver::singleton() + .save_ex(with_ext.upcast()) + .path(file_path.into()) + .done(), + Error::OK + ); +} + +#[gditest] +fn can_load_external() { + let path = "res://ext_test/test_ext.gdron"; + + let mut loader = ResourceLoader::singleton(); + let res = loader.load(path.into()); + + assert!(res.is_some()); + + let casted = res.unwrap().try_cast::(); + assert!(casted.is_ok()); +} diff --git a/tests/rust/src/itest/mod.rs b/tests/rust/src/itest/mod.rs index 9c69cb2..965a3dd 100644 --- a/tests/rust/src/itest/mod.rs +++ b/tests/rust/src/itest/mod.rs @@ -1,3 +1,5 @@ mod gdbin; mod gdron; mod saver_loader; +mod serde_gd; +mod types; diff --git a/tests/rust/src/itest/saver_loader.rs b/tests/rust/src/itest/saver_loader.rs index 1d4034c..38b4436 100644 --- a/tests/rust/src/itest/saver_loader.rs +++ b/tests/rust/src/itest/saver_loader.rs @@ -1,15 +1,11 @@ use gd_props::traits::{GdPropLoader, GdPropSaver}; use gd_rehearse::itest::gditest; -use godot::{ - builtin::GString, - engine::{load, try_load, DirAccess, ResourceLoader, ResourceSaver}, - obj::UserClass, -}; - -use crate::structs::{ - prop_handlers::{PropLoader, PropSaver}, - resource::TestResource, -}; +use godot::builtin::GString; +use godot::engine::{load, try_load, DirAccess, ResourceLoader, ResourceSaver}; +use godot::obj::UserClass; + +use crate::structs::prop_handlers::{PropLoader, PropSaver}; +use crate::structs::resource::TestResource; const RES_PATH: &str = "res://"; const RES_NAME: &str = "test_main_saver_loader.gdbin"; diff --git a/tests/rust/src/itest/serde_gd.rs b/tests/rust/src/itest/serde_gd.rs new file mode 100644 index 0000000..8af183f --- /dev/null +++ b/tests/rust/src/itest/serde_gd.rs @@ -0,0 +1,217 @@ +use std::collections::HashMap; +use std::io::BufWriter; + +use gd_props::types::GdResVec; +use gd_rehearse::itest::gditest; +use godot::builtin::GString; +use godot::engine::ResourceLoader; +use godot::obj::Gd; + +use ron::Serializer; +use serde::Serialize; + +use crate::remove_file; +use crate::structs::resource::*; + +#[gditest] +fn serde_bundled() { + let resource = WithBundledGd::new(); + + let mut buffer = Vec::new(); + let mut serializer = rmp_serde::Serializer::new(BufWriter::new(&mut buffer)); + + let result = resource.serialize(&mut serializer); + assert!(result.is_ok()); + drop(serializer); + + let result = rmp_serde::from_slice::(&buffer); + assert!(result.is_ok()); + let deserialized = result.unwrap(); + + assert!(TestResource::check_set_eq( + resource.first.bind().get_set(), + deserialized.first.bind().get_set() + )); + assert!(TestResource::check_vec_eq( + resource.first.bind().get_vec(), + deserialized.first.bind().get_vec() + )); + + assert!(TestResource::check_set_eq( + resource.second.clone().unwrap().bind().get_set(), + deserialized.second.clone().unwrap().bind().get_set() + )); + assert!(TestResource::check_vec_eq( + resource.second.unwrap().bind().get_vec(), + deserialized.second.unwrap().bind().get_vec() + )); +} + +#[gditest] +fn serde_bundled_recvec() { + let resource = WithBundleHashMap::new(5); + let mut buffer = Vec::new(); + let mut serializer = rmp_serde::Serializer::new(BufWriter::new(&mut buffer)); + + let result = resource.serialize(&mut serializer); + assert!(result.is_ok()); + drop(serializer); + + let result = rmp_serde::from_slice::(&buffer); + assert!(result.is_ok()); + let deserialized = result.unwrap(); + + let keys = resource.map.keys(); + for key in keys { + assert!(TestResource::check_set_eq( + resource.map.get(key).unwrap().bind().get_set(), + deserialized.map.get(key).unwrap().bind().get_set() + )); + assert!(TestResource::check_vec_eq( + resource.map.get(key).unwrap().bind().get_vec(), + deserialized.map.get(key).unwrap().bind().get_vec() + )); + } +} + +#[gditest] +fn serde_bundled_hashmap() { + let resource = WithBundleHashMap::new(5); + let mut buffer = Vec::new(); + let mut serializer = rmp_serde::Serializer::new(BufWriter::new(&mut buffer)); + + let result = resource.serialize(&mut serializer); + assert!(result.is_ok()); + drop(serializer); + + let result = rmp_serde::from_slice::(&buffer); + assert!(result.is_ok()); + let deserialized = result.unwrap(); + + let keys = resource.map.keys(); + for key in keys { + assert!(TestResource::check_set_eq( + resource.map.get(key).unwrap().bind().get_set(), + deserialized.map.get(key).unwrap().bind().get_set() + )); + assert!(TestResource::check_vec_eq( + resource.map.get(key).unwrap().bind().get_vec(), + deserialized.map.get(key).unwrap().bind().get_vec() + )); + } +} + +#[gditest] +fn serde_external() { + let mut loader = ResourceLoader::singleton(); + + let test_res = loader + .load(GString::from("res://ext_test/test_resource.gdron")) + .unwrap() + .cast::(); + let test_godot_res = loader + .load(GString::from("res://ext_test/test_godot_res.tres")) + .unwrap() + .cast::(); + + let resource = Gd::::from_object(WithExtGd { + first: test_res, + second: Some(test_godot_res), + }); + + let mut buffer = Vec::new(); + let mut serializer = ron::Serializer::new(BufWriter::new(&mut buffer), None).unwrap(); + + let result = resource.bind().serialize(&mut serializer); + if let Err(error) = &result { + println!("{}", error); + } + assert!(result.is_ok()); + drop(serializer); + + let result = ron::de::from_bytes::(&buffer); + if let Err(error) = &result { + println!("{}", error); + } + assert!(result.is_ok()); + let deserialized = result.unwrap(); + + assert_eq!( + deserialized.first.get_path(), + GString::from("res://ext_test/test_resource.gdron") + ); + assert_eq!( + deserialized.second.unwrap().get_path(), + GString::from("res://ext_test/test_godot_res.tres") + ); +} + +#[gditest] +fn serde_external_resvec() { + let path = "res://"; + let subresources = TestGodotResource::new_saved_multiple(path, 5); + let mut vec = GdResVec::default(); + for (_, subresource) in subresources.iter() { + vec.push(subresource.clone()); + } + let resource = WithExtResVec { vec }; + + let mut buffer = Vec::new(); + let mut serializer = Serializer::new(&mut buffer, None).unwrap(); + + let result = resource.serialize(&mut serializer); + assert!(result.is_ok()); + drop(serializer); + + let result = ron::de::from_bytes::(&buffer); + assert!(result.is_ok()); + let deserialized = result.unwrap(); + + let mut deserialized_hash = HashMap::new(); + + for subresource in deserialized.vec.iter() { + let res_path = subresource.get_path().to_string(); + let trimmed = res_path.trim_start_matches(path.clone()); + deserialized_hash.insert(trimmed.to_owned(), subresource.clone()); + } + + for (file, subresource) in subresources.iter() { + let de_res = deserialized_hash.get(file).unwrap(); + + assert_eq!(de_res.bind().int, subresource.bind().int); + assert_eq!(de_res.bind().str, subresource.bind().str); + + remove_file(path, file); + } +} + +#[gditest] +fn serde_external_hashmap() { + let path = "res://"; + let subresources = TestGodotResource::new_saved_multiple(path, 5); + let mut map = HashMap::new(); + for (key, subresource) in subresources.iter() { + map.insert(key.clone(), subresource.clone()); + } + let resource = WithExtHashMap { map }; + + let mut buffer = Vec::new(); + let mut serializer = Serializer::new(&mut buffer, None).unwrap(); + + let result = resource.serialize(&mut serializer); + assert!(result.is_ok()); + drop(serializer); + + let result = ron::de::from_bytes::(&buffer); + assert!(result.is_ok()); + let deserialized = result.unwrap(); + + for (file, subresource) in subresources.iter() { + let de_res = deserialized.map.get(file).unwrap(); + + assert_eq!(de_res.bind().int, subresource.bind().int); + assert_eq!(de_res.bind().str, subresource.bind().str); + + remove_file(path, file); + } +} diff --git a/tests/rust/src/itest/types.rs b/tests/rust/src/itest/types.rs new file mode 100644 index 0000000..45ecb16 --- /dev/null +++ b/tests/rust/src/itest/types.rs @@ -0,0 +1,34 @@ +use gd_props::types::GdResVec; +use gd_rehearse::itest::gditest; +use godot::{ + builtin::{meta::ToGodot, Array, VariantArray}, + engine::Resource, + obj::Gd, +}; + +#[gditest] +fn gdresvec_construction() { + let mut resvec = GdResVec::default(); + resvec.push(Gd::::default()); + resvec.push(Gd::::default()); + assert_eq!(resvec.len(), 2); + + // Create from existing vector + let vector = vec![Gd::::default(), Gd::::default()]; + let from_vec = GdResVec::from_vec(vector); + assert_eq!(from_vec.len(), 2); + + // Create from typed Godot array + let mut typed_arr: Array> = Array::new(); + typed_arr.push(Gd::::default()); + typed_arr.push(Gd::::default()); + let from_typed_arr = GdResVec::from_array(typed_arr); + assert_eq!(from_typed_arr.len(), 2); + + // Create from variant array + let mut var_arr = VariantArray::new(); + var_arr.push(Gd::::default().to_variant()); + var_arr.push(Gd::::default().to_variant()); + let from_var_arr = GdResVec::::from_variant_array(var_arr); + assert_eq!(from_var_arr.len(), 2); +} diff --git a/tests/rust/src/lib.rs b/tests/rust/src/lib.rs index 204cc73..dc5dc57 100644 --- a/tests/rust/src/lib.rs +++ b/tests/rust/src/lib.rs @@ -16,6 +16,7 @@ fn remove_file(path: impl Into, file_name: impl Into) { struct GodotIoTests; pub use gd_rehearse::GdTestRunner; +use rand::Rng; use structs::prop_handlers::{PropLoader, PropSaver}; // use crate::structs::{resource::TestResource, singleton::GodotSingleton}; @@ -35,3 +36,9 @@ unsafe impl ExtensionLibrary for GodotIoTests { #[cfg(test)] mod tests; + +pub fn random_string(rng: &mut impl Rng, len: usize) -> String { + (0..len) + .map(|_| rng.gen_range(b'A'..=b'Z') as char) + .collect::() +} diff --git a/tests/rust/src/structs/prop_handlers.rs b/tests/rust/src/structs/prop_handlers.rs index 98cc8f9..1cc3c6c 100644 --- a/tests/rust/src/structs/prop_handlers.rs +++ b/tests/rust/src/structs/prop_handlers.rs @@ -1,13 +1,13 @@ -use super::resource::TestResource; +use super::resource::*; use gd_props::{GdPropLoader, GdPropSaver}; use godot::bind::GodotClass; #[derive(GodotClass, GdPropSaver)] #[class(init, base = ResourceFormatSaver, tool)] -#[register(TestResource)] +#[register(TestResource, WithBundledGd, WithExtGd, WithBundleResVec)] pub struct PropSaver; #[derive(GodotClass, GdPropLoader)] #[class(init, base = ResourceFormatLoader, tool)] -#[register(TestResource)] +#[register(TestResource, WithBundledGd, WithExtGd, WithBundleResVec)] pub struct PropLoader; diff --git a/tests/rust/src/structs/resource.rs b/tests/rust/src/structs/resource.rs index 7cfc21c..d77fb9d 100644 --- a/tests/rust/src/structs/resource.rs +++ b/tests/rust/src/structs/resource.rs @@ -1,13 +1,18 @@ -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use gd_props::GdProp; -use godot::{ - obj::Gd, - prelude::{godot_api, GodotClass}, -}; + +use gd_props::types::GdResVec; +use godot::builtin::GString; +use godot::engine::{IResource, ResourceSaver}; +use godot::obj::Gd; +use godot::prelude::{godot_api, GodotClass}; + use rand::Rng; use serde::{Deserialize, Serialize}; +use crate::random_string; + use super::singleton::GodotSingleton; #[derive(Eq, PartialEq, Hash, Clone, Serialize, Deserialize)] @@ -93,3 +98,128 @@ impl GodotSingleton for TestResource { Self::new_random(50, 50) } } + +#[derive(GodotClass, Serialize, Deserialize, GdProp)] +#[class(base=Resource)] +pub(crate) struct WithBundledGd { + #[export] + #[serde(with = "gd_props::serde_gd::gd")] + pub first: Gd, + #[export] + #[serde(with = "gd_props::serde_gd::gd_option")] + pub second: Option>, +} + +impl WithBundledGd { + pub(crate) fn new() -> Self { + Self { + first: TestResource::new_random(3, 4), + second: Some(TestResource::new_random(5, 2)), + } + } +} + +#[godot_api] +impl IResource for WithBundledGd { + fn init(_base: godot::obj::Base) -> Self { + Self::new() + } +} + +#[derive(GodotClass, Serialize, Deserialize, GdProp)] +#[class(base=Resource, init)] +pub(crate) struct WithBundleHashMap { + #[serde(with = "gd_props::serde_gd::gd_hashmap")] + pub map: HashMap>, +} + +impl WithBundleHashMap { + pub fn new(res_n: usize) -> Self { + let mut map = HashMap::new(); + let mut rng = rand::thread_rng(); + let mut set = HashSet::new(); + for _ in 0..res_n { + set.insert(rng.gen_range(-256..=256)); + } + for key in set { + let vec_n = rng.gen_range(1..10); + let set_n = rng.gen_range(1..10); + let res = TestResource::new_random(set_n, vec_n); + map.insert(key, res); + } + Self { map } + } +} + +#[derive(GodotClass, Serialize, Deserialize, GdProp)] +#[class(base=Resource, init)] +pub(crate) struct WithBundleResVec { + #[export] + #[serde(with = "gd_props::serde_gd::gd_resvec")] + pub vec: GdResVec, +} + +#[derive(GodotClass)] +#[class(base=Resource)] +pub(crate) struct TestGodotResource { + #[export] + pub(crate) int: i32, + #[export] + pub(crate) str: GString, +} + +impl TestGodotResource { + pub fn new() -> Self { + let mut rng = rand::thread_rng(); + let int = rng.gen_range(-1000..1000); + let str = random_string(&mut rng, 10).into(); + Self { int, str } + } + + pub fn new_saved_multiple(path: &str, n: usize) -> HashMap> { + let mut obj = HashMap::new(); + let mut saver = ResourceSaver::singleton(); + for _ in 0..n { + let mut rng = rand::thread_rng(); + let mut file = random_string(&mut rng, 10); + file.push_str(".tres"); + let mut res = Gd::::default(); + res.set_path(format!("{path}{file}").into()); + saver.save(res.clone().upcast()); + obj.insert(file, res); + } + obj + } +} + +#[godot_api] +impl IResource for TestGodotResource { + fn init(_base: godot::obj::Base) -> Self { + Self::new() + } +} + +#[derive(GodotClass, Serialize, Deserialize, GdProp)] +#[class(base=Resource, init)] +pub(crate) struct WithExtGd { + #[export] + #[serde(with = "gd_props::serde_gd::ext")] + pub first: Gd, + #[export] + #[serde(with = "gd_props::serde_gd::ext_option")] + pub second: Option>, +} + +#[derive(GodotClass, Serialize, Deserialize, GdProp)] +#[class(base=Resource, init)] +pub(crate) struct WithExtResVec { + #[serde(with = "gd_props::serde_gd::ext_resvec")] + pub vec: GdResVec, +} + +#[derive(GodotClass, Serialize, Deserialize, GdProp)] +#[class(base=Resource, init)] +pub(crate) struct WithExtHashMap { + #[serde(with = "gd_props::serde_gd::ext_hashmap")] + pub map: HashMap>, +}