diff --git a/Cargo.toml b/Cargo.toml index 2406ccd..98dc51c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,8 @@ members = [ "examples/resource", "examples/native-plugin", "examples/rpc", - "examples/array-export", + "examples/builder-export", + "examples/property-export", "impl/proc-macros" ] diff --git a/README.md b/README.md index dca06b1..801f1a9 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,8 @@ The [/examples](https://github.com/godot-rust/godot-rust/tree/master/examples) d - [**hello-world**](https://github.com/godot-rust/godot-rust/tree/master/examples/hello-world) - Your first project, writes to the console. - [**spinning-cube**](https://github.com/godot-rust/godot-rust/tree/master/examples/spinning-cube) - Spin our own node in place, exposing editor properties. - [**scene-create**](https://github.com/godot-rust/godot-rust/tree/master/examples/scene-create) - Load, instance and place scenes using Rust code. -- [**array-export**](https://github.com/godot-rust/godot-rust/tree/master/examples/array-export) - Export more complex properties (here arrays) from Rust. +- [**builder-export**](https://github.com/godot-rust/godot-rust/tree/master/examples/builder-export) - Export using the builder API. +- [**property-export**](https://github.com/godot-rust/godot-rust/tree/master/examples/property-export) - Export complex properties such as collections. - [**dodge-the-creeps**](https://github.com/godot-rust/godot-rust/tree/master/examples/dodge-the-creeps) - A Rust port of the [little Godot game](https://docs.godotengine.org/en/stable/getting_started/step_by_step/your_first_game.html). - [**signals**](https://github.com/godot-rust/godot-rust/tree/master/examples/signals) - Connect and emit signals. - [**resource**](https://github.com/godot-rust/godot-rust/tree/master/examples/resource) - Create and use custom resources. diff --git a/examples/array-export/array_export_library.gdnlib b/examples/array-export/array_export_library.gdnlib deleted file mode 100644 index 563c0c4..0000000 --- a/examples/array-export/array_export_library.gdnlib +++ /dev/null @@ -1,17 +0,0 @@ -[entry] - -X11.64="res://../../target/debug/libarray_export.so" -OSX.64="res://../../target/debug/libarray_export.dylib" -Windows.64="res://../../target/debug/array_export.dll" - -[dependencies] - -X11.64=[ ] -OSX.64=[ ] - -[general] - -singleton=false -load_once=true -symbol_prefix="godot_" -reloadable=true diff --git a/examples/array-export/Cargo.toml b/examples/builder-export/Cargo.toml similarity index 90% rename from examples/array-export/Cargo.toml rename to examples/builder-export/Cargo.toml index 7f8bd2b..bdadd3d 100644 --- a/examples/array-export/Cargo.toml +++ b/examples/builder-export/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "array-export" +name = "builder-export" version = "0.1.0" authors = ["The godot-rust developers"] edition = "2021" diff --git a/examples/array-export/ExportsArrays.gdns b/examples/builder-export/ExportsArrays.gdns similarity index 69% rename from examples/array-export/ExportsArrays.gdns rename to examples/builder-export/ExportsArrays.gdns index e4a63b7..eb08650 100644 --- a/examples/array-export/ExportsArrays.gdns +++ b/examples/builder-export/ExportsArrays.gdns @@ -1,6 +1,6 @@ [gd_resource type="NativeScript" load_steps=2 format=2] -[ext_resource path="res://array_export_library.gdnlib" type="GDNativeLibrary" id=1] +[ext_resource path="res://builder_export_library.gdnlib" type="GDNativeLibrary" id=1] [resource] resource_name = "ExportsArrays" diff --git a/examples/array-export/ExportsArrays.tscn b/examples/builder-export/ExportsArrays.tscn similarity index 100% rename from examples/array-export/ExportsArrays.tscn rename to examples/builder-export/ExportsArrays.tscn diff --git a/examples/builder-export/builder_export_library.gdnlib b/examples/builder-export/builder_export_library.gdnlib new file mode 100644 index 0000000..6cd60c6 --- /dev/null +++ b/examples/builder-export/builder_export_library.gdnlib @@ -0,0 +1,17 @@ +[entry] + +X11.64="res://../../target/debug/libbuilder_export.so" +OSX.64="res://../../target/debug/libbuilder_export.dylib" +Windows.64="res://../../target/debug/builder_export.dll" + +[dependencies] + +X11.64=[ ] +OSX.64=[ ] + +[general] + +singleton=false +load_once=true +symbol_prefix="godot_" +reloadable=true diff --git a/examples/array-export/default_env.tres b/examples/builder-export/default_env.tres similarity index 100% rename from examples/array-export/default_env.tres rename to examples/builder-export/default_env.tres diff --git a/examples/array-export/icon.png b/examples/builder-export/icon.png similarity index 100% rename from examples/array-export/icon.png rename to examples/builder-export/icon.png diff --git a/examples/array-export/icon.png.import b/examples/builder-export/icon.png.import similarity index 100% rename from examples/array-export/icon.png.import rename to examples/builder-export/icon.png.import diff --git a/examples/array-export/project.godot b/examples/builder-export/project.godot similarity index 96% rename from examples/array-export/project.godot rename to examples/builder-export/project.godot index 426f913..9721870 100644 --- a/examples/array-export/project.godot +++ b/examples/builder-export/project.godot @@ -20,7 +20,7 @@ _global_script_class_icons={ [application] -config/name="array_export" +config/name="builder_export" run/main_scene="res://ExportsArrays.tscn" config/icon="res://icon.png" diff --git a/examples/array-export/src/lib.rs b/examples/builder-export/src/lib.rs similarity index 100% rename from examples/array-export/src/lib.rs rename to examples/builder-export/src/lib.rs diff --git a/examples/property-export/Cargo.toml b/examples/property-export/Cargo.toml new file mode 100644 index 0000000..2ed5f2b --- /dev/null +++ b/examples/property-export/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "property-export" +version = "0.1.0" +authors = ["The godot-rust developers"] +edition = "2021" +rust-version = "1.56" +license = "MIT" +publish = false + +[lib] +crate-type = ["cdylib"] + +[dependencies] +gdnative = { path = "../../gdnative" } diff --git a/examples/property-export/GDScriptPrinter.gd b/examples/property-export/GDScriptPrinter.gd new file mode 100644 index 0000000..6aed1a9 --- /dev/null +++ b/examples/property-export/GDScriptPrinter.gd @@ -0,0 +1,22 @@ +extends Node + +func _ready(): + var rust = get_node("../PropertyExport") + + print("\n-----------------------------------------------------------------") + print("Print from GDScript (note the lexicographically ordered map/set):") + print(" Vec (name):"); + for name in rust.name_vec: + print(" * %s" % name) + + print("\n HashMap (string -> color):") + for string in rust.color_map: + var color = rust.color_map[string] + print(" * %s -> #%s" % [string, color.to_html(false)]); + + print("\n HashSet (ID):") + for id in rust.id_set: + print(" * %s" % id) + + # The program has printed the contents and fulfilled its purpose, quit + get_tree().quit() \ No newline at end of file diff --git a/examples/property-export/Main.tscn b/examples/property-export/Main.tscn new file mode 100644 index 0000000..976a1f2 --- /dev/null +++ b/examples/property-export/Main.tscn @@ -0,0 +1,24 @@ +[gd_scene load_steps=4 format=2] + +[ext_resource path="res://property_export_library.gdnlib" type="GDNativeLibrary" id=1] +[ext_resource path="res://GDScriptPrinter.gd" type="Script" id=2] + +[sub_resource type="NativeScript" id=1] +resource_name = "PropertyExport" +class_name = "PropertyExport" +library = ExtResource( 1 ) + +[node name="Node" type="Node"] + +[node name="PropertyExport" type="Node" parent="."] +script = SubResource( 1 ) +name_vec = [ "Godot", "Godette", "Go ." ] +color_map = { +"blue": Color( 0.184314, 0.160784, 0.8, 1 ), +"green": Color( 0.0941176, 0.447059, 0.192157, 1 ), +"teal": Color( 0.0941176, 0.423529, 0.564706, 1 ) +} +id_set = [ 21, 77, 8, 90 ] + +[node name="GDScriptPrinter" type="Node" parent="."] +script = ExtResource( 2 ) diff --git a/examples/property-export/default_env.tres b/examples/property-export/default_env.tres new file mode 100644 index 0000000..dfe62ab --- /dev/null +++ b/examples/property-export/default_env.tres @@ -0,0 +1,14 @@ +[gd_resource type="Environment" load_steps=2 format=2] + +[sub_resource type="ProceduralSky" id=1] +sky_top_color = Color( 0.0470588, 0.454902, 0.976471, 1 ) +sky_horizon_color = Color( 0.556863, 0.823529, 0.909804, 1 ) +sky_curve = 0.25 +ground_bottom_color = Color( 0.101961, 0.145098, 0.188235, 1 ) +ground_horizon_color = Color( 0.482353, 0.788235, 0.952941, 1 ) +ground_curve = 0.01 +sun_energy = 16.0 + +[resource] +background_mode = 2 +background_sky = SubResource( 1 ) diff --git a/examples/property-export/project.godot b/examples/property-export/project.godot new file mode 100644 index 0000000..abf4b06 --- /dev/null +++ b/examples/property-export/project.godot @@ -0,0 +1,22 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=4 + +_global_script_classes=[ ] +_global_script_class_icons={ +} + +[application] + +config/name="Godot Rust - Property Export" +run/main_scene="res://Main.tscn" + +[rendering] + +environment/default_environment="res://default_env.tres" diff --git a/examples/property-export/property_export_library.gdnlib b/examples/property-export/property_export_library.gdnlib new file mode 100644 index 0000000..40b0bb0 --- /dev/null +++ b/examples/property-export/property_export_library.gdnlib @@ -0,0 +1,17 @@ +[entry] + +X11.64="res://../../target/debug/libproperty_export.so" +OSX.64="res://../../target/debug/libproperty_export.dylib" +Windows.64="res://../../target/debug/property_export.dll" + +[dependencies] + +X11.64=[ ] +OSX.64=[ ] + +[general] + +singleton=false +load_once=true +symbol_prefix="godot_" +reloadable=true diff --git a/examples/property-export/src/lib.rs b/examples/property-export/src/lib.rs new file mode 100644 index 0000000..0b54b86 --- /dev/null +++ b/examples/property-export/src/lib.rs @@ -0,0 +1,49 @@ +use gdnative::prelude::*; + +use std::collections::{HashMap, HashSet}; + +#[derive(NativeClass, Default)] +#[inherit(Node)] +pub struct PropertyExport { + #[property] + name_vec: Vec, + + #[property] + color_map: HashMap, + + #[property] + id_set: HashSet, +} + +#[methods] +impl PropertyExport { + fn new(_base: &Node) -> Self { + Self::default() + } + + #[export] + fn _ready(&self, _base: &Node) { + godot_print!("------------------------------------------------------------------"); + godot_print!("Print from Rust (note the unordered map/set):"); + godot_print!(" Vec (name):"); + for name in &self.name_vec { + godot_print!(" * {}", name); + } + + godot_print!("\n HashMap (string -> color):"); + for (string, color) in &self.color_map { + godot_print!(" * {} -> #{}", string, color.to_html(false)); + } + + godot_print!("\n HashSet (ID):"); + for id in &self.id_set { + godot_print!(" * {}", id); + } + } +} + +fn init(handle: InitHandle) { + handle.add_class::(); +} + +godot_init!(init); diff --git a/gdnative-core/src/core_types/variant.rs b/gdnative-core/src/core_types/variant.rs index 762cfc6..989b0db 100644 --- a/gdnative-core/src/core_types/variant.rs +++ b/gdnative-core/src/core_types/variant.rs @@ -1,6 +1,6 @@ use crate::*; use std::borrow::Cow; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::default::Default; use std::fmt; use std::hash::Hash; @@ -1605,17 +1605,37 @@ impl FromVariant for Vec { } } +/// Converts the hash map to a `Dictionary`, wrapped in a `Variant`. +/// +/// Note that Rust's `HashMap` is non-deterministically ordered for security reasons, meaning that +/// the order of the same elements will differ between two program invocations. To provide a +/// deterministic output in Godot (e.g. UI elements for properties), the elements are sorted by key. impl ToVariant for HashMap { #[inline] fn to_variant(&self) -> Variant { + // Note: dictionary currently provides neither a sort() function nor random access (or at least bidirectional) + // iterators, making it difficult to sort in-place. Workaround: copy to vector + + let mut intermediate: Vec<(Variant, Variant)> = self + .iter() + .map(|(k, v)| (k.to_variant(), v.to_variant())) + .collect(); + + intermediate.sort(); + let dict = Dictionary::new(); - for (key, value) in self { + for (key, value) in intermediate.into_iter() { dict.insert(key, value); } + dict.owned_to_variant() } } +/// Expects a `Variant` populated with a `Dictionary` and tries to convert it into a `HashMap`. +/// +/// Since Rust's `HashMap` is unordered, there is no guarantee about the resulting element order. +/// In fact it is possible that two program invocations cause a different output. impl FromVariant for HashMap { #[inline] fn from_variant(variant: &Variant) -> Result { @@ -1624,6 +1644,7 @@ impl FromVariant for HashMap { .len() .try_into() .expect("Dictionary length should fit in usize"); + let mut hash_map = HashMap::with_capacity(len); for (key, value) in dictionary.iter() { hash_map.insert(K::from_variant(&key)?, V::from_variant(&value)?); @@ -1632,6 +1653,50 @@ impl FromVariant for HashMap { } } +/// Converts the hash set to a `VariantArray`, wrapped in a `Variant`. +/// +/// Note that Rust's `HashSet` is non-deterministically ordered for security reasons, meaning that +/// the order of the same elements will differ between two program invocations. To provide a +/// deterministic output in Godot (e.g. UI elements for properties), the elements are sorted by key. +impl ToVariant for HashSet { + #[inline] + fn to_variant(&self) -> Variant { + let array = VariantArray::new(); + for value in self { + array.push(value.to_variant()); + } + + array.sort(); // deterministic order in Godot + array.owned_to_variant() + } +} + +/// Expects a `Variant` populated with a `VariantArray` and tries to convert it into a `HashSet`. +/// +/// Since Rust's `HashSet` is unordered, there is no guarantee about the resulting element order. +/// In fact it is possible that two program invocations cause a different output. +impl FromVariant for HashSet { + #[inline] + fn from_variant(variant: &Variant) -> Result { + let arr = VariantArray::from_variant(variant)?; + let len: usize = arr + .len() + .try_into() + .expect("VariantArray length should fit in usize"); + + let mut set = HashSet::with_capacity(len); + for idx in 0..len as i32 { + let item = + T::from_variant(&arr.get(idx)).map_err(|e| FromVariantError::InvalidItem { + index: idx as usize, + error: Box::new(e), + })?; + set.insert(item); + } + Ok(set) + } +} + macro_rules! tuple_length { () => { 0usize }; ($_x:ident, $($xs:ident,)*) => { @@ -1836,6 +1901,63 @@ godot_test!( ); } + test_variant_hash_set { + let original_hash_set = HashSet::from([ + "Foo".to_string(), + "Bar".to_string(), + ]); + let variant = original_hash_set.to_variant(); + let check_hash_set = variant.try_to::>().expect("should be hash set"); + assert_eq!(original_hash_set, check_hash_set); + + let duplicate_variant_set = VariantArray::new(); + duplicate_variant_set.push("Foo".to_string()); + duplicate_variant_set.push("Bar".to_string()); + duplicate_variant_set.push("Bar".to_string()); + let duplicate_hash_set = variant.try_to::>().expect("should be hash set"); + assert_eq!(original_hash_set, duplicate_hash_set); + + // Check conversion of heterogeneous set types + let non_homogenous_set = VariantArray::new(); + non_homogenous_set.push("Foo".to_string()); + non_homogenous_set.push(7); + assert_eq!( + non_homogenous_set.owned_to_variant().try_to::>(), + Err(FromVariantError::InvalidItem { + index: 1, + error: Box::new(FromVariantError::InvalidVariantType { + variant_type: VariantType::I64, + expected: VariantType::GodotString + }) + }), + ); + } + + test_variant_vec { + let original_vec = Vec::from([ + "Foo".to_string(), + "Bar".to_string(), + ]); + let variant = original_vec.to_variant(); + let check_vec = variant.try_to::>().expect("should be hash set"); + assert_eq!(original_vec, check_vec); + + // Check conversion of heterogeneous vec types + let non_homogenous_vec = VariantArray::new(); + non_homogenous_vec.push("Foo".to_string()); + non_homogenous_vec.push(7); + assert_eq!( + non_homogenous_vec.owned_to_variant().try_to::>(), + Err(FromVariantError::InvalidItem { + index: 1, + error: Box::new(FromVariantError::InvalidVariantType { + variant_type: VariantType::I64, + expected: VariantType::GodotString + }) + }), + ); + } + test_variant_tuple { let variant = (42i64, 54i64).to_variant(); let arr = variant.try_to::().expect("should be array"); diff --git a/gdnative-core/src/export/property.rs b/gdnative-core/src/export/property.rs index bc98b56..d899048 100644 --- a/gdnative-core/src/export/property.rs +++ b/gdnative-core/src/export/property.rs @@ -433,6 +433,8 @@ pub struct Property { } mod impl_export { + use std::collections::{HashMap, HashSet}; + use super::*; /// Hint type indicating that there are no hints available for the time being. @@ -591,4 +593,41 @@ mod impl_export { hint.unwrap_or_default().export_info() } } + + impl Export for HashMap + where + K: std::hash::Hash + ToVariantEq + ToVariant, + V: ToVariant, + { + type Hint = NoHint; + + #[inline] + fn export_info(_hint: Option) -> ExportInfo { + ExportInfo::new(VariantType::Dictionary) + } + } + + impl Export for HashSet + where + T: ToVariant, + { + type Hint = NoHint; + + #[inline] + fn export_info(_hint: Option) -> ExportInfo { + ExportInfo::new(VariantType::VariantArray) + } + } + + impl Export for Vec + where + T: ToVariant, + { + type Hint = NoHint; + + #[inline] + fn export_info(_hint: Option) -> ExportInfo { + ExportInfo::new(VariantType::VariantArray) + } + } } diff --git a/test/src/lib.rs b/test/src/lib.rs index 0f1a19d..36c2f96 100644 --- a/test/src/lib.rs +++ b/test/src/lib.rs @@ -45,6 +45,8 @@ pub extern "C" fn run_tests( status &= gdnative::core_types::test_variant_option(); status &= gdnative::core_types::test_variant_result(); status &= gdnative::core_types::test_variant_hash_map(); + status &= gdnative::core_types::test_variant_hash_set(); + status &= gdnative::core_types::test_variant_vec(); status &= gdnative::core_types::test_to_variant_iter(); status &= gdnative::core_types::test_variant_tuple(); status &= gdnative::core_types::test_variant_dispatch();