From d3d2f590850773ecf08551692c8d3f3dff4ff15e Mon Sep 17 00:00:00 2001 From: Will Thompson Date: Wed, 15 Oct 2025 12:59:35 +0100 Subject: [PATCH 1/2] Prefix quest files (not dirs) with quest name Previously, bootstrapping a quest named "dennis" would copy scenes/quests/story_quests/NO_EDIT/1_NO_EDIT_stealth/NO_EDIT_stealth_components/NO_EDIT_stealth.dialogue to scenes/quests/story_quests/dennis/1_stealth/stealth_components/stealth.dialogue. This doesn't match what we have been teaching students to do by hand, which is to name scenes/quests/story_quests/dennis/1_dennis_stealth/dennis_stealth_components/dennis_stealth.dialogue. Compromise on including the quest name in the filename, to make it always visible when the filename is shown, but not in the intermediate folders: scenes/quests/story_quests/dennis/1_stealth/stealth_components/dennis_stealth.dialogue. This does have the disadvantage that if a learner later chooses to rename their quest, they now have to rename every file rather than just its root folder. --- addons/storyquest_bootstrap/copier.gd | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/addons/storyquest_bootstrap/copier.gd b/addons/storyquest_bootstrap/copier.gd index c497981b67..1ca03722a2 100644 --- a/addons/storyquest_bootstrap/copier.gd +++ b/addons/storyquest_bootstrap/copier.gd @@ -11,7 +11,11 @@ const QUEST_FILENAME := "quest.tres" ## Map from UID to already-copied resource var orig_uid_to_copy: Dictionary[String, Resource] -## Root directory of instantiated SQ +## Name of root directory of quest, e.g. [code]"stella"[/code] +var quest_name: String + +## Root directory of copied StoryQuest, +## i.e. [code]STORYQUESTS_PATH.path_join(quest_name)[/code] var target_path: String var _title: String @@ -22,6 +26,7 @@ var _depth: int = 0 func _init(filename: String, title: String, description: String) -> void: + quest_name = filename target_path = STORYQUESTS_PATH.path_join(filename) self._title = title self._description = description @@ -160,8 +165,12 @@ func copy(uid: String, resource: Resource) -> Resource: return orig_uid_to_copy[uid] var subpath := resource.resource_path.right(-TEMPLATE_PATH.length()) - subpath = subpath.replace(TEMPLATE_PREFIX + "_", "") - var copy_path := target_path.path_join(subpath) + var subdir := subpath.get_base_dir() + var filename := subpath.get_file() + + subdir = subdir.replace(TEMPLATE_PREFIX + "_", "") + filename = filename.replace(TEMPLATE_PREFIX, quest_name) + var copy_path := target_path.path_join(subdir).path_join(filename) _debug(["Copying", uid, resource.resource_path, "to", copy_path]) _depth += 1 From 309673751d03dd28d29906f3dfef49bfdf4fd741 Mon Sep 17 00:00:00 2001 From: Will Thompson Date: Wed, 15 Oct 2025 17:42:33 +0100 Subject: [PATCH 2/2] storyquest_bootstrap: Limit filename length & allow editing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently, if someone calls their StoryQuest “Hurrah! Another Year, Surely This One Will Be Better Than the Last; The Inexorable March of Progress Will Lead Us All to Happiness” the resulting quest is stored in a folder called (line-wrapped for legibility): scenes/quests/story_quests/hurrah_another_year_surely_this_one_will_be_better_ than_the_last_the_inexorable_march_of_progress_will_lead_us_all_to_happiness Introduce a new field to the dialog which allows editing the folder name. Pre-populate it with the generated name whenever the title is changed. Set a maximum length on the field and truncate the generated name to that length, too. Add some new validation for problems that would previously be impossible. (This splits the validation function awkwardly in two; we can refactor this later, sorry.) This has the added bonus that if someone tries to name a quest “Sueños de Helado”, the generated folder will still be `sue_os_de_helado`, but the creator can manually edit it to `suenios_de_helado`. --- .../new_storyquest_dialog.gd | 49 +++++++++++++------ .../new_storyquest_dialog.tscn | 16 +++++- 2 files changed, 50 insertions(+), 15 deletions(-) diff --git a/addons/storyquest_bootstrap/new_storyquest_dialog.gd b/addons/storyquest_bootstrap/new_storyquest_dialog.gd index de1db63a8b..b3ba9b5bc0 100644 --- a/addons/storyquest_bootstrap/new_storyquest_dialog.gd +++ b/addons/storyquest_bootstrap/new_storyquest_dialog.gd @@ -13,12 +13,14 @@ signal cancel var _title: String var _description: String var _filename: String +var _invalid_char_regex: RegEx @onready var create_button: Button = %CreateButton @onready var panel: Panel = %Panel @onready var title_edit: LineEdit = %TitleEdit +@onready var folder_edit: LineEdit = %FolderEdit +@onready var full_path_label: Label = %FullPathLabel @onready var errors_label: RichTextLabel = %ErrorsLabel -@onready var folder_res_label: Label = %FolderResLabel @onready var description_edit: TextEdit = %DescriptionEdit @onready var progress_bar: ProgressBar = %ProgressBar @@ -32,6 +34,10 @@ func _ready() -> void: "default_color", get_theme_color("warning_color", "Editor") ) + _invalid_char_regex = RegEx.new() + var error := _invalid_char_regex.compile("\\W+", true) + assert(error == OK, error_string(error)) + func _on_create_button_pressed() -> void: title_edit.editable = false @@ -56,30 +62,45 @@ func _make_filename(title: String) -> String: # # TODO: Replace e.g. ö with oe, ß with ss, ı with i, ñ with n, etc. # TODO: Transliterate non-Latin scripts - var regex := RegEx.new() - var error := regex.compile("\\W+", true) - assert(error == OK, error_string(error)) - var subbed := regex.sub(snaked, "_", true) + var subbed := _invalid_char_regex.sub(snaked, "_", true) - # Now remove leading or trailing underscores. This may + # Now remove leading or trailing underscores. This may mean the resulting + # filename is empty. var stripped := subbed.lstrip("_").rstrip("_") - return stripped + # Limit the generated filename to a reasonable length. + return stripped.left(folder_edit.max_length) func _on_title_edit_text_changed(new_text: String) -> void: _title = new_text - _filename = _make_filename(_title) + folder_edit.text = _make_filename(_title) + _on_folder_edit_text_changed(folder_edit.text) + + +func _on_folder_edit_text_changed(new_text: String) -> void: + _filename = new_text + full_path_label.text = storyquests_path.path_join(_filename) + _revalidate() + + +func _on_description_edit_text_changed() -> void: + _description = description_edit.text + +func _revalidate() -> void: var errors: PackedStringArray + errors.append_array(validate_title.call(_title)) + + var matches := _invalid_char_regex.search_all(_filename) + if matches: + for match: RegExMatch in matches: + errors.append("⚠ ‘%s’ is not allowed in the folder name" % match.get_string(0)) + if _filename.to_lower() != _filename: + errors.append("⚠ Folder name must be lower-case") if _title: errors.append_array(validate_filename.call(_filename)) - create_button.disabled = errors.size() - folder_res_label.text = storyquests_path.path_join(_filename) errors_label.text = "\n".join(errors) - - -func _on_description_edit_text_changed() -> void: - _description = description_edit.text + create_button.disabled = errors.size() > 0 diff --git a/addons/storyquest_bootstrap/new_storyquest_dialog.tscn b/addons/storyquest_bootstrap/new_storyquest_dialog.tscn index 59b1a88886..3c8337e511 100644 --- a/addons/storyquest_bootstrap/new_storyquest_dialog.tscn +++ b/addons/storyquest_bootstrap/new_storyquest_dialog.tscn @@ -6,6 +6,7 @@ bg_color = Color(0, 0, 0, 1) [node name="StoryQuestDialogue" type="Window"] +oversampling_override = 1.0 title = "Create StoryQuest" initial_position = 1 size = Vector2i(600, 400) @@ -55,7 +56,19 @@ placeholder_text = "My Quest" layout_mode = 2 text = "Folder Name:" -[node name="FolderResLabel" type="Label" parent="PanelContainer/VBoxContainer/GridContainer"] +[node name="FolderEdit" type="LineEdit" parent="PanelContainer/VBoxContainer/GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +placeholder_text = "My Quest" +max_length = 20 +emoji_menu_enabled = false + +[node name="FullPathLabelLabel" type="Label" parent="PanelContainer/VBoxContainer/GridContainer"] +layout_mode = 2 +text = "Full Path:" + +[node name="FullPathLabel" type="Label" parent="PanelContainer/VBoxContainer/GridContainer"] unique_name_in_owner = true layout_mode = 2 size_flags_horizontal = 3 @@ -99,5 +112,6 @@ text = "Create" [connection signal="close_requested" from="." to="." method="_on_close_requested"] [connection signal="text_changed" from="PanelContainer/VBoxContainer/GridContainer/TitleEdit" to="." method="_on_title_edit_text_changed"] +[connection signal="text_changed" from="PanelContainer/VBoxContainer/GridContainer/FolderEdit" to="." method="_on_folder_edit_text_changed"] [connection signal="text_changed" from="PanelContainer/VBoxContainer/DescriptionEdit" to="." method="_on_description_edit_text_changed"] [connection signal="pressed" from="PanelContainer/VBoxContainer/MarginContainer/CreateButton" to="." method="_on_create_button_pressed"]