From fe773e6f29b4980d595b6f27ce1887ff9c76b287 Mon Sep 17 00:00:00 2001 From: Paolo Roth Date: Thu, 6 Jun 2024 10:56:11 +0200 Subject: [PATCH] Feat/csv-import (#3) * feat: starts working on the csv importer * feat: adds CSV resource importer * feat: adds export to csv --------- Co-authored-by: OctoD --- .vscode/tasks.json | 4 +- Cargo.toml | 4 +- src/editor/docks/node_tagging_dock.rs | 21 ++-- .../csv_import_plugin.rs | 112 ++++++++++++++++++ src/editor/editor_import_plugins/mod.rs | 1 + .../tag_dictionary_editor_inspector_plugin.rs | 78 ++++++++++-- src/editor/mod.rs | 17 ++- src/editor/tag_dictionary_fs.rs | 38 +++--- src/importers/csv_importer.rs | 38 ------ src/importers/mod.rs | 1 - src/lib.rs | 46 +++---- src/tag_dictionary.rs | 73 +++++++----- 12 files changed, 296 insertions(+), 137 deletions(-) create mode 100644 src/editor/editor_import_plugins/csv_import_plugin.rs create mode 100644 src/editor/editor_import_plugins/mod.rs delete mode 100644 src/importers/csv_importer.rs delete mode 100644 src/importers/mod.rs diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 024e1a5..a714cae 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -13,13 +13,13 @@ "options": { "env": { "RUST_BACKTRACE": "1" - }, + } }, "runOptions": { "instanceLimit": 1 }, "detail": "Run the built addon in Godot", - "command": "godot godot/project.godot" + "command": "godot godot/project.godot", }, { "type": "cargo", diff --git a/Cargo.toml b/Cargo.toml index 60f3b73..09741df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "godot_gameplay_tags" -version = "0.1.0" +version = "0.3.0" edition = "2021" [lib] @@ -9,4 +9,4 @@ crate-type = ["cdylib"] # Compile this crate to a dynamic C library. # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -godot = { git = "https://github.com/godot-rust/gdext", branch = "master" } +godot = { git = "https://github.com/godot-rust/gdext", branch = "master", features = ["experimental-threads"] } diff --git a/src/editor/docks/node_tagging_dock.rs b/src/editor/docks/node_tagging_dock.rs index 11e4513..46238d0 100644 --- a/src/editor/docks/node_tagging_dock.rs +++ b/src/editor/docks/node_tagging_dock.rs @@ -56,10 +56,10 @@ impl NodeTaggingDock { #[func] fn render_tag_trees(&mut self) { - // let's start with cleaning up child elements - for mut child in self.to_gd().get_children().iter_shared() { - child.queue_free(); - } + // let's start with cleaning up child elements + for mut child in self.to_gd().get_children().iter_shared() { + child.queue_free(); + } let mut tag_dictionary_fs = TagDictionaryFs::new_gd(); let tag_manager = TagManager::new_alloc(); @@ -100,12 +100,11 @@ impl IVBoxContainer for NodeTaggingDock { fn ready(&mut self) { self.to_gd().set_name("Node tags".into()); - if let Some(mut selection) = EditorInterface::singleton().get_selection() { - selection.connect( - "selection_changed".into(), - Callable::from_object_method(&self.to_gd(), "render_tag_trees"), - ); - } - + if let Some(mut selection) = EditorInterface::singleton().get_selection() { + selection.connect( + "selection_changed".into(), + Callable::from_object_method(&self.to_gd(), "render_tag_trees"), + ); + } } } diff --git a/src/editor/editor_import_plugins/csv_import_plugin.rs b/src/editor/editor_import_plugins/csv_import_plugin.rs new file mode 100644 index 0000000..ad50ec7 --- /dev/null +++ b/src/editor/editor_import_plugins/csv_import_plugin.rs @@ -0,0 +1,112 @@ +use godot::{ + engine::{file_access::ModeFlags, FileAccess, IEditorImportPlugin, ResourceSaver}, + global::Error, + prelude::*, +}; + +use crate::tag_dictionary::TagDictionary; + +#[derive(GodotClass)] +#[class(tool, hidden, init, base = EditorImportPlugin)] +pub struct CsvImportPlugin {} + +#[godot_api] +impl IEditorImportPlugin for CsvImportPlugin { + fn get_import_options(&self, _path: GString, _preset_index: i32) -> Array { + Array::new() as Array + } + + fn get_import_order(&self) -> i32 { + 0 + } + + fn get_importer_name(&self) -> GString { + "ggt_importer_csv".into() + } + + fn get_recognized_extensions(&self) -> PackedStringArray { + let mut out = PackedStringArray::new(); + out.push("csv".into()); + out + } + + fn get_option_visibility( + &self, + _path: GString, + _option_name: StringName, + _options: Dictionary, + ) -> bool { + true + } + + fn get_preset_count(&self) -> i32 { + 0 + } + + fn get_preset_name(&self, _preset_index: i32) -> GString { + "".into() + } + + fn get_priority(&self) -> f32 { + 1_000.0 + } + + fn get_resource_type(&self) -> GString { + "TagDictionary".into() + } + + fn get_save_extension(&self) -> GString { + "".into() + } + + fn get_visible_name(&self) -> GString { + "Gameplay Tags Importer".into() + } + + fn import( + &self, + source_file: GString, + _save_path: GString, + _options: Dictionary, + _platform_variants: Array, + mut gen_files: Array, + ) -> Error { + let mut tag_dictionary = TagDictionary::new_gd(); + let resource_save_path = source_file.to_string() + ".tres"; + + tag_dictionary.take_over_path(resource_save_path.clone().into()); + + if let Some(file_contents) = FileAccess::open(source_file.clone(), ModeFlags::READ) { + let text_content = file_contents.get_as_text(); + let text_content_str = text_content.to_string(); + let lines = text_content_str.lines(); + let mut imported_line_count = 0; + + if lines.clone().count() == 0 { + return Error::OK; + } + + lines.for_each(|line| { + let binding = line.replace(",", ".").replace("..", ""); + let mut tag = binding; + + if tag.ends_with(".") { + tag = tag[0..tag.len() - 1].to_string(); + } + + if !tag.is_empty() && tag_dictionary.bind_mut().add_tag(tag.into()) { + imported_line_count += 1; + } + }); + + if imported_line_count > 0 { + gen_files.push(resource_save_path.into()); + return ResourceSaver::singleton().save(tag_dictionary.upcast()); + } + + return Error::OK; + } + + Error::ERR_CANT_OPEN + } +} diff --git a/src/editor/editor_import_plugins/mod.rs b/src/editor/editor_import_plugins/mod.rs new file mode 100644 index 0000000..485c96b --- /dev/null +++ b/src/editor/editor_import_plugins/mod.rs @@ -0,0 +1 @@ +pub mod csv_import_plugin; diff --git a/src/editor/inspector_plugins/tag_dictionary_editor_inspector_plugin.rs b/src/editor/inspector_plugins/tag_dictionary_editor_inspector_plugin.rs index bee2c26..355c2bc 100644 --- a/src/editor/inspector_plugins/tag_dictionary_editor_inspector_plugin.rs +++ b/src/editor/inspector_plugins/tag_dictionary_editor_inspector_plugin.rs @@ -1,6 +1,9 @@ +use godot::engine::box_container::AlignmentMode; use godot::engine::control::{LayoutPreset, SizeFlags}; +use godot::engine::file_access::ModeFlags; use godot::engine::{ - EditorInspectorPlugin, IEditorInspectorPlugin, ResourceSaver + Button, EditorInspectorPlugin, FileAccess, HBoxContainer, IEditorInspectorPlugin, + ResourceSaver, VBoxContainer, }; use godot::prelude::*; @@ -11,6 +14,7 @@ use crate::tag_dictionary::TagDictionary; #[class(tool, init, base = EditorInspectorPlugin)] pub struct TagDictionaryEditorInspectorPlugin { base: Base, + tag_dictionary: Option>, } #[godot_api] @@ -19,6 +23,41 @@ impl TagDictionaryEditorInspectorPlugin { pub fn handle_tag_dictionary_changed(tag_dictionary: Gd) { ResourceSaver::singleton().save(tag_dictionary.to_variant().to()); } + + #[func] + pub fn handle_tag_dictionary_export(&self) { + if let Some(td) = self.tag_dictionary.clone() { + let tags = td.bind().get_tags(); + let mut output = String::from(""); + + for tag in tags.as_slice() { + output.push_str(&format!("{}\n", tag)); + } + + if let Some(mut f) = FileAccess::open( + td.get_path().to_string().replace("tres", "csv").into(), + ModeFlags::WRITE, + ) { + f.store_string(GString::from(output)); + f.close(); + godot::global::print(&[ + "Exported tag dictionary to: ".to_variant(), + td.get_path() + .to_string() + .replace("tres", "csv") + .to_variant(), + ]); + } else { + godot::global::printerr(&[ + "Failed to open file for writing: ".to_variant(), + td.get_path() + .to_string() + .replace("tres", "csv") + .to_variant(), + ]); + } + } + } } #[godot_api] @@ -41,23 +80,36 @@ impl IEditorInspectorPlugin for TagDictionaryEditorInspectorPlugin { return false; } + let mut export_button = Button::new_alloc(); + + export_button.connect( + "pressed".into(), + Callable::from_object_method(&self.to_gd(), "handle_tag_dictionary_export"), + ); + export_button.set_text("Export to CVS".into()); + + let mut footer_container = HBoxContainer::new_alloc(); + + footer_container.add_child(export_button.to_variant().to()); + footer_container.set_alignment(AlignmentMode::END); + let mut tag_tree = TagTree::new_alloc(); let mut tag_dictionary = _object .try_cast::() .expect("Failed to cast to TagDictionary"); let mut callable_args = VariantArray::new(); + self.tag_dictionary = Some(tag_dictionary.clone()); + callable_args.push(tag_dictionary.clone().to_variant()); - - let callable = Callable::from_object_method(&self.to_gd(), "handle_tag_dictionary_changed").bindv(callable_args); + + let callable = Callable::from_object_method(&self.to_gd(), "handle_tag_dictionary_changed") + .bindv(callable_args); if !tag_dictionary.is_connected("changed".into(), callable.clone()) { - tag_dictionary.connect( - "changed".into(), - callable - ); + tag_dictionary.connect("changed".into(), callable); } - + tag_tree.bind_mut().set_tag_dictionary(Some(tag_dictionary)); tag_tree.set_hide_root(false); @@ -68,7 +120,15 @@ impl IEditorInspectorPlugin for TagDictionaryEditorInspectorPlugin { tag_tree.set_v_size_flags(SizeFlags::EXPAND_FILL); // done this because of this https://github.com/godot-rust/gdext/issues/156 - self.to_gd().add_custom_control(tag_tree.to_variant().to()); + let mut vbox_container = VBoxContainer::new_alloc(); + + vbox_container.add_child(tag_tree.to_variant().to()); + vbox_container.add_spacer(false); + vbox_container.add_child(footer_container.to_variant().to()); + vbox_container.set_anchors_and_offsets_preset(LayoutPreset::FULL_RECT); + + self.to_gd() + .add_custom_control(vbox_container.to_variant().to()); true } diff --git a/src/editor/mod.rs b/src/editor/mod.rs index 7e77048..d2bf7c8 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -1,4 +1,5 @@ mod docks; +mod editor_import_plugins; mod inspector_plugins; mod tag_dictionary_fs; pub mod ui; @@ -15,6 +16,7 @@ use self::inspector_plugins::tag_dictionary_editor_inspector_plugin; pub struct GameplayTagsEditorPlugin { base: Base, node_tagging_dock: Option>, + import_plugin_csv: Gd, tag_dictionary_editor_inspector_plugin: Gd, } @@ -23,8 +25,14 @@ pub struct GameplayTagsEditorPlugin { impl IEditorPlugin for GameplayTagsEditorPlugin { fn enter_tree(&mut self) { self.node_tagging_dock = Some(docks::node_tagging_dock::NodeTaggingDock::new_alloc()); + self.import_plugin_csv = + editor_import_plugins::csv_import_plugin::CsvImportPlugin::new_gd(); - self.to_gd().add_control_to_dock(DockSlot::RIGHT_UL, self.node_tagging_dock.to_variant().to()); + self.to_gd() + .add_control_to_dock(DockSlot::RIGHT_UL, self.node_tagging_dock.to_variant().to()); + + self.to_gd() + .add_import_plugin(self.import_plugin_csv.clone().upcast()); self.tag_dictionary_editor_inspector_plugin = tag_dictionary_editor_inspector_plugin::TagDictionaryEditorInspectorPlugin::new_gd(); @@ -34,7 +42,6 @@ impl IEditorPlugin for GameplayTagsEditorPlugin { .to_variant() .to(), ); - } fn exit_tree(&mut self) { @@ -44,7 +51,11 @@ impl IEditorPlugin for GameplayTagsEditorPlugin { .to(), ); - self.to_gd().remove_control_from_docks(self.node_tagging_dock.to_variant().to()); + self.to_gd() + .remove_import_plugin(self.import_plugin_csv.to_variant().to()); + + self.to_gd() + .remove_control_from_docks(self.node_tagging_dock.to_variant().to()); } fn get_plugin_name(&self) -> GString { diff --git a/src/editor/tag_dictionary_fs.rs b/src/editor/tag_dictionary_fs.rs index 1d174d6..3711563 100644 --- a/src/editor/tag_dictionary_fs.rs +++ b/src/editor/tag_dictionary_fs.rs @@ -18,27 +18,27 @@ impl TagDictionaryFs { let mut output = PackedStringArray::new(); if let Some(mut dir_access) = DirAccess::open(dir.into_godot()) { - dir_access.list_dir_begin(); + dir_access.list_dir_begin(); - let mut filename = dir_access.get_next(); + let mut filename = dir_access.get_next(); - while !filename.is_empty() { - if dir_access.current_is_dir() { - if !filename.to_string().starts_with(".") { - let sub_dir = format!("{}/{}", dir, filename); - let sub_dir_files = self._read_dir_recursive(&sub_dir); + while !filename.is_empty() { + if dir_access.current_is_dir() { + if !filename.to_string().starts_with(".") { + let sub_dir = format!("{}/{}", dir, filename); + let sub_dir_files = self._read_dir_recursive(&sub_dir); - for file in sub_dir_files.as_slice() { - output.push(file.to_godot().clone()); - } - } - } else { - output.push(GString::from(format!("{}/{}", dir, filename))); - } + for file in sub_dir_files.as_slice() { + output.push(file.to_godot().clone()); + } + } + } else { + output.push(GString::from(format!("{}/{}", dir, filename))); + } - filename = dir_access.get_next(); - } - } + filename = dir_access.get_next(); + } + } output } @@ -55,7 +55,9 @@ impl TagDictionaryFs { let mut loader = ResourceLoader::singleton(); for resource_path in resources.as_slice() { - if resource_path.to_string().ends_with(".res") || resource_path.to_string().ends_with(".tres") { + if resource_path.to_string().ends_with(".res") + || resource_path.to_string().ends_with(".tres") + { if let Some(resource) = loader.load(resource_path.clone()) { if let Ok(dictionary) = resource.try_cast::() { self.dictionaries.push(dictionary); diff --git a/src/importers/csv_importer.rs b/src/importers/csv_importer.rs deleted file mode 100644 index 30a9bd5..0000000 --- a/src/importers/csv_importer.rs +++ /dev/null @@ -1,38 +0,0 @@ -use godot::{engine::file_access::ModeFlags, prelude::*}; - -use crate::tag_dictionary::TagDictionary; - -#[derive(GodotClass)] -#[class(tool, init, base = RefCounted)] -struct CsvImporter { - base: Base, - content: PackedStringArray, -} - -#[godot_api] -impl CsvImporter { - #[func] - pub fn from_csv(&mut self, path: GString) { - self.content = GFile::open(path, ModeFlags::READ) - .expect("Failed to open file") - .read_csv_line(",") - .expect("Failed to read file"); - } - - #[func] - pub fn to_csv(&self, path: GString) { - GFile::open(path, ModeFlags::WRITE) - .expect("Failed to open file") - .write_csv_line(self.content.clone(), ",") - .expect("Failed to write file"); - } - - #[func] - pub fn to_tag_dictionary(&self) -> Gd { - let mut tag_dictionary = TagDictionary::new_gd(); - - tag_dictionary.bind_mut().add_tags(self.content.clone()); - - tag_dictionary - } -} diff --git a/src/importers/mod.rs b/src/importers/mod.rs deleted file mode 100644 index 827aa52..0000000 --- a/src/importers/mod.rs +++ /dev/null @@ -1 +0,0 @@ -mod csv_importer; diff --git a/src/lib.rs b/src/lib.rs index bc28efa..d020d40 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,6 @@ mod editor; -mod tag_manager; mod tag_dictionary; -mod importers; +mod tag_manager; use godot::{engine::Engine, prelude::*}; @@ -9,28 +8,29 @@ struct GodotGameplayTags; #[gdextension] unsafe impl ExtensionLibrary for GodotGameplayTags { - fn on_level_init(init_level: InitLevel) { - if init_level == InitLevel::Scene { - Engine::singleton().register_singleton( - StringName::from(tag_manager::SINGLETON_NAME), - tag_manager::TagManager::new_alloc().upcast() - ); - } - } + fn on_level_init(init_level: InitLevel) { + if init_level == InitLevel::Scene { + Engine::singleton().register_singleton( + StringName::from(tag_manager::SINGLETON_NAME), + tag_manager::TagManager::new_alloc().upcast(), + ); + } + } + + fn on_level_deinit(init_level: InitLevel) { + if init_level == InitLevel::Scene { + let mut engine = Engine::singleton(); - fn on_level_deinit(init_level: InitLevel) { - if init_level == InitLevel::Scene { - let mut engine = Engine::singleton(); + // unregistering singletons + let tag_manager_instance = + engine.get_singleton(StringName::from(tag_manager::SINGLETON_NAME)); - // unregistering singletons - let tag_manager_instance = engine.get_singleton(StringName::from(tag_manager::SINGLETON_NAME)); - - engine.unregister_singleton(StringName::from(tag_manager::SINGLETON_NAME)); + engine.unregister_singleton(StringName::from(tag_manager::SINGLETON_NAME)); - // freeing memory - tag_manager_instance - .expect("Failed to get TagManager singleton") - .free(); - } - } + // freeing memory + tag_manager_instance + .expect("Failed to get TagManager singleton") + .free(); + } + } } diff --git a/src/tag_dictionary.rs b/src/tag_dictionary.rs index fa0ddc8..90b3b7c 100644 --- a/src/tag_dictionary.rs +++ b/src/tag_dictionary.rs @@ -13,15 +13,18 @@ pub struct TagDictionary { #[godot_api] impl TagDictionary { #[func] - pub fn add_tag(&mut self, tag: GString) { + pub fn add_tag(&mut self, tag: GString) -> bool { if !self.tags.contains(&tag) { self.tags.push(tag); self.base_mut().emit_changed(); + return true; } + + false } #[func] - pub fn add_tags(&mut self, tags: PackedStringArray) { + pub fn add_tags(&mut self, tags: PackedStringArray) -> u64 { let mut count = 0; for tag in tags.as_slice() { @@ -34,6 +37,8 @@ impl TagDictionary { if count > 0 { self.base_mut().emit_changed(); } + + count } #[func] @@ -49,7 +54,7 @@ impl TagDictionary { let mut args = VariantArray::new(); args.push(tag.to_variant()); - + if predicate.callv(args).booleanize() { result.push(tag.clone()); } @@ -116,34 +121,27 @@ impl TagDictionary { #[func] pub fn has_some_tags(&self, tags: PackedStringArray) -> bool { - tags.as_slice() - .iter() - .any(|tag| self.tags.contains(&tag)) + tags.as_slice().iter().any(|tag| self.tags.contains(&tag)) } #[func] pub fn has_none_tags(&self, tags: PackedStringArray) -> bool { - tags.as_slice() - .iter() - .all(|tag| !self.tags.contains(&tag)) + tags.as_slice().iter().all(|tag| !self.tags.contains(&tag)) } #[func] pub fn none(&self, predicate: Callable) -> bool { - self.tags - .as_slice() - .iter() - .all(|tag| { - let mut args = VariantArray::new(); + self.tags.as_slice().iter().all(|tag| { + let mut args = VariantArray::new(); - args.push(tag.to_variant()); + args.push(tag.to_variant()); - !predicate.callv(args).booleanize() - }) + !predicate.callv(args).booleanize() + }) } #[func] - pub fn replace_tag(&mut self, old_tag: String, new_tag: String) { + pub fn replace_tag(&mut self, old_tag: String, new_tag: String) -> bool { if let Some(index) = self .tags .as_slice() @@ -152,11 +150,19 @@ impl TagDictionary { { self.tags[index] = new_tag.into(); self.base_mut().emit_changed(); + + return true; } + + false } #[func] - pub fn replace_tags(&mut self, old_tags: PackedStringArray, new_tags: PackedStringArray) { + pub fn replace_tags( + &mut self, + old_tags: PackedStringArray, + new_tags: PackedStringArray, + ) -> u64 { let mut count = 0; for old_tag in old_tags.as_slice() { @@ -171,18 +177,24 @@ impl TagDictionary { if count > 0 { self.base_mut().emit_changed(); } + + count } #[func] - pub fn remove_tag(&mut self, tag: GString) { + pub fn remove_tag(&mut self, tag: GString) -> bool { if let Some(index) = self.tags.as_slice().iter().position(|t| tag.eq(t)) { self.tags.remove(index); self.base_mut().emit_changed(); + + return true; } + + false } #[func] - pub fn remove_path(&mut self, path: String) { + pub fn remove_path(&mut self, path: String) -> u64 { let mut count = 0; for tag in self.tags.clone().as_slice() { @@ -195,24 +207,23 @@ impl TagDictionary { if count > 0 { self.base_mut().emit_changed(); } + + count } #[func] pub fn some(&self, predicate: Callable) -> bool { - self.tags - .as_slice() - .iter() - .any(|tag| { - let mut args = VariantArray::new(); + self.tags.as_slice().iter().any(|tag| { + let mut args = VariantArray::new(); - args.push(tag.to_variant()); + args.push(tag.to_variant()); - predicate.callv(args).booleanize() - }) + predicate.callv(args).booleanize() + }) } #[func] - pub fn update_path(&mut self, old_path: String, new_path: String) { + pub fn update_path(&mut self, old_path: String, new_path: String) -> u64 { let mut count = 0; for tag in self.tags.clone().as_slice() { @@ -226,5 +237,7 @@ impl TagDictionary { if count > 0 { self.base_mut().emit_changed(); } + + count } }