From 679f0ecad4825b9c35752cf373e6f27d32b91845 Mon Sep 17 00:00:00 2001 From: Amethyst-szs Date: Sun, 26 Nov 2023 12:06:26 -0800 Subject: [PATCH 01/30] Improved in-line docs --- addons/savedata-dx/backend/save_accessor.gd | 98 ++++++++++++++------- addons/savedata-dx/backend/save_holder.gd | 25 ++++-- addons/savedata-dx/view/save_view.gd | 6 +- 3 files changed, 85 insertions(+), 44 deletions(-) diff --git a/addons/savedata-dx/backend/save_accessor.gd b/addons/savedata-dx/backend/save_accessor.gd index e240de2..29cf817 100644 --- a/addons/savedata-dx/backend/save_accessor.gd +++ b/addons/savedata-dx/backend/save_accessor.gd @@ -1,63 +1,82 @@ extends Node -# SaveDataDX plugin by Amethyst-szs -# - SaveAccessor - -# -# This autoload singleton is used to manage reading/writing save data. -# The main functions include reading, writing, and verifying a specific save slot -# as well as reading, writing, and verifying the common save data shared over all slots. +## SaveDataDX plugin by Amethyst-szs - +## Used to manage reading/writing save data. +## This class is meant to be used an autoload singleton, +## access it via "SaveAccessor" in your scritps. +class_name SaveAccessorPlugin # Include datatype parser for converting Object into JSON const datatype_dict_parser = preload("res://addons/savedata-dx/backend/datatype_parser.gd") var dict_parse = datatype_dict_parser.new() # Constants defining where the saves are located and how they are named/stored + +## Directory to store save files const SAVE_DIR: String = "user://sv/" +## Name of the common file const SAVE_COMMON_NAME: String = "common" +## File extension used by save files const SAVE_EXTENSION_NAME: String = ".bin" +## Encryption key, can be changed but will break all existing saves if changed const KEY: String = "no@NqlqGu8PTG#weQ77t$%bBQ9$HG5itZ#8#Xnbd%&L$y5Sd" # Signal messages + +## Emitted when saving to a slot is completed successfully signal save_slot_complete +## Emitted when saving the common data is completed successfully signal save_common_complete +## Emitted when loading a slot is completed successfully signal load_slot_complete +## Emitted when loading the common data is completed successfully signal load_common_complete +## Emitted when a save call fails signal save_error +## Emitted when a load call fails signal load_error # Check/modify a save slot determined by the variable "active_save_slot" -@export var active_save_slot: int = 0 +## Current save slot, useful to manage which file is getting read/written to +var active_save_slot: int = 1 + +## Sets the "active_save_slot" variable func set_active_slot(index: int) -> void: active_save_slot = index - + +## Checks if a file exists for the active save slot, does not ensure it is valid func is_active_slot_exist() -> bool: return is_slot_exist(active_save_slot) - + +## Writes to the active save slot, and emits "save_slot_complete" when successful func write_active_slot() -> void: write_slot(active_save_slot) - + +## Loads data from the active save slot, and emits "load_slot_complete" when successful func read_active_slot() -> void: read_slot(active_save_slot) # Check/modify a specific save slot, specified by index +## Checks if a file exists for a specific save slot index, does not ensure it is valid func is_slot_exist(index: int) -> bool: var path = SAVE_DIR + "s" + str(index) + SAVE_EXTENSION_NAME return FileAccess.file_exists(path) +## Writes to a specific save slot index, and emits "save_slot_complete" when successful func write_slot(index: int) -> void: - if write_backend("s%s" % [str(index)], SaveHolder.slot): + if _write_backend("s%s" % [str(index)], SaveHolder.slot): # Tell the signal that the save is finished successfully save_slot_complete.emit() else: save_error.emit() - +## Loads data a specific save slot index, and emits "load_slot_complete" when successful func read_slot(index: int) -> void: # Get dictionary from file in save directory - var dict: Dictionary = read_backend_by_name("s%s" % [str(index)]) + var dict: Dictionary = _read_backend_by_name("s%s" % [str(index)]) if dict.is_empty(): load_error.emit() return @@ -74,22 +93,24 @@ func read_slot(index: int) -> void: # Check/modify the common save data shared between all slots -# Note that "read_common" is automatically called on startup if it already exists +## Checks if the common save exists in save directory func is_common_exist() -> bool: var path = SAVE_DIR + SAVE_COMMON_NAME + SAVE_EXTENSION_NAME return FileAccess.file_exists(path) +## Writes the common data to disk, and emits "save_common_complete" when successful func write_common() -> void: - if write_backend(SAVE_COMMON_NAME, SaveHolder.common): + if _write_backend(SAVE_COMMON_NAME, SaveHolder.common): # Tell the signal that the save is finished successfully save_common_complete.emit() else: save_error.emit() +## Reads the common data from the disk, and emits "load_common_complete" when successful func read_common() -> void: # Get dictionary from file in save directory - var dict: Dictionary = read_backend_by_name(SAVE_COMMON_NAME) + var dict: Dictionary = _read_backend_by_name(SAVE_COMMON_NAME) if dict.is_empty(): load_error.emit() return @@ -106,7 +127,9 @@ func read_common() -> void: # Backend functions handling reading and writing of data -func write_backend(name: String, object) -> bool: +## Not intended for the end user. +## Write object to disk with file name, called by write_slot and write_common +func _write_backend(name: String, object) -> bool: # Ensure the directory 100% exists to avoid issues DirAccess.make_dir_absolute(SAVE_DIR) @@ -119,7 +142,7 @@ func write_backend(name: String, object) -> bool: return false # Create a dictionary out of the object using the parser - var data: Dictionary = object_to_dict(object) + var data: Dictionary = _object_to_dict(object) # Write this JSON data to disk var json_string = JSON.stringify(data, "\t") @@ -128,7 +151,9 @@ func write_backend(name: String, object) -> bool: return true -func write_backend_with_json_string(path: String, json_string: String) -> bool: +## Not intended for the end user. +## Write stringified JSON to disk at path +func _write_backend_with_json_string(path: String, json_string: String) -> bool: # Ensure the directory 100% exists to avoid issues DirAccess.make_dir_absolute(SAVE_DIR) @@ -144,12 +169,16 @@ func write_backend_with_json_string(path: String, json_string: String) -> bool: return true -func read_backend_by_name(name: String) -> Dictionary: - return read_backend(SAVE_DIR + name + SAVE_EXTENSION_NAME) +## Not intended for the end user. +## Read save file by file name and return dictionary +func _read_backend_by_name(name: String) -> Dictionary: + return _read_backend(SAVE_DIR + name + SAVE_EXTENSION_NAME) -func read_backend(path: String) -> Dictionary: +## Not intended for the end user. +## Read save file by path and return dictionary +func _read_backend(path: String) -> Dictionary: # Get the content of the path - var content = read_backend_raw_data(path) + var content = _read_backend_raw_data(path) if content == null or content.is_empty(): return {} @@ -166,7 +195,9 @@ func read_backend(path: String) -> Dictionary: # Return the JSON data to then be converted into a object later return data -func read_backend_raw_data(path: String) -> String: +## Not intended for the end user. +## Read save file by path and return raw string data +func _read_backend_raw_data(path: String) -> String: # Verify the file exists and return early if not if not FileAccess.file_exists(path): printerr("Cannot open non-existent file at %s" % [path]) @@ -189,9 +220,9 @@ func read_backend_raw_data(path: String) -> String: # Return the JSON data to then be converted into a object later return content -# Recursive conversion from object to JSON dictionary - -func object_to_dict(obj: Object) -> Dictionary: +## Not intended for the end user. +## Converts object class into dictionary for saving process +func _object_to_dict(obj: Object) -> Dictionary: # Create empty dictionary and get all properties from object var members = obj.get_property_list() var member_index: int = 0 @@ -206,9 +237,9 @@ func object_to_dict(obj: Object) -> Dictionary: # Write the member to dictionary depending on type of member match(typeof(obj.get(member.name))): TYPE_OBJECT: # Call self and create sub-dictionary for object - dict[member.name] = object_to_dict(obj.get(member.name)) + dict[member.name] = _object_to_dict(obj.get(member.name)) TYPE_ARRAY: # Expand the array with function and add to dictionary - dict[member.name] = expand_array_for_dict(obj.get(member.name)) + dict[member.name] = _expand_array_for_dict(obj.get(member.name)) TYPE_VECTOR2, TYPE_VECTOR2I: dict[member.name] = dict_parse.parse_vector2(obj.get(member.name)) TYPE_VECTOR3, TYPE_VECTOR3I: @@ -222,18 +253,19 @@ func object_to_dict(obj: Object) -> Dictionary: return dict -func expand_array_for_dict(list: Array) -> Array: +## Not intended for the end user. +## Converts inside of array into dictionaries, allowing arrays of objects to be saved correctly +func _expand_array_for_dict(list: Array) -> Array: # Create empty array var new_list: Array = [] # Iterate through all items for item in list: - # Write the member to dictionary depending on type of member match(typeof(item)): TYPE_OBJECT: # Call object to dict converter for item - new_list.push_back(object_to_dict(item)) + new_list.push_back(_object_to_dict(item)) TYPE_ARRAY: # Create sub array - new_list.push_back(expand_array_for_dict(item)) + new_list.push_back(_expand_array_for_dict(item)) TYPE_VECTOR2, TYPE_VECTOR2I: new_list.push_back(dict_parse.parse_vector2(item)) TYPE_VECTOR3, TYPE_VECTOR3I: diff --git a/addons/savedata-dx/backend/save_holder.gd b/addons/savedata-dx/backend/save_holder.gd index 2bf743b..377d1df 100644 --- a/addons/savedata-dx/backend/save_holder.gd +++ b/addons/savedata-dx/backend/save_holder.gd @@ -1,17 +1,23 @@ extends Node -# SaveDataDX plugin by Amethyst-szs -# - SaveHolder - -# -# An autoload singleton storing and allowing access to your current save game -# and the common data shared across all save files. The common data is automatically -# read on startup if it already exists +## SaveDataDX plugin by Amethyst-szs - +## Stores and allows access to your current save data +## This class is meant to be used an autoload singleton, +## access it via "SaveHolder" in your scritps. +class_name SaveHolderPlugin +# Load in scripts + +## Preload script for save slot const slot_script = preload("res://addons/savedata-dx/data_slot.gd") +## Preload script for common data const common_script = preload("res://addons/savedata-dx/data_common.gd") -# Create an empty save data slot and common file +# Create a new slot and common file using their default values + +## Access and modify your save slot var slot: slot_script = slot_script.new() +## Access and modify your common data var common: common_script = common_script.new() func _ready(): @@ -21,13 +27,16 @@ func _ready(): else: SaveAccessor.write_common() -# Reset save data to default +## Resets the data in the slot variable to default func reset_slot(): slot = slot_script.new() +## Resets the data in the common variable to default func reset_common(): common = common_script.new() +## Resets the data in the slot and common variable to default. +## Not recommended unless you're including a full "factory reset" style feature func reset_all(): reset_common() reset_slot() diff --git a/addons/savedata-dx/view/save_view.gd b/addons/savedata-dx/view/save_view.gd index d8bc1d7..726ce82 100644 --- a/addons/savedata-dx/view/save_view.gd +++ b/addons/savedata-dx/view/save_view.gd @@ -1,7 +1,7 @@ @tool extends Control -# SaveData accessor script, this is added here so the accessor doesn't have to include @tool +# SaveData accessor script const accessor_script = preload("res://addons/savedata-dx/backend/save_accessor.gd") var accessor_inst = accessor_script.new() @@ -155,7 +155,7 @@ func _on_head_save_pressed(): inspector_save_fail_dialog.popup() return - accessor_inst.write_backend_with_json_string(open_file_path, code_editor.text) + accessor_inst._write_backend_with_json_string(open_file_path, code_editor.text) else: # If this isn't inspector mode, write script to disk normally code_editor_save_script() @@ -194,7 +194,7 @@ func _on_slot_import_file_dialog(path: String) -> void: # Convert file path to dictionary func decrypt_save(path: String) -> String: - return accessor_inst.read_backend_raw_data(path) + return accessor_inst._read_backend_raw_data(path) # Called upon selecting a file in the debugger mode func _on_inspector_select_file(path: String) -> void: From f5c17fdf276c6e5aa2e79f3b8c7bbae6add3fd7b Mon Sep 17 00:00:00 2001 From: Amethyst-szs Date: Sun, 26 Nov 2023 13:28:19 -0800 Subject: [PATCH 02/30] Documentation link update --- addons/savedata-dx/backend/save_accessor.gd | 14 ++++++++++++-- addons/savedata-dx/view/save_view.gd | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/addons/savedata-dx/backend/save_accessor.gd b/addons/savedata-dx/backend/save_accessor.gd index 29cf817..976afcc 100644 --- a/addons/savedata-dx/backend/save_accessor.gd +++ b/addons/savedata-dx/backend/save_accessor.gd @@ -32,6 +32,9 @@ signal load_slot_complete ## Emitted when loading the common data is completed successfully signal load_common_complete +## Emitted when the active_save_slot is updated to a new, different value +signal active_slot_changed + ## Emitted when a save call fails signal save_error ## Emitted when a load call fails @@ -40,9 +43,16 @@ signal load_error # Check/modify a save slot determined by the variable "active_save_slot" ## Current save slot, useful to manage which file is getting read/written to -var active_save_slot: int = 1 +var active_save_slot: int = 1: + set (value): + if not active_save_slot == value: + active_slot_changed.emit() + + active_save_slot = value + get: + return active_save_slot -## Sets the "active_save_slot" variable +## Sets the "active_save_slot", and emits "active_slot_changed" if the new slot is different func set_active_slot(index: int) -> void: active_save_slot = index diff --git a/addons/savedata-dx/view/save_view.gd b/addons/savedata-dx/view/save_view.gd index 726ce82..43fd25c 100644 --- a/addons/savedata-dx/view/save_view.gd +++ b/addons/savedata-dx/view/save_view.gd @@ -165,7 +165,7 @@ func _on_head_add_pressed(): EditModeType.COMMON: common_import_file_dialog.popup() func _on_head_info_pressed(): - OS.shell_open("https://github.com/Amethyst-szs/godot-savedata-dx") + OS.shell_open("https://github.com/Amethyst-szs/godot-savedata-dx/wiki") func _on_head_close_pressed(): _on_edit_mode_selected(edit_mode) From eb34871f123537c61a1c7a349262eb74004cab08 Mon Sep 17 00:00:00 2001 From: Amethyst-szs Date: Sun, 26 Nov 2023 13:29:53 -0800 Subject: [PATCH 03/30] Update README.md --- README.md | 46 +++++++++++++++++++--------------------------- 1 file changed, 19 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index c4cae4b..edd34be 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,5 @@ # Godot SaveData DX Plugin *A plugin for Godot adding a simple, convenient, and secure save data system* - -![Banner](https://github.com/Amethyst-szs/godot-savedata-dx/assets/62185604/096e49b6-a26b-4283-8ac6-555dfb7ffb5b) ## Explanation This plugin was created to avoid the issues with the usual JSON or Resource saving methods used by Godot developers. Working with JSON can prove clunky and waste a lot of development time on bugs and @@ -10,33 +8,27 @@ annoying copy-paste work, and using Resources has a major security vulnerability This plugin attempts to be the best of both worlds, combining the convience of the resource method with the safety and security of the JSON method. On top of that, it provides some additonal tools to make managing your game's save files easier. -## Usage -### What is Common vs. Slot -There are two types of save file, common and slot. - -**Common** is automatically read when launching the program and should be shared between all save files. This is useful for things like settings, controller config, and other global changes. - -**Slot** is the info specific to this save slot, things related to game progression would go here. This lets the player have seperate playthroughs of the game. There is no limit to the max amount of slots you can create. - - -### Writing/Reading/Accessing save data -The plugin has two autoloaded singletons that can be accessed from your scripts, `SaveAccessor` and `SaveHolder`. - -`SaveAccessor` features a bunch of methods for saving, loading, and checking various save data. - -`SaveHolder` contains the active save slot data and the common save file data and can be access from anywhere in your godot project. - - -### Adding properties to save -To add more content to your save files, open `res://addons/savedata-dx/` and navigate to either `common` or `slot`. These contain scripts that you can add properties to. These can be accessed from anywhere -in your code and are automatically saved by the plugin, no extra work required! - -To further organize your additonal properties, you can make new resources as additonal scripts and add them into your save data! Just remember to declare the `START` variable at the beginning of your script -so the plugin is able to find all of properties. - ## Installation - 1. Download a copy of the repo 2. Copy the `addons/savedata-dx` directory into your project's `res://addons/` directory 3. Enable under Project Settings -> Plugins ![image](https://github.com/Amethyst-szs/godot-savedata-dx/assets/62185604/11b57f7d-dcdc-4f93-a595-5612df1bf188) + + +## [Docs](https://github.com/Amethyst-szs/godot-savedata-dx/wiki/) +- [Core Concepts & FAQ](https://github.com/Amethyst-szs/godot-savedata-dx/wiki/Core-concepts-&-FAQ) +- [General usage](https://github.com/Amethyst-szs/godot-savedata-dx/wiki/General-Usage) + - [Installing Plugin](https://github.com/Amethyst-szs/godot-savedata-dx/wiki/General-Usage#installing-plugin) + - [Using SaveData menu](https://github.com/Amethyst-szs/godot-savedata-dx/wiki/General-Usage#using-savedata-menu) + - [Inspector Mode](https://github.com/Amethyst-szs/godot-savedata-dx/wiki/General-Usage#inspector-mode) + - [Slot Mode](https://github.com/Amethyst-szs/godot-savedata-dx/wiki/General-Usage#slot-mode) + - [Common Mode](https://github.com/Amethyst-szs/godot-savedata-dx/wiki/General-Usage#common-mode) +- [SaveAccessor](https://github.com/Amethyst-szs/godot-savedata-dx/wiki/SaveAccessor) + - [Active Save Slot](https://github.com/Amethyst-szs/godot-savedata-dx/wiki/SaveAccessor#active-save-slot) + - [Writing to Disk](https://github.com/Amethyst-szs/godot-savedata-dx/wiki/SaveAccessor#writing-to-disk) + - [Reading from Disk](https://github.com/Amethyst-szs/godot-savedata-dx/wiki/SaveAccessor#reading-from-disk) + - [Checking for saves on Disk](https://github.com/Amethyst-szs/godot-savedata-dx/wiki/SaveAccessor#checking-for-saves-on-disk) + - [Signals](https://github.com/Amethyst-szs/godot-savedata-dx/wiki/SaveAccessor#signals) +- [SaveHolder](https://github.com/Amethyst-szs/godot-savedata-dx/wiki/SaveHolder) + - [Accessing Slot & Common](https://github.com/Amethyst-szs/godot-savedata-dx/wiki/SaveHolder#accessing-slot--common) + - [Reset Functions](https://github.com/Amethyst-szs/godot-savedata-dx/wiki/SaveHolder#reset-functions) From 8d4a5e1391f60c52767aec656c434cec77505296 Mon Sep 17 00:00:00 2001 From: Amethyst-szs Date: Sun, 26 Nov 2023 13:52:17 -0800 Subject: [PATCH 04/30] Update README.md --- README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index edd34be..452f7db 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,18 @@ -# Godot SaveData DX Plugin +# Godot SaveData DX Plugin *A plugin for Godot adding a simple, convenient, and secure save data system* +![icon](https://github.com/Amethyst-szs/godot-savedata-dx/assets/62185604/3bc7d641-0708-4ab9-b24e-47a542322e58) ## Explanation This plugin was created to avoid the issues with the usual JSON or Resource saving methods used by Godot developers. Working with JSON can prove clunky and waste a lot of development time on bugs and annoying copy-paste work, and using Resources has a major security vulnerability allowing arbitrary code execution. - + +![image](https://github.com/Amethyst-szs/godot-savedata-dx/assets/62185604/aff7cde3-61be-471d-842a-462f2b907b58) + This plugin attempts to be the best of both worlds, combining the convience of the resource method with the safety and security of the JSON method. On top of that, it provides some additonal tools to make managing your game's save files easier. +![image](https://github.com/Amethyst-szs/godot-savedata-dx/assets/62185604/cd7918e5-556f-4f30-8089-3dbc3c946b7d) + ## Installation 1. Download a copy of the repo 2. Copy the `addons/savedata-dx` directory into your project's `res://addons/` directory From 8f4f1ea74b8059ef7e70f748b2866c49ab5429e4 Mon Sep 17 00:00:00 2001 From: Amethyst-szs Date: Sun, 26 Nov 2023 14:11:52 -0800 Subject: [PATCH 05/30] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 452f7db..d5fe977 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Godot SaveData DX Plugin *A plugin for Godot adding a simple, convenient, and secure save data system* -![icon](https://github.com/Amethyst-szs/godot-savedata-dx/assets/62185604/3bc7d641-0708-4ab9-b24e-47a542322e58) +![graphic](https://github.com/Amethyst-szs/godot-savedata-dx/assets/62185604/f162738a-72ee-49b7-b96c-5fa0dbad7394) ## Explanation This plugin was created to avoid the issues with the usual JSON or Resource saving methods used by Godot developers. Working with JSON can prove clunky and waste a lot of development time on bugs and From 7b413189610ec082f0123cb7394c12680fa4b28f Mon Sep 17 00:00:00 2001 From: Amethyst-szs Date: Sun, 26 Nov 2023 14:12:50 -0800 Subject: [PATCH 06/30] Small code editor fixes --- addons/savedata-dx/view/save_view.gd | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/addons/savedata-dx/view/save_view.gd b/addons/savedata-dx/view/save_view.gd index 43fd25c..dbc09cd 100644 --- a/addons/savedata-dx/view/save_view.gd +++ b/addons/savedata-dx/view/save_view.gd @@ -246,6 +246,8 @@ func code_editor_close(text: String = "Open a file or change edit mode in the to code_editor.placeholder_text = text head_button_save.disabled = true code_error_footer.visible = false + + code_editor.clear_undo_history() func code_editor_open() -> void: unsaved_changes = false @@ -255,6 +257,8 @@ func code_editor_open() -> void: code_editor.placeholder_text = "" head_button_save.disabled = false code_error_footer.visible = false + + code_editor.clear_undo_history() func code_editor_open_file(path: String) -> void: # Verify the file exists and return early if not @@ -285,6 +289,8 @@ func code_editor_open_file(path: String) -> void: code_editor.text = content code_editor.placeholder_text = "" head_button_save.disabled = false + + code_editor.clear_undo_history() func code_editor_save_script() -> void: # Attempt to open new file and print an error if it fails @@ -319,6 +325,7 @@ func apply_theme() -> void: highlight.add_keyword_color("var", set.get_setting("text_editor/theme/highlighting/keyword_color")) highlight.add_keyword_color("const", set.get_setting("text_editor/theme/highlighting/keyword_color")) + highlight.add_keyword_color("bool", set.get_setting("text_editor/theme/highlighting/base_type_color")) highlight.add_keyword_color("int", set.get_setting("text_editor/theme/highlighting/base_type_color")) highlight.add_keyword_color("float", set.get_setting("text_editor/theme/highlighting/base_type_color")) highlight.add_keyword_color("String", set.get_setting("text_editor/theme/highlighting/base_type_color")) From 56debcc7b8b86b8d21aab679a06e3f731b93b4c7 Mon Sep 17 00:00:00 2001 From: Amethyst-szs Date: Sun, 26 Nov 2023 14:13:27 -0800 Subject: [PATCH 07/30] Update plugin version --- addons/savedata-dx/plugin.cfg | 2 +- addons/savedata-dx/view/highlighter.tres | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/addons/savedata-dx/plugin.cfg b/addons/savedata-dx/plugin.cfg index 0c80e31..c62d518 100644 --- a/addons/savedata-dx/plugin.cfg +++ b/addons/savedata-dx/plugin.cfg @@ -3,5 +3,5 @@ name="SaveDataDX" description="A plugin combining the benefits of JSON & Resource saving, making save data easy, fast, and secure" author="Amethyst-szs" -version="0.6.0" +version="1.0.0" script="savedata-dx.gd" diff --git a/addons/savedata-dx/view/highlighter.tres b/addons/savedata-dx/view/highlighter.tres index 21a3508..933acf1 100644 --- a/addons/savedata-dx/view/highlighter.tres +++ b/addons/savedata-dx/view/highlighter.tres @@ -16,6 +16,7 @@ keyword_colors = { "Vector3i": Color(0.258824, 1, 0.760784, 1), "Vector4": Color(0.258824, 1, 0.760784, 1), "Vector4i": Color(0.258824, 1, 0.760784, 1), +"bool": Color(0.258824, 1, 0.760784, 1), "const": Color(1, 0.44, 0.52, 1), "float": Color(0.258824, 1, 0.760784, 1), "int": Color(0.258824, 1, 0.760784, 1), From 97d9abc0d4ce9757ff97ac22e8e1d8b5feb1b08d Mon Sep 17 00:00:00 2001 From: Amethyst-szs Date: Sun, 26 Nov 2023 14:59:19 -0800 Subject: [PATCH 08/30] Added example scene --- example/example_scene.gd | 36 ++++++++++++++++ example/example_scene.tscn | 86 ++++++++++++++++++++++++++++++++++++++ project.godot | 2 +- 3 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 example/example_scene.gd create mode 100644 example/example_scene.tscn diff --git a/example/example_scene.gd b/example/example_scene.gd new file mode 100644 index 0000000..b4078de --- /dev/null +++ b/example/example_scene.gd @@ -0,0 +1,36 @@ +extends Control + +func _ready(): + SaveAccessor.save_slot_complete.connect(_on_slot_saved) + SaveAccessor.load_slot_complete.connect(_on_slot_loaded) + SaveAccessor.save_error.connect(_on_save_error) + SaveAccessor.load_error.connect(_on_load_error) + +func _process(_delta): + %ProgressSlider.value = SaveHolder.slot.progress + pass + +func _on_h_slider_changed(value: float): + SaveHolder.slot.progress = value + pass + +func _on_spin_box_value_changed(value: int): + SaveAccessor.set_active_slot(value) + +func _on_button_save_pressed(): + SaveAccessor.write_active_slot() + +func _on_button_load_pressed(): + SaveAccessor.read_active_slot() + +func _on_slot_saved(): + %SignalNotif.text = "Signal: Save Completed" + +func _on_slot_loaded(): + %SignalNotif.text = "Signal: Load Completed" + +func _on_save_error(): + %SignalNotif.text = "Signal: Save Errored" + +func _on_load_error(): + %SignalNotif.text = "Signal: Load Errored" diff --git a/example/example_scene.tscn b/example/example_scene.tscn new file mode 100644 index 0000000..2aa2ac5 --- /dev/null +++ b/example/example_scene.tscn @@ -0,0 +1,86 @@ +[gd_scene load_steps=2 format=3 uid="uid://3l7o1u8eqkp3"] + +[ext_resource type="Script" path="res://example/example_scene.gd" id="1_gg2b8"] + +[node name="ExampleScene" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_gg2b8") + +[node name="Notifications" type="MarginContainer" parent="."] +layout_mode = 1 +anchors_preset = 12 +anchor_top = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 0 + +[node name="VBox" type="VBoxContainer" parent="Notifications"] +layout_mode = 2 + +[node name="SignalHeader" type="Label" parent="Notifications/VBox"] +layout_mode = 2 +text = "Signal Events:" + +[node name="SignalNotif" type="Label" parent="Notifications/VBox"] +unique_name_in_owner = true +layout_mode = 2 +text = "None yet" + +[node name="Header" type="VBoxContainer" parent="."] +layout_mode = 1 +anchors_preset = 10 +anchor_right = 1.0 +offset_bottom = 31.0 +grow_horizontal = 2 + +[node name="SaveLoad" type="HBoxContainer" parent="Header"] +layout_mode = 2 +alignment = 1 + +[node name="SpinBox" type="SpinBox" parent="Header/SaveLoad"] +layout_mode = 2 +size_flags_horizontal = 3 +min_value = 1.0 +max_value = 5.0 +value = 1.0 +rounded = true +prefix = "Save Slot" + +[node name="ButtonSave" type="Button" parent="Header/SaveLoad"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +text = "Save" + +[node name="ButtonLoad" type="Button" parent="Header/SaveLoad"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +text = "Load" + +[node name="HSeparator2" type="HSeparator" parent="Header"] +layout_mode = 2 +theme_override_constants/separation = 25 + +[node name="RichTextLabel" type="RichTextLabel" parent="Header"] +layout_mode = 2 +size_flags_vertical = 4 +bbcode_enabled = true +text = "Progress Value: [i](this is saved to the specific save slot)[/i]" +fit_content = true +scroll_active = false + +[node name="ProgressSlider" type="HSlider" parent="Header"] +unique_name_in_owner = true +layout_mode = 2 + +[connection signal="value_changed" from="Header/SaveLoad/SpinBox" to="." method="_on_spin_box_value_changed"] +[connection signal="pressed" from="Header/SaveLoad/ButtonSave" to="." method="_on_button_save_pressed"] +[connection signal="pressed" from="Header/SaveLoad/ButtonLoad" to="." method="_on_button_load_pressed"] +[connection signal="value_changed" from="Header/ProgressSlider" to="." method="_on_h_slider_changed"] diff --git a/project.godot b/project.godot index 890ad26..d1fdf94 100644 --- a/project.godot +++ b/project.godot @@ -12,7 +12,7 @@ config_version=5 config/name="Save Load Plugin" config/tags=PackedStringArray("plugin") -run/main_scene="res://testing/test_save.tscn" +run/main_scene="res://example/example_scene.tscn" config/features=PackedStringArray("4.1", "Mobile") config/icon="res://icon.svg" From eb2c0dc163cce1e9470ee8603388a74292ba5871 Mon Sep 17 00:00:00 2001 From: Amethyst-szs Date: Sun, 26 Nov 2023 15:02:24 -0800 Subject: [PATCH 09/30] Made the example scene less ugly --- example/example_scene.tscn | 55 +++++++++++++++++++++----------------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/example/example_scene.tscn b/example/example_scene.tscn index 2aa2ac5..ef32f01 100644 --- a/example/example_scene.tscn +++ b/example/example_scene.tscn @@ -11,39 +11,46 @@ grow_horizontal = 2 grow_vertical = 2 script = ExtResource("1_gg2b8") -[node name="Notifications" type="MarginContainer" parent="."] +[node name="Panel" type="PanelContainer" parent="."] layout_mode = 1 -anchors_preset = 12 -anchor_top = 1.0 +anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 grow_horizontal = 2 -grow_vertical = 0 +grow_vertical = 2 + +[node name="Margin" type="MarginContainer" parent="Panel"] +layout_mode = 2 +theme_override_constants/margin_left = 25 +theme_override_constants/margin_top = 25 +theme_override_constants/margin_right = 25 +theme_override_constants/margin_bottom = 25 + +[node name="Notifications" type="MarginContainer" parent="Panel/Margin"] +layout_mode = 2 +size_flags_vertical = 8 -[node name="VBox" type="VBoxContainer" parent="Notifications"] +[node name="VBox" type="VBoxContainer" parent="Panel/Margin/Notifications"] layout_mode = 2 -[node name="SignalHeader" type="Label" parent="Notifications/VBox"] +[node name="SignalHeader" type="Label" parent="Panel/Margin/Notifications/VBox"] layout_mode = 2 text = "Signal Events:" -[node name="SignalNotif" type="Label" parent="Notifications/VBox"] +[node name="SignalNotif" type="Label" parent="Panel/Margin/Notifications/VBox"] unique_name_in_owner = true layout_mode = 2 text = "None yet" -[node name="Header" type="VBoxContainer" parent="."] -layout_mode = 1 -anchors_preset = 10 -anchor_right = 1.0 -offset_bottom = 31.0 -grow_horizontal = 2 +[node name="Header" type="VBoxContainer" parent="Panel/Margin"] +layout_mode = 2 +size_flags_vertical = 0 -[node name="SaveLoad" type="HBoxContainer" parent="Header"] +[node name="SaveLoad" type="HBoxContainer" parent="Panel/Margin/Header"] layout_mode = 2 alignment = 1 -[node name="SpinBox" type="SpinBox" parent="Header/SaveLoad"] +[node name="SpinBox" type="SpinBox" parent="Panel/Margin/Header/SaveLoad"] layout_mode = 2 size_flags_horizontal = 3 min_value = 1.0 @@ -52,23 +59,23 @@ value = 1.0 rounded = true prefix = "Save Slot" -[node name="ButtonSave" type="Button" parent="Header/SaveLoad"] +[node name="ButtonSave" type="Button" parent="Panel/Margin/Header/SaveLoad"] unique_name_in_owner = true layout_mode = 2 size_flags_horizontal = 3 text = "Save" -[node name="ButtonLoad" type="Button" parent="Header/SaveLoad"] +[node name="ButtonLoad" type="Button" parent="Panel/Margin/Header/SaveLoad"] unique_name_in_owner = true layout_mode = 2 size_flags_horizontal = 3 text = "Load" -[node name="HSeparator2" type="HSeparator" parent="Header"] +[node name="HSeparator2" type="HSeparator" parent="Panel/Margin/Header"] layout_mode = 2 theme_override_constants/separation = 25 -[node name="RichTextLabel" type="RichTextLabel" parent="Header"] +[node name="RichTextLabel" type="RichTextLabel" parent="Panel/Margin/Header"] layout_mode = 2 size_flags_vertical = 4 bbcode_enabled = true @@ -76,11 +83,11 @@ text = "Progress Value: [i](this is saved to the specific save slot)[/i]" fit_content = true scroll_active = false -[node name="ProgressSlider" type="HSlider" parent="Header"] +[node name="ProgressSlider" type="HSlider" parent="Panel/Margin/Header"] unique_name_in_owner = true layout_mode = 2 -[connection signal="value_changed" from="Header/SaveLoad/SpinBox" to="." method="_on_spin_box_value_changed"] -[connection signal="pressed" from="Header/SaveLoad/ButtonSave" to="." method="_on_button_save_pressed"] -[connection signal="pressed" from="Header/SaveLoad/ButtonLoad" to="." method="_on_button_load_pressed"] -[connection signal="value_changed" from="Header/ProgressSlider" to="." method="_on_h_slider_changed"] +[connection signal="value_changed" from="Panel/Margin/Header/SaveLoad/SpinBox" to="." method="_on_spin_box_value_changed"] +[connection signal="pressed" from="Panel/Margin/Header/SaveLoad/ButtonSave" to="." method="_on_button_save_pressed"] +[connection signal="pressed" from="Panel/Margin/Header/SaveLoad/ButtonLoad" to="." method="_on_button_load_pressed"] +[connection signal="value_changed" from="Panel/Margin/Header/ProgressSlider" to="." method="_on_h_slider_changed"] From abd60325a4b356006f7b119caa8ff47dbd621d65 Mon Sep 17 00:00:00 2001 From: Amethyst-szs Date: Wed, 29 Nov 2023 18:57:01 -0800 Subject: [PATCH 10/30] Very important bug-fix with arrays Turns out sub-objects and arrays weren't getting read correctly and I had to do a pretty major refactor to the save reading system to fix it, finally got it though :D --- addons/savedata-dx/backend/save_accessor.gd | 102 ++++++++++++++++++-- addons/savedata-dx/backend/save_holder.gd | 2 + 2 files changed, 96 insertions(+), 8 deletions(-) diff --git a/addons/savedata-dx/backend/save_accessor.gd b/addons/savedata-dx/backend/save_accessor.gd index 976afcc..9cf93f7 100644 --- a/addons/savedata-dx/backend/save_accessor.gd +++ b/addons/savedata-dx/backend/save_accessor.gd @@ -93,10 +93,7 @@ func read_slot(index: int) -> void: # Create a new current save and write each key from the JSON into it SaveHolder.reset_slot() - for key in range(dict.size()): - var key_name: String = dict.keys()[key] - var value = dict.values()[key] - SaveHolder.slot.set(key_name, value) + _dict_to_object(dict, [SaveHolder.slot]) # Tell the signal that the load is finished load_slot_complete.emit() @@ -127,10 +124,7 @@ func read_common() -> void: # Create a new common save and write each key from the JSON into it SaveHolder.reset_common() - for key in range(dict.size()): - var key_name: String = dict.keys()[key] - var value = dict.values()[key] - SaveHolder.common.set(key_name, value) + _dict_to_object(dict, [SaveHolder.common]) # Tell the signal that the load is finished load_common_complete.emit() @@ -288,3 +282,95 @@ func _expand_array_for_dict(list: Array) -> Array: new_list.push_back(item) return new_list + +## Not intended for the end user. +## Writes dictionary data into object, including dedicated array handling +func _dict_to_object(dict: Dictionary, obj_ref: Array) -> void: + # Iterate through every key in dictionary + for key in range(dict.size()): + # Get the variable from the object reference so you can compare type + var member: Array = [obj_ref[0].get(dict.keys()[key])] + + # Perform different behavior depending on the type of this member + match(typeof(member[0])): + TYPE_OBJECT: # Call self again with sub-object + _dict_to_object(dict.values()[key], member) + TYPE_ARRAY: + _handle_array_in_dict_for_object(dict.values()[key], member[0], dict.keys()[key]) + _: # Default behavior + obj_ref[0].set(dict.keys()[key], dict.values()[key]) + +## Not intended for the end user. +## Handle converting a dictionary array into a typed array, part of the dict_to_obj method +func _handle_array_in_dict_for_object(dict_ar: Array, obj_ar: Array, ar_name: String) -> void: + # Get the type of the array + var type: int = obj_ar.get_typed_builtin() + var obj_type: Object = obj_ar.get_typed_script() + + # Reset array to empty to avoid duplication if the array has default values + if not obj_ar.is_empty(): + # If the array holds objects, manually free them before emptying array + if type == TYPE_OBJECT: + for item in obj_ar: + if item.has_method("free"): + item.free() + else: + push_warning("Object in SaveData array \"%s\" doesn't have free method. + Memory leak!" % [ar_name]) + + obj_ar.clear() + + # Handle proceeding from here differently depending on type + match(type): + TYPE_OBJECT: # Handle an object inside an array + # Iterate through every index in the dictionary array + for item in dict_ar: + # Create new object of array's type, then build it with dict_to_obj + obj_ar.push_back(obj_type.new()) + _dict_to_object(item, [obj_ar.back()]) + TYPE_BOOL, TYPE_FLOAT, TYPE_STRING, TYPE_DICTIONARY: + for item in dict_ar: + obj_ar.push_back(item) + TYPE_INT: + for item in dict_ar: + obj_ar.push_back(int(item)) + _: # Error if the array has a bad type + push_error("SaveData script arrays must be typed with one of the following types: + Object, bool, int, float, string, or dictionary. + Name of array that spawned error: %s + + All scripts that extend from object are valid here, + this includes scripts created with the SaveData menu. + To use other types, put those types inside an object and use that object." + % [ar_name]) + return + +## Not intended for the end user. +## Called by SaveHolder to prevent memory leaks when destroying save data +func _free_object_and_subobjects(obj_ref: Array[Object]) -> void: + # Get all properties from object + var members = obj_ref[0].get_property_list() + var member_index: int = 0 + + # Iterate through all members + for member in members: + member_index += 1 + if member_index <= 2: + continue + + # Get value of current member + var value = obj_ref[0].get(member.name) + + match(typeof(obj_ref[0].get(member.name))): + TYPE_OBJECT: # Free this object + obj_ref[0].get(member.name).free() + TYPE_ARRAY: # Check if this is an array and check if it holds objects + if not value.get_typed_builtin() == TYPE_OBJECT: + continue + + # If it holds objects, scan each of these for subobjects and then destroy it + for item in value: + _free_object_and_subobjects([item]) + + # Once iterating is complete, destroy root object + obj_ref[0].free() diff --git a/addons/savedata-dx/backend/save_holder.gd b/addons/savedata-dx/backend/save_holder.gd index 377d1df..6275501 100644 --- a/addons/savedata-dx/backend/save_holder.gd +++ b/addons/savedata-dx/backend/save_holder.gd @@ -29,10 +29,12 @@ func _ready(): ## Resets the data in the slot variable to default func reset_slot(): + SaveAccessor._free_object_and_subobjects([slot]) slot = slot_script.new() ## Resets the data in the common variable to default func reset_common(): + SaveAccessor._free_object_and_subobjects([common]) common = common_script.new() ## Resets the data in the slot and common variable to default. From 91856fc851c4172e0762cc3234def51f6060e7d2 Mon Sep 17 00:00:00 2001 From: Amethyst-szs Date: Wed, 29 Nov 2023 20:13:18 -0800 Subject: [PATCH 11/30] Added multi-threading to SaveAccessor --- addons/savedata-dx/backend/save_accessor.gd | 138 ++++++++++++++++++-- example/example_scene.gd | 10 +- example/example_scene.tscn | 11 +- 3 files changed, 148 insertions(+), 11 deletions(-) diff --git a/addons/savedata-dx/backend/save_accessor.gd b/addons/savedata-dx/backend/save_accessor.gd index 9cf93f7..93e6009 100644 --- a/addons/savedata-dx/backend/save_accessor.gd +++ b/addons/savedata-dx/backend/save_accessor.gd @@ -23,6 +23,11 @@ const KEY: String = "no@NqlqGu8PTG#weQ77t$%bBQ9$HG5itZ#8#Xnbd%&L$y5Sd" # Signal messages +## Emitted when the thread becomes busy due to a request +signal thread_busy +## Emitted when the current save/load is finished, regardless of success +signal thread_complete + ## Emitted when saving to a slot is completed successfully signal save_slot_complete ## Emitted when saving the common data is completed successfully @@ -40,6 +45,81 @@ signal save_error ## Emitted when a load call fails signal load_error +# Thread system + +## What kind of action are you requesting the thread to perform +enum ThreadRequestType { + UNKNOWN = -1, + WRITE_SLOT, + READ_SLOT, + WRITE_COMMON, + READ_COMMON +} + +## Class containing information about what the thread should start doing +class ThreadRequest: + var type: ThreadRequestType = ThreadRequestType.UNKNOWN + var slot_id: int = 0 + +## The thread used for reading/writting save data +var thread: Thread + +## Trigger used to start the thread's process +var thread_semaphore: Semaphore + +## Information about what the thread should be doing +var thread_request: ThreadRequest = ThreadRequest.new() + +## Is the thread currently busy +var is_thread_busy: bool = false + +## Should the thread be terminated +var is_thread_terminate: bool = false + + +# Methods start below here + +func _ready(): + # Setup thread and and its components + thread_semaphore = Semaphore.new() + is_thread_busy = false + is_thread_terminate = false + + thread = Thread.new() + thread.start(_thread_func, Thread.PRIORITY_NORMAL) + +func _thread_func(): + while true: + thread_semaphore.wait() + if is_thread_terminate: break + + is_thread_busy = true + call_deferred("emit_signal", "thread_busy") + + match(thread_request.type): + ThreadRequestType.WRITE_SLOT: + _write_slot_thread_func(thread_request.slot_id) + ThreadRequestType.READ_SLOT: + _read_slot_thread_func(thread_request.slot_id) + ThreadRequestType.WRITE_COMMON: + _write_common_thread_func() + ThreadRequestType.READ_COMMON: + _read_common_thread_func() + + is_thread_busy = false + call_deferred("emit_signal", "thread_complete") + + if is_thread_terminate: break + +func _thread_busy_warning(): + push_warning("Save/Load request ignored cause SaveAccessor thread is busy! + You can be notified when the thread is free with the \"thread_complete\" signal") + +func _exit_tree(): + is_thread_terminate = true + thread_semaphore.post() + thread.wait_to_finish() + # Check/modify a save slot determined by the variable "active_save_slot" ## Current save slot, useful to manage which file is getting read/written to @@ -77,18 +157,40 @@ func is_slot_exist(index: int) -> bool: ## Writes to a specific save slot index, and emits "save_slot_complete" when successful func write_slot(index: int) -> void: + if is_thread_busy: + _thread_busy_warning() + return + + thread_request.type = ThreadRequestType.WRITE_SLOT + thread_request.slot_id = index + thread_semaphore.post() + +## Not intended for the end user. +## Functionality for save slot writing, handled on a seperate thread +func _write_slot_thread_func(index: int) -> void: if _write_backend("s%s" % [str(index)], SaveHolder.slot): # Tell the signal that the save is finished successfully - save_slot_complete.emit() + call_deferred("emit_signal", "save_slot_complete") else: - save_error.emit() + call_deferred("emit_signal", "save_error") ## Loads data a specific save slot index, and emits "load_slot_complete" when successful func read_slot(index: int) -> void: + if is_thread_busy: + _thread_busy_warning() + return + + thread_request.type = ThreadRequestType.READ_SLOT + thread_request.slot_id = index + thread_semaphore.post() + +## Not intended for the end user. +## Functionality for save slot reading, handled on a seperate thread +func _read_slot_thread_func(index: int) -> void: # Get dictionary from file in save directory var dict: Dictionary = _read_backend_by_name("s%s" % [str(index)]) if dict.is_empty(): - load_error.emit() + call_deferred("emit_signal", "load_error") return # Create a new current save and write each key from the JSON into it @@ -96,7 +198,7 @@ func read_slot(index: int) -> void: _dict_to_object(dict, [SaveHolder.slot]) # Tell the signal that the load is finished - load_slot_complete.emit() + call_deferred("emit_signal", "load_slot_complete") # Check/modify the common save data shared between all slots @@ -108,18 +210,38 @@ func is_common_exist() -> bool: ## Writes the common data to disk, and emits "save_common_complete" when successful func write_common() -> void: + if is_thread_busy: + _thread_busy_warning() + return + + thread_request.type = ThreadRequestType.WRITE_COMMON + thread_semaphore.post() + +## Not intended for the end user. +## Functionality for common writing, handled on a seperate thread +func _write_common_thread_func() -> void: if _write_backend(SAVE_COMMON_NAME, SaveHolder.common): # Tell the signal that the save is finished successfully - save_common_complete.emit() + call_deferred("emit_signal", "save_common_complete") else: - save_error.emit() + call_deferred("emit_signal", "save_error") ## Reads the common data from the disk, and emits "load_common_complete" when successful func read_common() -> void: + if is_thread_busy: + _thread_busy_warning() + return + + thread_request.type = ThreadRequestType.READ_COMMON + thread_semaphore.post() + +## Not intended for the end user. +## Functionality for common reading, handled on a seperate thread +func _read_common_thread_func() -> void: # Get dictionary from file in save directory var dict: Dictionary = _read_backend_by_name(SAVE_COMMON_NAME) if dict.is_empty(): - load_error.emit() + call_deferred("emit_signal", "load_error") return # Create a new common save and write each key from the JSON into it @@ -127,7 +249,7 @@ func read_common() -> void: _dict_to_object(dict, [SaveHolder.common]) # Tell the signal that the load is finished - load_common_complete.emit() + call_deferred("emit_signal", "load_slot_complete") # Backend functions handling reading and writing of data diff --git a/example/example_scene.gd b/example/example_scene.gd index b4078de..dd09280 100644 --- a/example/example_scene.gd +++ b/example/example_scene.gd @@ -5,14 +5,14 @@ func _ready(): SaveAccessor.load_slot_complete.connect(_on_slot_loaded) SaveAccessor.save_error.connect(_on_save_error) SaveAccessor.load_error.connect(_on_load_error) + SaveAccessor.thread_busy.connect(_on_thread_busy) + SaveAccessor.thread_complete.connect(_on_thread_complete) func _process(_delta): %ProgressSlider.value = SaveHolder.slot.progress - pass func _on_h_slider_changed(value: float): SaveHolder.slot.progress = value - pass func _on_spin_box_value_changed(value: int): SaveAccessor.set_active_slot(value) @@ -34,3 +34,9 @@ func _on_save_error(): func _on_load_error(): %SignalNotif.text = "Signal: Load Errored" + +func _on_thread_busy(): + %ThreadNotif.text = "Thread Busy!" + +func _on_thread_complete(): + %ThreadNotif.text = "Thread Inactive" diff --git a/example/example_scene.tscn b/example/example_scene.tscn index ef32f01..82cdcee 100644 --- a/example/example_scene.tscn +++ b/example/example_scene.tscn @@ -35,13 +35,18 @@ layout_mode = 2 [node name="SignalHeader" type="Label" parent="Panel/Margin/Notifications/VBox"] layout_mode = 2 -text = "Signal Events:" +text = "Events and Status:" [node name="SignalNotif" type="Label" parent="Panel/Margin/Notifications/VBox"] unique_name_in_owner = true layout_mode = 2 text = "None yet" +[node name="ThreadNotif" type="Label" parent="Panel/Margin/Notifications/VBox"] +unique_name_in_owner = true +layout_mode = 2 +text = "Thread Inactive" + [node name="Header" type="VBoxContainer" parent="Panel/Margin"] layout_mode = 2 size_flags_vertical = 0 @@ -87,6 +92,10 @@ scroll_active = false unique_name_in_owner = true layout_mode = 2 +[node name="HSeparator3" type="HSeparator" parent="Panel/Margin/Header"] +layout_mode = 2 +theme_override_constants/separation = 25 + [connection signal="value_changed" from="Panel/Margin/Header/SaveLoad/SpinBox" to="." method="_on_spin_box_value_changed"] [connection signal="pressed" from="Panel/Margin/Header/SaveLoad/ButtonSave" to="." method="_on_button_save_pressed"] [connection signal="pressed" from="Panel/Margin/Header/SaveLoad/ButtonLoad" to="." method="_on_button_load_pressed"] From bf7ed288bfa071181a1f79d9837c0e8a8f181496 Mon Sep 17 00:00:00 2001 From: Amethyst-szs Date: Thu, 30 Nov 2023 07:28:56 -0800 Subject: [PATCH 12/30] Updated and cleaned for Godot 4.2 --- addons/savedata-dx/backend/save_accessor.gd | 240 ++++++++++---------- addons/savedata-dx/plugin.cfg | 2 +- project.godot | 2 +- 3 files changed, 128 insertions(+), 116 deletions(-) diff --git a/addons/savedata-dx/backend/save_accessor.gd b/addons/savedata-dx/backend/save_accessor.gd index 93e6009..75666b9 100644 --- a/addons/savedata-dx/backend/save_accessor.gd +++ b/addons/savedata-dx/backend/save_accessor.gd @@ -6,11 +6,15 @@ extends Node ## access it via "SaveAccessor" in your scritps. class_name SaveAccessorPlugin +#region Imports + # Include datatype parser for converting Object into JSON const datatype_dict_parser = preload("res://addons/savedata-dx/backend/datatype_parser.gd") var dict_parse = datatype_dict_parser.new() -# Constants defining where the saves are located and how they are named/stored +#endregion + +#region Constants ## Directory to store save files const SAVE_DIR: String = "user://sv/" @@ -21,7 +25,9 @@ const SAVE_EXTENSION_NAME: String = ".bin" ## Encryption key, can be changed but will break all existing saves if changed const KEY: String = "no@NqlqGu8PTG#weQ77t$%bBQ9$HG5itZ#8#Xnbd%&L$y5Sd" -# Signal messages +#endregion + +#region Signals ## Emitted when the thread becomes busy due to a request signal thread_busy @@ -45,82 +51,9 @@ signal save_error ## Emitted when a load call fails signal load_error -# Thread system - -## What kind of action are you requesting the thread to perform -enum ThreadRequestType { - UNKNOWN = -1, - WRITE_SLOT, - READ_SLOT, - WRITE_COMMON, - READ_COMMON -} - -## Class containing information about what the thread should start doing -class ThreadRequest: - var type: ThreadRequestType = ThreadRequestType.UNKNOWN - var slot_id: int = 0 - -## The thread used for reading/writting save data -var thread: Thread - -## Trigger used to start the thread's process -var thread_semaphore: Semaphore - -## Information about what the thread should be doing -var thread_request: ThreadRequest = ThreadRequest.new() - -## Is the thread currently busy -var is_thread_busy: bool = false - -## Should the thread be terminated -var is_thread_terminate: bool = false - - -# Methods start below here - -func _ready(): - # Setup thread and and its components - thread_semaphore = Semaphore.new() - is_thread_busy = false - is_thread_terminate = false - - thread = Thread.new() - thread.start(_thread_func, Thread.PRIORITY_NORMAL) - -func _thread_func(): - while true: - thread_semaphore.wait() - if is_thread_terminate: break - - is_thread_busy = true - call_deferred("emit_signal", "thread_busy") - - match(thread_request.type): - ThreadRequestType.WRITE_SLOT: - _write_slot_thread_func(thread_request.slot_id) - ThreadRequestType.READ_SLOT: - _read_slot_thread_func(thread_request.slot_id) - ThreadRequestType.WRITE_COMMON: - _write_common_thread_func() - ThreadRequestType.READ_COMMON: - _read_common_thread_func() - - is_thread_busy = false - call_deferred("emit_signal", "thread_complete") - - if is_thread_terminate: break - -func _thread_busy_warning(): - push_warning("Save/Load request ignored cause SaveAccessor thread is busy! - You can be notified when the thread is free with the \"thread_complete\" signal") - -func _exit_tree(): - is_thread_terminate = true - thread_semaphore.post() - thread.wait_to_finish() +#endregion -# Check/modify a save slot determined by the variable "active_save_slot" +#region End-user Functions ## Current save slot, useful to manage which file is getting read/written to var active_save_slot: int = 1: @@ -148,8 +81,6 @@ func write_active_slot() -> void: func read_active_slot() -> void: read_slot(active_save_slot) -# Check/modify a specific save slot, specified by index - ## Checks if a file exists for a specific save slot index, does not ensure it is valid func is_slot_exist(index: int) -> bool: var path = SAVE_DIR + "s" + str(index) + SAVE_EXTENSION_NAME @@ -165,15 +96,6 @@ func write_slot(index: int) -> void: thread_request.slot_id = index thread_semaphore.post() -## Not intended for the end user. -## Functionality for save slot writing, handled on a seperate thread -func _write_slot_thread_func(index: int) -> void: - if _write_backend("s%s" % [str(index)], SaveHolder.slot): - # Tell the signal that the save is finished successfully - call_deferred("emit_signal", "save_slot_complete") - else: - call_deferred("emit_signal", "save_error") - ## Loads data a specific save slot index, and emits "load_slot_complete" when successful func read_slot(index: int) -> void: if is_thread_busy: @@ -184,6 +106,42 @@ func read_slot(index: int) -> void: thread_request.slot_id = index thread_semaphore.post() +## Checks if the common save exists in save directory +func is_common_exist() -> bool: + var path = SAVE_DIR + SAVE_COMMON_NAME + SAVE_EXTENSION_NAME + return FileAccess.file_exists(path) + +## Writes the common data to disk, and emits "save_common_complete" when successful +func write_common() -> void: + if is_thread_busy: + _thread_busy_warning() + return + + thread_request.type = ThreadRequestType.WRITE_COMMON + thread_semaphore.post() + +## Reads the common data from the disk, and emits "load_common_complete" when successful +func read_common() -> void: + if is_thread_busy: + _thread_busy_warning() + return + + thread_request.type = ThreadRequestType.READ_COMMON + thread_semaphore.post() + +#endregion + +#region Backend functions called by thread + +## Not intended for the end user. +## Functionality for save slot writing, handled on a seperate thread +func _write_slot_thread_func(index: int) -> void: + if _write_backend("s%s" % [str(index)], SaveHolder.slot): + # Tell the signal that the save is finished successfully + call_deferred("emit_signal", "save_slot_complete") + else: + call_deferred("emit_signal", "save_error") + ## Not intended for the end user. ## Functionality for save slot reading, handled on a seperate thread func _read_slot_thread_func(index: int) -> void: @@ -200,23 +158,6 @@ func _read_slot_thread_func(index: int) -> void: # Tell the signal that the load is finished call_deferred("emit_signal", "load_slot_complete") - -# Check/modify the common save data shared between all slots - -## Checks if the common save exists in save directory -func is_common_exist() -> bool: - var path = SAVE_DIR + SAVE_COMMON_NAME + SAVE_EXTENSION_NAME - return FileAccess.file_exists(path) - -## Writes the common data to disk, and emits "save_common_complete" when successful -func write_common() -> void: - if is_thread_busy: - _thread_busy_warning() - return - - thread_request.type = ThreadRequestType.WRITE_COMMON - thread_semaphore.post() - ## Not intended for the end user. ## Functionality for common writing, handled on a seperate thread func _write_common_thread_func() -> void: @@ -226,15 +167,6 @@ func _write_common_thread_func() -> void: else: call_deferred("emit_signal", "save_error") -## Reads the common data from the disk, and emits "load_common_complete" when successful -func read_common() -> void: - if is_thread_busy: - _thread_busy_warning() - return - - thread_request.type = ThreadRequestType.READ_COMMON - thread_semaphore.post() - ## Not intended for the end user. ## Functionality for common reading, handled on a seperate thread func _read_common_thread_func() -> void: @@ -346,6 +278,84 @@ func _read_backend_raw_data(path: String) -> String: # Return the JSON data to then be converted into a object later return content +#endregion + +#region Multithreading System + +## What kind of action are you requesting the thread to perform +enum ThreadRequestType { + UNKNOWN = -1, + WRITE_SLOT, + READ_SLOT, + WRITE_COMMON, + READ_COMMON +} + +## Class containing information about what the thread should start doing +class ThreadRequest: + var type: ThreadRequestType = ThreadRequestType.UNKNOWN + var slot_id: int = 0 + +## The thread used for reading/writting save data +var thread: Thread + +## Trigger used to start the thread's process +var thread_semaphore: Semaphore + +## Information about what the thread should be doing +var thread_request: ThreadRequest = ThreadRequest.new() + +## Is the thread currently busy +var is_thread_busy: bool = false + +## Should the thread be terminated +var is_thread_terminate: bool = false + +func _ready(): + # Setup thread and and its components + thread_semaphore = Semaphore.new() + is_thread_busy = false + is_thread_terminate = false + + thread = Thread.new() + thread.start(_thread_func, Thread.PRIORITY_NORMAL) + +func _thread_func(): + while true: + thread_semaphore.wait() + if is_thread_terminate: break + + is_thread_busy = true + call_deferred("emit_signal", "thread_busy") + + match(thread_request.type): + ThreadRequestType.WRITE_SLOT: + _write_slot_thread_func(thread_request.slot_id) + ThreadRequestType.READ_SLOT: + _read_slot_thread_func(thread_request.slot_id) + ThreadRequestType.WRITE_COMMON: + _write_common_thread_func() + ThreadRequestType.READ_COMMON: + _read_common_thread_func() + + is_thread_busy = false + call_deferred("emit_signal", "thread_complete") + + if is_thread_terminate: break + +func _thread_busy_warning(): + push_warning("Save/Load request ignored cause SaveAccessor thread is busy! + You can be notified when the thread is free with the \"thread_complete\" signal") + +func _exit_tree(): + is_thread_terminate = true + thread_semaphore.post() + thread.wait_to_finish() + +#endregion + +#region Backend Save Parsing Utilities + ## Not intended for the end user. ## Converts object class into dictionary for saving process func _object_to_dict(obj: Object) -> Dictionary: @@ -496,3 +506,5 @@ func _free_object_and_subobjects(obj_ref: Array[Object]) -> void: # Once iterating is complete, destroy root object obj_ref[0].free() + +#endregion diff --git a/addons/savedata-dx/plugin.cfg b/addons/savedata-dx/plugin.cfg index c62d518..56c59e2 100644 --- a/addons/savedata-dx/plugin.cfg +++ b/addons/savedata-dx/plugin.cfg @@ -3,5 +3,5 @@ name="SaveDataDX" description="A plugin combining the benefits of JSON & Resource saving, making save data easy, fast, and secure" author="Amethyst-szs" -version="1.0.0" +version="1.2.1" script="savedata-dx.gd" diff --git a/project.godot b/project.godot index d1fdf94..9fced9e 100644 --- a/project.godot +++ b/project.godot @@ -13,7 +13,7 @@ config_version=5 config/name="Save Load Plugin" config/tags=PackedStringArray("plugin") run/main_scene="res://example/example_scene.tscn" -config/features=PackedStringArray("4.1", "Mobile") +config/features=PackedStringArray("4.2", "Mobile") config/icon="res://icon.svg" [autoload] From 737789c377595e1c4800682bbb54280a65d07924 Mon Sep 17 00:00:00 2001 From: Amethyst-szs Date: Thu, 30 Nov 2023 07:35:36 -0800 Subject: [PATCH 13/30] Added handling for if JSON has key not in object --- addons/savedata-dx/backend/save_accessor.gd | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/addons/savedata-dx/backend/save_accessor.gd b/addons/savedata-dx/backend/save_accessor.gd index 75666b9..c98fc0c 100644 --- a/addons/savedata-dx/backend/save_accessor.gd +++ b/addons/savedata-dx/backend/save_accessor.gd @@ -425,6 +425,10 @@ func _dict_to_object(dict: Dictionary, obj_ref: Array) -> void: # Perform different behavior depending on the type of this member match(typeof(member[0])): + TYPE_NIL: # Make sure this property exists on the object + push_warning("Loading SaveData: Property \"%s\" does not exist on object" + % [dict.keys()[key]]) + continue TYPE_OBJECT: # Call self again with sub-object _dict_to_object(dict.values()[key], member) TYPE_ARRAY: From 29d05a4693c702b3daa8ba33e3bbff2bf6cbe077 Mon Sep 17 00:00:00 2001 From: Amethyst-szs Date: Fri, 1 Dec 2023 07:31:21 -0800 Subject: [PATCH 14/30] Massively cleaner and faster code with RefCounted Changed type of slot and common RefCounted instead of regular Object so it doesn't have to run a ton of extra code cleaning up memory leaks --- addons/savedata-dx/backend/save_accessor.gd | 41 +-------------------- addons/savedata-dx/backend/save_holder.gd | 2 - addons/savedata-dx/data_common.gd | 2 +- addons/savedata-dx/data_slot.gd | 4 +- addons/savedata-dx/view/save_view.gd | 2 +- 5 files changed, 5 insertions(+), 46 deletions(-) diff --git a/addons/savedata-dx/backend/save_accessor.gd b/addons/savedata-dx/backend/save_accessor.gd index c98fc0c..528409a 100644 --- a/addons/savedata-dx/backend/save_accessor.gd +++ b/addons/savedata-dx/backend/save_accessor.gd @@ -367,7 +367,7 @@ func _object_to_dict(obj: Object) -> Dictionary: # Iterate through all members for member in members: member_index += 1 - if member_index <= 2: + if member_index <= 3: continue # Write the member to dictionary depending on type of member @@ -445,15 +445,6 @@ func _handle_array_in_dict_for_object(dict_ar: Array, obj_ar: Array, ar_name: St # Reset array to empty to avoid duplication if the array has default values if not obj_ar.is_empty(): - # If the array holds objects, manually free them before emptying array - if type == TYPE_OBJECT: - for item in obj_ar: - if item.has_method("free"): - item.free() - else: - push_warning("Object in SaveData array \"%s\" doesn't have free method. - Memory leak!" % [ar_name]) - obj_ar.clear() # Handle proceeding from here differently depending on type @@ -481,34 +472,4 @@ func _handle_array_in_dict_for_object(dict_ar: Array, obj_ar: Array, ar_name: St % [ar_name]) return -## Not intended for the end user. -## Called by SaveHolder to prevent memory leaks when destroying save data -func _free_object_and_subobjects(obj_ref: Array[Object]) -> void: - # Get all properties from object - var members = obj_ref[0].get_property_list() - var member_index: int = 0 - - # Iterate through all members - for member in members: - member_index += 1 - if member_index <= 2: - continue - - # Get value of current member - var value = obj_ref[0].get(member.name) - - match(typeof(obj_ref[0].get(member.name))): - TYPE_OBJECT: # Free this object - obj_ref[0].get(member.name).free() - TYPE_ARRAY: # Check if this is an array and check if it holds objects - if not value.get_typed_builtin() == TYPE_OBJECT: - continue - - # If it holds objects, scan each of these for subobjects and then destroy it - for item in value: - _free_object_and_subobjects([item]) - - # Once iterating is complete, destroy root object - obj_ref[0].free() - #endregion diff --git a/addons/savedata-dx/backend/save_holder.gd b/addons/savedata-dx/backend/save_holder.gd index 6275501..377d1df 100644 --- a/addons/savedata-dx/backend/save_holder.gd +++ b/addons/savedata-dx/backend/save_holder.gd @@ -29,12 +29,10 @@ func _ready(): ## Resets the data in the slot variable to default func reset_slot(): - SaveAccessor._free_object_and_subobjects([slot]) slot = slot_script.new() ## Resets the data in the common variable to default func reset_common(): - SaveAccessor._free_object_and_subobjects([common]) common = common_script.new() ## Resets the data in the slot and common variable to default. diff --git a/addons/savedata-dx/data_common.gd b/addons/savedata-dx/data_common.gd index 05d1e01..4a0e47a 100644 --- a/addons/savedata-dx/data_common.gd +++ b/addons/savedata-dx/data_common.gd @@ -1 +1 @@ -extends Object +extends RefCounted diff --git a/addons/savedata-dx/data_slot.gd b/addons/savedata-dx/data_slot.gd index 20c79e1..4e0ec80 100644 --- a/addons/savedata-dx/data_slot.gd +++ b/addons/savedata-dx/data_slot.gd @@ -1,2 +1,2 @@ -extends Object -var progress = 50 +extends RefCounted +var progress = 50 \ No newline at end of file diff --git a/addons/savedata-dx/view/save_view.gd b/addons/savedata-dx/view/save_view.gd index dbc09cd..2560dc6 100644 --- a/addons/savedata-dx/view/save_view.gd +++ b/addons/savedata-dx/view/save_view.gd @@ -300,7 +300,7 @@ func code_editor_save_script() -> void: return # Write data to disk - file.store_string("extends Object\n" + code_editor.text) + file.store_string("extends RefCounted\n" + code_editor.text) file.close() func apply_theme() -> void: From 754a939a9f97d2f59227954056b2c01a5c083a67 Mon Sep 17 00:00:00 2001 From: Amethyst-szs Date: Sat, 2 Dec 2023 05:46:13 -0800 Subject: [PATCH 15/30] Update project icon --- icon.svg | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/icon.svg b/icon.svg index b370ceb..8338da0 100644 --- a/icon.svg +++ b/icon.svg @@ -1 +1,17 @@ - + + + + + + + + + + + + \ No newline at end of file From 714ce1de02ef1d618fd3722bd0502b425bf44d65 Mon Sep 17 00:00:00 2001 From: Amethyst-szs Date: Wed, 6 Dec 2023 09:27:52 -0800 Subject: [PATCH 16/30] Create LICENSE --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4ed03c4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Amethyst-szs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From fa18cb2783995624e62378d98a191abe6f13a7c5 Mon Sep 17 00:00:00 2001 From: Amethyst-szs Date: Wed, 6 Dec 2023 09:29:29 -0800 Subject: [PATCH 17/30] Update .gitignore --- .gitignore | 15 +++++++++++++- addons/savedata-dx/view/highlighter.tres | 26 ++++++++++++------------ 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index 10914dc..f4ac02e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,17 @@ .godot/ # Remove testing folder -testing/ \ No newline at end of file +testing/ + +# Godot-specific ignores +.import/ +export.cfg +export_presets.cfg + +# Imported translations (automatically generated from CSV files) +*.translation + +# Mono-specific ignores +.mono/ +data_*/ +mono_crash.*.json \ No newline at end of file diff --git a/addons/savedata-dx/view/highlighter.tres b/addons/savedata-dx/view/highlighter.tres index 933acf1..ef84efa 100644 --- a/addons/savedata-dx/view/highlighter.tres +++ b/addons/savedata-dx/view/highlighter.tres @@ -6,20 +6,20 @@ symbol_color = Color(0.67, 0.79, 1, 1) function_color = Color(0.34, 0.7, 1, 1) member_variable_color = Color(0.736, 0.88, 1, 1) keyword_colors = { -"Array": Color(0.258824, 1, 0.760784, 1), -"Color": Color(0.258824, 1, 0.760784, 1), -"Quaternion": Color(0.258824, 1, 0.760784, 1), -"String": Color(0.258824, 1, 0.760784, 1), -"Vector2": Color(0.258824, 1, 0.760784, 1), -"Vector2i": Color(0.258824, 1, 0.760784, 1), -"Vector3": Color(0.258824, 1, 0.760784, 1), -"Vector3i": Color(0.258824, 1, 0.760784, 1), -"Vector4": Color(0.258824, 1, 0.760784, 1), -"Vector4i": Color(0.258824, 1, 0.760784, 1), -"bool": Color(0.258824, 1, 0.760784, 1), +"Array": Color(0.26, 1, 0.76, 1), +"Color": Color(0.26, 1, 0.76, 1), +"Quaternion": Color(0.26, 1, 0.76, 1), +"String": Color(0.26, 1, 0.76, 1), +"Vector2": Color(0.26, 1, 0.76, 1), +"Vector2i": Color(0.26, 1, 0.76, 1), +"Vector3": Color(0.26, 1, 0.76, 1), +"Vector3i": Color(0.26, 1, 0.76, 1), +"Vector4": Color(0.26, 1, 0.76, 1), +"Vector4i": Color(0.26, 1, 0.76, 1), +"bool": Color(0.26, 1, 0.76, 1), "const": Color(1, 0.44, 0.52, 1), -"float": Color(0.258824, 1, 0.760784, 1), -"int": Color(0.258824, 1, 0.760784, 1), +"float": Color(0.26, 1, 0.76, 1), +"int": Color(0.26, 1, 0.76, 1), "var": Color(1, 0.44, 0.52, 1) } color_regions = { From c9e867a18299cc656b10c6df4cd6f93c2c1442d8 Mon Sep 17 00:00:00 2001 From: Amethyst-szs Date: Tue, 12 Dec 2023 22:45:57 -0800 Subject: [PATCH 18/30] Added autosave slot system --- addons/savedata-dx/backend/save_accessor.gd | 71 +++++++++++++++++---- addons/savedata-dx/view/save_view.tscn | 8 +-- example/example_scene.gd | 6 ++ example/example_scene.tscn | 18 +++++- 4 files changed, 86 insertions(+), 17 deletions(-) diff --git a/addons/savedata-dx/backend/save_accessor.gd b/addons/savedata-dx/backend/save_accessor.gd index 528409a..b81139c 100644 --- a/addons/savedata-dx/backend/save_accessor.gd +++ b/addons/savedata-dx/backend/save_accessor.gd @@ -2,7 +2,7 @@ extends Node ## SaveDataDX plugin by Amethyst-szs - ## Used to manage reading/writing save data. -## This class is meant to be used an autoload singleton, +## This class is meant to be used as an autoload singleton, ## access it via "SaveAccessor" in your scritps. class_name SaveAccessorPlugin @@ -20,6 +20,8 @@ var dict_parse = datatype_dict_parser.new() const SAVE_DIR: String = "user://sv/" ## Name of the common file const SAVE_COMMON_NAME: String = "common" +## Name of the automatic save file +const SAVE_AUTO_NAME: String = "auto" ## File extension used by save files const SAVE_EXTENSION_NAME: String = ".bin" ## Encryption key, can be changed but will break all existing saves if changed @@ -56,8 +58,13 @@ signal load_error #region End-user Functions ## Current save slot, useful to manage which file is getting read/written to -var active_save_slot: int = 1: +var active_save_slot: int = 0: set (value): + # Clamp value to be zero or higher + if value < 0: + push_warning("Cannot have an active save slot below zero! Defaulting to zero") + value = max(0, value) + if not active_save_slot == value: active_slot_changed.emit() @@ -81,6 +88,31 @@ func write_active_slot() -> void: func read_active_slot() -> void: read_slot(active_save_slot) +## Checks if a file exists for autosave slot, does not ensure it is valid +func is_autosave_exist() -> bool: + var path = SAVE_DIR + "s" + SAVE_AUTO_NAME + SAVE_EXTENSION_NAME + return FileAccess.file_exists(path) + +## Writes to the autosave slot, and emits "save_slot_complete" when successful +func write_autosave_slot() -> void: + if is_thread_busy: + _thread_busy_warning() + return + + thread_request.type = ThreadRequestType.WRITE_SLOT + thread_request.is_slot_auto = true + thread_semaphore.post() + +## Loads data from the autosave slot, and emits "load_slot_complete" when successful +func read_autosave_slot() -> void: + if is_thread_busy: + _thread_busy_warning() + return + + thread_request.type = ThreadRequestType.READ_SLOT + thread_request.is_slot_auto = true + thread_semaphore.post() + ## Checks if a file exists for a specific save slot index, does not ensure it is valid func is_slot_exist(index: int) -> bool: var path = SAVE_DIR + "s" + str(index) + SAVE_EXTENSION_NAME @@ -94,6 +126,7 @@ func write_slot(index: int) -> void: thread_request.type = ThreadRequestType.WRITE_SLOT thread_request.slot_id = index + thread_request.is_slot_auto = false thread_semaphore.post() ## Loads data a specific save slot index, and emits "load_slot_complete" when successful @@ -104,6 +137,7 @@ func read_slot(index: int) -> void: thread_request.type = ThreadRequestType.READ_SLOT thread_request.slot_id = index + thread_request.is_slot_auto = false thread_semaphore.post() ## Checks if the common save exists in save directory @@ -135,8 +169,15 @@ func read_common() -> void: ## Not intended for the end user. ## Functionality for save slot writing, handled on a seperate thread -func _write_slot_thread_func(index: int) -> void: - if _write_backend("s%s" % [str(index)], SaveHolder.slot): +func _write_slot_thread_func(index: int, is_auto_slot: bool) -> void: + # Get file name to write from + var file_name: String = "" + if is_auto_slot: + file_name = "s%s" % [SAVE_AUTO_NAME] + else: + file_name = "s%s" % [str(index)] + + if _write_backend(file_name, SaveHolder.slot): # Tell the signal that the save is finished successfully call_deferred("emit_signal", "save_slot_complete") else: @@ -144,9 +185,16 @@ func _write_slot_thread_func(index: int) -> void: ## Not intended for the end user. ## Functionality for save slot reading, handled on a seperate thread -func _read_slot_thread_func(index: int) -> void: - # Get dictionary from file in save directory - var dict: Dictionary = _read_backend_by_name("s%s" % [str(index)]) +func _read_slot_thread_func(index: int, is_auto_slot: bool) -> void: + # Get file name to read from + var file_name: String = "" + if is_auto_slot: + file_name = "s%s" % [SAVE_AUTO_NAME] + else: + file_name = "s%s" % [str(index)] + + # Get dictionary from file + var dict: Dictionary = _read_backend_by_name(file_name) if dict.is_empty(): call_deferred("emit_signal", "load_error") return @@ -248,7 +296,7 @@ func _read_backend(path: String) -> Dictionary: # Print message saying that the dictionary is empty if needed if data.is_empty(): - printerr("File at %s was parsed correctly, but contains no data" % [path]) + push_warning("File at %s was parsed correctly, but contains no data" % [path]) # Return the JSON data to then be converted into a object later return data @@ -273,7 +321,7 @@ func _read_backend_raw_data(path: String) -> String: # Print message saying that the dictionary is empty if needed if content.is_empty(): - printerr("File at %s was parsed correctly, but contains no data" % [path]) + push_warning("File at %s was parsed correctly, but contains no data" % [path]) # Return the JSON data to then be converted into a object later return content @@ -295,6 +343,7 @@ enum ThreadRequestType { class ThreadRequest: var type: ThreadRequestType = ThreadRequestType.UNKNOWN var slot_id: int = 0 + var is_slot_auto: bool = false ## The thread used for reading/writting save data var thread: Thread @@ -330,9 +379,9 @@ func _thread_func(): match(thread_request.type): ThreadRequestType.WRITE_SLOT: - _write_slot_thread_func(thread_request.slot_id) + _write_slot_thread_func(thread_request.slot_id, thread_request.is_slot_auto) ThreadRequestType.READ_SLOT: - _read_slot_thread_func(thread_request.slot_id) + _read_slot_thread_func(thread_request.slot_id, thread_request.is_slot_auto) ThreadRequestType.WRITE_COMMON: _write_common_thread_func() ThreadRequestType.READ_COMMON: diff --git a/addons/savedata-dx/view/save_view.tscn b/addons/savedata-dx/view/save_view.tscn index 21da6b6..23bd4ca 100644 --- a/addons/savedata-dx/view/save_view.tscn +++ b/addons/savedata-dx/view/save_view.tscn @@ -135,13 +135,13 @@ vertical_alignment = 1 unique_name_in_owner = true layout_mode = 2 size_flags_vertical = 3 -highlight_all_occurrences = true -highlight_current_line = true -draw_tabs = true -syntax_highlighter = ExtResource("2_cnvvi") minimap_draw = true minimap_width = 60 caret_blink = true +syntax_highlighter = ExtResource("2_cnvvi") +highlight_all_occurrences = true +highlight_current_line = true +draw_tabs = true gutters_draw_line_numbers = true auto_brace_completion_enabled = true auto_brace_completion_highlight_matching = true diff --git a/example/example_scene.gd b/example/example_scene.gd index dd09280..79b6624 100644 --- a/example/example_scene.gd +++ b/example/example_scene.gd @@ -23,6 +23,12 @@ func _on_button_save_pressed(): func _on_button_load_pressed(): SaveAccessor.read_active_slot() +func _on_autosave_button_save_pressed(): + SaveAccessor.write_autosave_slot() + +func _on_autosave_button_load_pressed(): + SaveAccessor.read_autosave_slot() + func _on_slot_saved(): %SignalNotif.text = "Signal: Save Completed" diff --git a/example/example_scene.tscn b/example/example_scene.tscn index 82cdcee..1cf84a9 100644 --- a/example/example_scene.tscn +++ b/example/example_scene.tscn @@ -58,9 +58,7 @@ alignment = 1 [node name="SpinBox" type="SpinBox" parent="Panel/Margin/Header/SaveLoad"] layout_mode = 2 size_flags_horizontal = 3 -min_value = 1.0 max_value = 5.0 -value = 1.0 rounded = true prefix = "Save Slot" @@ -76,6 +74,20 @@ layout_mode = 2 size_flags_horizontal = 3 text = "Load" +[node name="SaveLoadAuto" type="HBoxContainer" parent="Panel/Margin/Header"] +layout_mode = 2 +alignment = 1 + +[node name="AutosaveButtonSave" type="Button" parent="Panel/Margin/Header/SaveLoadAuto"] +layout_mode = 2 +size_flags_horizontal = 3 +text = "Save to Autosave" + +[node name="AutosaveButtonLoad" type="Button" parent="Panel/Margin/Header/SaveLoadAuto"] +layout_mode = 2 +size_flags_horizontal = 3 +text = "Load from Autosave" + [node name="HSeparator2" type="HSeparator" parent="Panel/Margin/Header"] layout_mode = 2 theme_override_constants/separation = 25 @@ -99,4 +111,6 @@ theme_override_constants/separation = 25 [connection signal="value_changed" from="Panel/Margin/Header/SaveLoad/SpinBox" to="." method="_on_spin_box_value_changed"] [connection signal="pressed" from="Panel/Margin/Header/SaveLoad/ButtonSave" to="." method="_on_button_save_pressed"] [connection signal="pressed" from="Panel/Margin/Header/SaveLoad/ButtonLoad" to="." method="_on_button_load_pressed"] +[connection signal="pressed" from="Panel/Margin/Header/SaveLoadAuto/AutosaveButtonSave" to="." method="_on_autosave_button_save_pressed"] +[connection signal="pressed" from="Panel/Margin/Header/SaveLoadAuto/AutosaveButtonLoad" to="." method="_on_autosave_button_load_pressed"] [connection signal="value_changed" from="Panel/Margin/Header/ProgressSlider" to="." method="_on_h_slider_changed"] From eb3f1b3efe900074dd78bbc1c680e06310e28d63 Mon Sep 17 00:00:00 2001 From: Amethyst-szs Date: Wed, 13 Dec 2023 07:24:17 -0800 Subject: [PATCH 19/30] Ensure sv, slot, and common folders in editor --- addons/savedata-dx/savedata-dx.gd | 5 +++++ addons/savedata-dx/view/save_view.gd | 7 ++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/addons/savedata-dx/savedata-dx.gd b/addons/savedata-dx/savedata-dx.gd index c6095f4..39c5a81 100644 --- a/addons/savedata-dx/savedata-dx.gd +++ b/addons/savedata-dx/savedata-dx.gd @@ -5,6 +5,11 @@ const SaveView = preload("./view/save_view.tscn") var save_view func _enter_tree(): + # Ensure various folders + DirAccess.make_dir_recursive_absolute(SaveAccessorPlugin.SAVE_DIR) + DirAccess.make_dir_recursive_absolute("res://addons/savedata-dx/slot/") + DirAccess.make_dir_recursive_absolute("res://addons/savedata-dx/common/") + # Setup singletons for accessing and holding save data add_autoload_singleton("SaveAccessor", "res://addons/savedata-dx/backend/save_accessor.gd") add_autoload_singleton("SaveHolder", "res://addons/savedata-dx/backend/save_holder.gd") diff --git a/addons/savedata-dx/view/save_view.gd b/addons/savedata-dx/view/save_view.gd index 2560dc6..702c647 100644 --- a/addons/savedata-dx/view/save_view.gd +++ b/addons/savedata-dx/view/save_view.gd @@ -66,7 +66,12 @@ var editor_plugin: EditorPlugin func _ready() -> void: if not is_instance_valid(editor_plugin): return - + + # Ensure various folders + DirAccess.make_dir_recursive_absolute(SaveAccessorPlugin.SAVE_DIR) + DirAccess.make_dir_recursive_absolute("res://addons/savedata-dx/slot/") + DirAccess.make_dir_recursive_absolute("res://addons/savedata-dx/common/") + apply_theme() _on_edit_mode_selected(EditModeType.INSPECTOR) From a356e0d08d5065b82eb2af346d2cab67bc60d196 Mon Sep 17 00:00:00 2001 From: Amethyst-szs Date: Wed, 13 Dec 2023 08:16:57 -0800 Subject: [PATCH 20/30] Improved consistency with access thread signals --- addons/savedata-dx/backend/save_accessor.gd | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/addons/savedata-dx/backend/save_accessor.gd b/addons/savedata-dx/backend/save_accessor.gd index b81139c..c11cd80 100644 --- a/addons/savedata-dx/backend/save_accessor.gd +++ b/addons/savedata-dx/backend/save_accessor.gd @@ -355,7 +355,14 @@ var thread_semaphore: Semaphore var thread_request: ThreadRequest = ThreadRequest.new() ## Is the thread currently busy -var is_thread_busy: bool = false +var is_thread_busy: bool = false: + set(value): + is_thread_busy = value + + if value: + call_deferred("emit_signal", "thread_busy") + else: + call_deferred("emit_signal", "thread_complete") ## Should the thread be terminated var is_thread_terminate: bool = false @@ -363,8 +370,6 @@ var is_thread_terminate: bool = false func _ready(): # Setup thread and and its components thread_semaphore = Semaphore.new() - is_thread_busy = false - is_thread_terminate = false thread = Thread.new() thread.start(_thread_func, Thread.PRIORITY_NORMAL) @@ -375,20 +380,18 @@ func _thread_func(): if is_thread_terminate: break is_thread_busy = true - call_deferred("emit_signal", "thread_busy") match(thread_request.type): ThreadRequestType.WRITE_SLOT: - _write_slot_thread_func(thread_request.slot_id, thread_request.is_slot_auto) + await _write_slot_thread_func(thread_request.slot_id, thread_request.is_slot_auto) ThreadRequestType.READ_SLOT: - _read_slot_thread_func(thread_request.slot_id, thread_request.is_slot_auto) + await _read_slot_thread_func(thread_request.slot_id, thread_request.is_slot_auto) ThreadRequestType.WRITE_COMMON: - _write_common_thread_func() + await _write_common_thread_func() ThreadRequestType.READ_COMMON: - _read_common_thread_func() + await _read_common_thread_func() is_thread_busy = false - call_deferred("emit_signal", "thread_complete") if is_thread_terminate: break From ab671b1459dadd18063d03507a7d3f09889d50cd Mon Sep 17 00:00:00 2001 From: Amethyst-szs Date: Mon, 18 Dec 2023 09:17:46 -0800 Subject: [PATCH 21/30] Early WIP 2.0 --- addons/savedata-dx/plugin.cfg | 4 +- .../savedata-dx/{savedata-dx.gd => plugin.gd} | 2 +- addons/savedata-dx/view/save_view.gd | 167 ++++-------------- addons/savedata-dx/view/save_view.tscn | 90 ---------- 4 files changed, 33 insertions(+), 230 deletions(-) rename addons/savedata-dx/{savedata-dx.gd => plugin.gd} (98%) diff --git a/addons/savedata-dx/plugin.cfg b/addons/savedata-dx/plugin.cfg index 56c59e2..e2f6bb6 100644 --- a/addons/savedata-dx/plugin.cfg +++ b/addons/savedata-dx/plugin.cfg @@ -3,5 +3,5 @@ name="SaveDataDX" description="A plugin combining the benefits of JSON & Resource saving, making save data easy, fast, and secure" author="Amethyst-szs" -version="1.2.1" -script="savedata-dx.gd" +version="2.0.0" +script="plugin.gd" diff --git a/addons/savedata-dx/savedata-dx.gd b/addons/savedata-dx/plugin.gd similarity index 98% rename from addons/savedata-dx/savedata-dx.gd rename to addons/savedata-dx/plugin.gd index 39c5a81..be9033c 100644 --- a/addons/savedata-dx/savedata-dx.gd +++ b/addons/savedata-dx/plugin.gd @@ -41,7 +41,7 @@ func _make_visible(visible): save_view.visible = visible func _get_plugin_name(): - return "SaveData" + return "Saves" func _get_plugin_icon(): return get_editor_interface().get_base_control().get_theme_icon("Save", "EditorIcons") diff --git a/addons/savedata-dx/view/save_view.gd b/addons/savedata-dx/view/save_view.gd index 702c647..1c5b789 100644 --- a/addons/savedata-dx/view/save_view.gd +++ b/addons/savedata-dx/view/save_view.gd @@ -1,18 +1,20 @@ @tool extends Control +#region Constants and Node References + # SaveData accessor script const accessor_script = preload("res://addons/savedata-dx/backend/save_accessor.gd") var accessor_inst = accessor_script.new() +# Reference to editor plugin +var editor_plugin: EditorPlugin + # Constants const root_slot_path: String = "res://addons/savedata-dx/data_slot.gd" const root_common_path: String = "res://addons/savedata-dx/data_common.gd" # Header buttons -@onready var head_button_edit_mode := %HeadEditMode -@onready var head_button_add := %HeadAdd -@onready var head_button_new := %HeadNew @onready var head_button_load := %HeadLoad @onready var head_button_save := %HeadSave @onready var head_button_info := %HeadInfo @@ -27,12 +29,10 @@ const root_common_path: String = "res://addons/savedata-dx/data_common.gd" # Dialog popups @onready var inspector_file_dialog := $InspectorFileDialog @onready var inspector_save_fail_dialog := $InspectorSaveFailDialog -@onready var slot_new_file_dialog := $SlotNewFileDialog -@onready var slot_load_file_dialog := $SlotLoadFileDialog -@onready var slot_import_file_dialog := $SlotImportFileDialog -@onready var common_new_file_dialog := $CommonNewFileDialog -@onready var common_load_file_dialog := $CommonLoadFileDialog -@onready var common_import_file_dialog := $CommonImportFileDialog + +#endregion + +#region State Variables # Current editor information var open_file_path: String: @@ -53,15 +53,9 @@ var unsaved_changes: bool = false: get: return unsaved_changes -var edit_mode: EditModeType -enum EditModeType { - INSPECTOR, - SLOT, - COMMON -} +#endregion -# Reference to editor plugin -var editor_plugin: EditorPlugin +#region Virtual Functions func _ready() -> void: if not is_instance_valid(editor_plugin): @@ -73,7 +67,6 @@ func _ready() -> void: DirAccess.make_dir_recursive_absolute("res://addons/savedata-dx/common/") apply_theme() - _on_edit_mode_selected(EditModeType.INSPECTOR) func _input(event: InputEvent) -> void: if not visible: return @@ -88,64 +81,18 @@ func _input(event: InputEvent) -> void: _on_head_load_pressed() "Ctrl+N", "Command+N": get_viewport().set_input_as_handled() - _on_head_new_pressed() "Ctrl+W", "Command+W": get_viewport().set_input_as_handled() _on_head_close_pressed() "Ctrl+I", "Command+I": get_viewport().set_input_as_handled() - _on_head_add_pressed() -# Common utility functions -func setup_slot_mode() -> void: - # Open the root of the slot script in code editor - code_editor_open_file(root_slot_path) - - # Toggle button activeness - head_button_new.visible = true - head_button_add.visible = true - -func setup_common_mode() -> void: - # Open the root of the common script in code editor - code_editor_open_file(root_common_path) - - # Toggle button activeness - head_button_new.visible = true - head_button_add.visible = true - -func setup_inspector_mode() -> void: - # Toggle button activeness - head_button_new.visible = false - head_button_add.visible = false - -# Update the interface when the edit mode is changed -func _on_edit_mode_selected(index: int) -> void: - # Copy selected into into current edit mode - head_button_edit_mode.selected = index - edit_mode = index - - # Reset the code editor panel - code_editor_close() - head_button_close.disabled = true - - # Configure depending on selected option in menu - match(index): - EditModeType.SLOT: setup_slot_mode() - EditModeType.COMMON: setup_common_mode() - EditModeType.INSPECTOR: setup_inspector_mode() +#endregion -# Header button functions - -func _on_head_new_pressed(): - match edit_mode: - EditModeType.SLOT: slot_new_file_dialog.popup() - EditModeType.COMMON: common_new_file_dialog.popup() +#region Header Button Interactions func _on_head_load_pressed(): - match edit_mode: - EditModeType.SLOT: slot_load_file_dialog.popup() - EditModeType.COMMON: common_load_file_dialog.popup() - EditModeType.INSPECTOR: inspector_file_dialog.popup() + inspector_file_dialog.popup() func _on_head_save_pressed(): if open_file_path.is_empty(): @@ -154,48 +101,23 @@ func _on_head_save_pressed(): unsaved_changes = false # If in inspector mode, verify user input and then write to disk - if edit_mode == EditModeType.INSPECTOR: - var test_parse = JSON.parse_string(code_editor.text) - if test_parse == null: - inspector_save_fail_dialog.popup() - return - - accessor_inst._write_backend_with_json_string(open_file_path, code_editor.text) - else: # If this isn't inspector mode, write script to disk normally - code_editor_save_script() - -func _on_head_add_pressed(): - match edit_mode: - EditModeType.SLOT: slot_import_file_dialog.popup() - EditModeType.COMMON: common_import_file_dialog.popup() + var test_parse = JSON.parse_string(code_editor.text) + if test_parse == null: + inspector_save_fail_dialog.popup() + return + + accessor_inst._write_backend_with_json_string(open_file_path, code_editor.text) func _on_head_info_pressed(): OS.shell_open("https://github.com/Amethyst-szs/godot-savedata-dx/wiki") func _on_head_close_pressed(): - _on_edit_mode_selected(edit_mode) - -# Slot and Common mode functions - -func _on_slot_new_file_dialog(path: String) -> void: - open_file_path = path - head_button_close.disabled = false - code_editor_open() - -func _on_slot_load_file_dialog(path: String) -> void: - head_button_close.disabled = false - code_editor_open_file(path) + code_editor_close() + head_button_close.disabled = true -func _on_slot_import_file_dialog(path: String) -> void: - _on_code_edit_text_changed() - - # Create constant name - var path_end: int = path.rfind("/") + 1 - var name: String = path.substr(path_end).replacen(".", "_") - - code_editor.text = "const %s = preload(\"%s\")\n\n%s" % [name, path, code_editor.text] +#endregion -# Inspector mode functions +#region Decrypt Save Data # Convert file path to dictionary func decrypt_save(path: String) -> String: @@ -216,6 +138,10 @@ func _on_inspector_select_file(path: String) -> void: code_editor_open() code_editor.text = data +#endregion + +#region Utility + # Code editor management func _on_code_edit_text_changed(): @@ -223,10 +149,6 @@ func _on_code_edit_text_changed(): $CompileTimer.start() func _on_compile_timer_timeout(): - # Only needs to test compiling for the inspector mode - if not edit_mode == EditModeType.INSPECTOR: - return - # Try parsing the JSON var json: JSON = JSON.new() var test_parse: Error = json.parse(code_editor.text) @@ -297,25 +219,12 @@ func code_editor_open_file(path: String) -> void: code_editor.clear_undo_history() -func code_editor_save_script() -> void: - # Attempt to open new file and print an error if it fails - var file = FileAccess.open(open_file_path, FileAccess.WRITE) - if open_file_path == null: - printerr("FileAccess open error: " + str(FileAccess.get_open_error())) - return - - # Write data to disk - file.store_string("extends RefCounted\n" + code_editor.text) - file.close() - func apply_theme() -> void: if is_instance_valid(editor_plugin) and is_instance_valid(code_editor): var scale: float = editor_plugin.get_editor_interface().get_editor_scale() var set = editor_plugin.get_editor_interface().get_editor_settings() var highlight: CodeHighlighter = code_editor.syntax_highlighter - head_button_add.icon = get_theme_icon("Add", "EditorIcons") - head_button_new.icon = get_theme_icon("New", "EditorIcons") head_button_load.icon = get_theme_icon("Load", "EditorIcons") head_button_save.icon = get_theme_icon("Save", "EditorIcons") head_button_info.icon = get_theme_icon("Help", "EditorIcons") @@ -326,21 +235,5 @@ func apply_theme() -> void: highlight.symbol_color = set.get_setting("text_editor/theme/highlighting/symbol_color") highlight.function_color = set.get_setting("text_editor/theme/highlighting/function_color") highlight.member_variable_color = set.get_setting("text_editor/theme/highlighting/member_variable_color") - - highlight.add_keyword_color("var", set.get_setting("text_editor/theme/highlighting/keyword_color")) - highlight.add_keyword_color("const", set.get_setting("text_editor/theme/highlighting/keyword_color")) - - highlight.add_keyword_color("bool", set.get_setting("text_editor/theme/highlighting/base_type_color")) - highlight.add_keyword_color("int", set.get_setting("text_editor/theme/highlighting/base_type_color")) - highlight.add_keyword_color("float", set.get_setting("text_editor/theme/highlighting/base_type_color")) - highlight.add_keyword_color("String", set.get_setting("text_editor/theme/highlighting/base_type_color")) - highlight.add_keyword_color("Vector2", set.get_setting("text_editor/theme/highlighting/base_type_color")) - highlight.add_keyword_color("Vector2i", set.get_setting("text_editor/theme/highlighting/base_type_color")) - highlight.add_keyword_color("Vector2i", set.get_setting("text_editor/theme/highlighting/base_type_color")) - highlight.add_keyword_color("Vector3", set.get_setting("text_editor/theme/highlighting/base_type_color")) - highlight.add_keyword_color("Vector3i", set.get_setting("text_editor/theme/highlighting/base_type_color")) - highlight.add_keyword_color("Vector4", set.get_setting("text_editor/theme/highlighting/base_type_color")) - highlight.add_keyword_color("Vector4i", set.get_setting("text_editor/theme/highlighting/base_type_color")) - highlight.add_keyword_color("Quaternion", set.get_setting("text_editor/theme/highlighting/base_type_color")) - highlight.add_keyword_color("Color", set.get_setting("text_editor/theme/highlighting/base_type_color")) - highlight.add_keyword_color("Array", set.get_setting("text_editor/theme/highlighting/base_type_color")) + +#endregion diff --git a/addons/savedata-dx/view/save_view.tscn b/addons/savedata-dx/view/save_view.tscn index 23bd4ca..d303be8 100644 --- a/addons/savedata-dx/view/save_view.tscn +++ b/addons/savedata-dx/view/save_view.tscn @@ -51,36 +51,6 @@ layout_mode = 2 size_flags_vertical = 4 theme_override_constants/separation = 3 -[node name="Label" type="Label" parent="Margin/VerticalSplit/Header/VBox/Top"] -layout_mode = 2 -text = "Mode:" -horizontal_alignment = 2 -vertical_alignment = 1 - -[node name="HeadEditMode" type="OptionButton" parent="Margin/VerticalSplit/Header/VBox/Top"] -unique_name_in_owner = true -layout_mode = 2 -expand_icon = true -item_count = 3 -selected = 0 -popup/item_0/text = "Inspector" -popup/item_0/id = 0 -popup/item_1/text = "Slot" -popup/item_1/id = 1 -popup/item_2/text = "Common" -popup/item_2/id = 2 - -[node name="VSeparator" type="VSeparator" parent="Margin/VerticalSplit/Header/VBox/Top"] -layout_mode = 2 -theme_override_constants/separation = 15 - -[node name="HeadNew" type="Button" parent="Margin/VerticalSplit/Header/VBox/Top"] -unique_name_in_owner = true -layout_mode = 2 -tooltip_text = "Create new resource -(Ctrl+N)" -text = " New" - [node name="HeadLoad" type="Button" parent="Margin/VerticalSplit/Header/VBox/Top"] unique_name_in_owner = true layout_mode = 2 @@ -95,13 +65,6 @@ tooltip_text = "Save data to disk (Ctrl+S)" text = " Save" -[node name="HeadAdd" type="Button" parent="Margin/VerticalSplit/Header/VBox/Top"] -unique_name_in_owner = true -layout_mode = 2 -tooltip_text = "Import another script using preload -(Ctrl+I)" -text = " Import" - [node name="HeadInfo" type="Button" parent="Margin/VerticalSplit/Header/VBox/Top"] unique_name_in_owner = true layout_mode = 2 @@ -177,63 +140,10 @@ dialog_text = "The save slot open in the inspector could not be saved! Check the error display for additional info" dialog_autowrap = true -[node name="SlotNewFileDialog" type="FileDialog" parent="."] -title = "Input new file name" -initial_position = 2 -size = Vector2i(500, 600) -root_subfolder = "res://addons/savedata-dx/slot/" - -[node name="SlotLoadFileDialog" type="FileDialog" parent="."] -title = "Open a File" -initial_position = 2 -size = Vector2i(500, 600) -ok_button_text = "Open" -file_mode = 0 -root_subfolder = "res://addons/savedata-dx/slot/" - -[node name="SlotImportFileDialog" type="FileDialog" parent="."] -title = "Open a File" -initial_position = 2 -size = Vector2i(500, 600) -ok_button_text = "Open" -file_mode = 0 -root_subfolder = "res://addons/savedata-dx/slot/" - -[node name="CommonNewFileDialog" type="FileDialog" parent="."] -title = "Input new file name" -initial_position = 2 -size = Vector2i(500, 600) -root_subfolder = "res://addons/savedata-dx/common/" - -[node name="CommonLoadFileDialog" type="FileDialog" parent="."] -title = "Open a File" -initial_position = 2 -size = Vector2i(500, 600) -ok_button_text = "Open" -file_mode = 0 -root_subfolder = "res://addons/savedata-dx/common/" - -[node name="CommonImportFileDialog" type="FileDialog" parent="."] -title = "Open a File" -initial_position = 2 -size = Vector2i(500, 600) -ok_button_text = "Open" -file_mode = 0 -root_subfolder = "res://addons/savedata-dx/common/" - [connection signal="timeout" from="CompileTimer" to="." method="_on_compile_timer_timeout"] -[connection signal="item_selected" from="Margin/VerticalSplit/Header/VBox/Top/HeadEditMode" to="." method="_on_edit_mode_selected"] -[connection signal="pressed" from="Margin/VerticalSplit/Header/VBox/Top/HeadNew" to="." method="_on_head_new_pressed"] [connection signal="pressed" from="Margin/VerticalSplit/Header/VBox/Top/HeadLoad" to="." method="_on_head_load_pressed"] [connection signal="pressed" from="Margin/VerticalSplit/Header/VBox/Top/HeadSave" to="." method="_on_head_save_pressed"] -[connection signal="pressed" from="Margin/VerticalSplit/Header/VBox/Top/HeadAdd" to="." method="_on_head_add_pressed"] [connection signal="pressed" from="Margin/VerticalSplit/Header/VBox/Top/HeadInfo" to="." method="_on_head_info_pressed"] [connection signal="pressed" from="Margin/VerticalSplit/Header/VBox/Bottom/HeadClose" to="." method="_on_head_close_pressed"] [connection signal="text_changed" from="Margin/VerticalSplit/CodeEdit" to="." method="_on_code_edit_text_changed"] [connection signal="file_selected" from="InspectorFileDialog" to="." method="_on_inspector_select_file"] -[connection signal="file_selected" from="SlotNewFileDialog" to="." method="_on_slot_new_file_dialog"] -[connection signal="file_selected" from="SlotLoadFileDialog" to="." method="_on_slot_load_file_dialog"] -[connection signal="file_selected" from="SlotImportFileDialog" to="." method="_on_slot_import_file_dialog"] -[connection signal="file_selected" from="CommonNewFileDialog" to="." method="_on_slot_new_file_dialog"] -[connection signal="file_selected" from="CommonLoadFileDialog" to="." method="_on_slot_load_file_dialog"] -[connection signal="file_selected" from="CommonImportFileDialog" to="." method="_on_slot_import_file_dialog"] From 9405c69a81b23ea0d24ede3b2babe4b6d46d8aa0 Mon Sep 17 00:00:00 2001 From: Amethyst-szs Date: Mon, 18 Dec 2023 09:40:30 -0800 Subject: [PATCH 22/30] Added "Edit Slot" and "Edit Common" buttons --- addons/savedata-dx/data_slot.gd | 2 +- addons/savedata-dx/view/save_view.gd | 32 ++++++++++++++------------ addons/savedata-dx/view/save_view.tscn | 14 +++++++++++ 3 files changed, 32 insertions(+), 16 deletions(-) diff --git a/addons/savedata-dx/data_slot.gd b/addons/savedata-dx/data_slot.gd index 4e0ec80..3e5408e 100644 --- a/addons/savedata-dx/data_slot.gd +++ b/addons/savedata-dx/data_slot.gd @@ -1,2 +1,2 @@ extends RefCounted -var progress = 50 \ No newline at end of file +var progress = 50 diff --git a/addons/savedata-dx/view/save_view.gd b/addons/savedata-dx/view/save_view.gd index 1c5b789..7094ce5 100644 --- a/addons/savedata-dx/view/save_view.gd +++ b/addons/savedata-dx/view/save_view.gd @@ -17,6 +17,8 @@ const root_common_path: String = "res://addons/savedata-dx/data_common.gd" # Header buttons @onready var head_button_load := %HeadLoad @onready var head_button_save := %HeadSave +@onready var head_button_edit_slot := %HeadEditSlot +@onready var head_button_edit_common := %HeadEditCommon @onready var head_button_info := %HeadInfo @onready var head_button_close := %HeadClose @onready var head_file_name := %OpenFileTextLabel @@ -70,7 +72,7 @@ func _ready() -> void: func _input(event: InputEvent) -> void: if not visible: return - + if event is InputEventKey and event.is_pressed(): match event.as_text(): "Ctrl+S", "Command+S": @@ -79,13 +81,9 @@ func _input(event: InputEvent) -> void: "Ctrl+O", "Command+O": get_viewport().set_input_as_handled() _on_head_load_pressed() - "Ctrl+N", "Command+N": - get_viewport().set_input_as_handled() "Ctrl+W", "Command+W": get_viewport().set_input_as_handled() _on_head_close_pressed() - "Ctrl+I", "Command+I": - get_viewport().set_input_as_handled() #endregion @@ -98,16 +96,25 @@ func _on_head_save_pressed(): if open_file_path.is_empty(): return - unsaved_changes = false - # If in inspector mode, verify user input and then write to disk var test_parse = JSON.parse_string(code_editor.text) if test_parse == null: inspector_save_fail_dialog.popup() return + unsaved_changes = false accessor_inst._write_backend_with_json_string(open_file_path, code_editor.text) +func _on_head_edit_slot_pressed(): + var script: Script = load("res://addons/savedata-dx/data_slot.gd") + EditorInterface.edit_script(script) + EditorInterface.set_main_screen_editor("Script") + +func _on_head_edit_common_pressed(): + var script: Script = load("res://addons/savedata-dx/data_common.gd") + EditorInterface.edit_script(script) + EditorInterface.set_main_screen_editor("Script") + func _on_head_info_pressed(): OS.shell_open("https://github.com/Amethyst-szs/godot-savedata-dx/wiki") @@ -119,13 +126,9 @@ func _on_head_close_pressed(): #region Decrypt Save Data -# Convert file path to dictionary -func decrypt_save(path: String) -> String: - return accessor_inst._read_backend_raw_data(path) - # Called upon selecting a file in the debugger mode func _on_inspector_select_file(path: String) -> void: - var data: String = decrypt_save(path) + var data: String = accessor_inst._read_backend_raw_data(path) if data.is_empty(): code_editor_close("Save data could not be opened, check output for more info") return @@ -142,8 +145,6 @@ func _on_inspector_select_file(path: String) -> void: #region Utility -# Code editor management - func _on_code_edit_text_changed(): unsaved_changes = true $CompileTimer.start() @@ -158,7 +159,6 @@ func _on_compile_timer_timeout(): head_button_save.disabled = true code_error_footer.visible = true code_error_text.text = "Error on Line %s: %s" % [str(json.get_error_line()), json.get_error_message()] - print() return code_error_footer.visible = false @@ -227,6 +227,8 @@ func apply_theme() -> void: head_button_load.icon = get_theme_icon("Load", "EditorIcons") head_button_save.icon = get_theme_icon("Save", "EditorIcons") + head_button_edit_slot.icon = get_theme_icon("Edit", "EditorIcons") + head_button_edit_common.icon = get_theme_icon("EditInternal", "EditorIcons") head_button_info.icon = get_theme_icon("Help", "EditorIcons") head_button_close.icon = get_theme_icon("Back", "EditorIcons") diff --git a/addons/savedata-dx/view/save_view.tscn b/addons/savedata-dx/view/save_view.tscn index d303be8..f82a4f2 100644 --- a/addons/savedata-dx/view/save_view.tscn +++ b/addons/savedata-dx/view/save_view.tscn @@ -65,6 +65,18 @@ tooltip_text = "Save data to disk (Ctrl+S)" text = " Save" +[node name="HeadEditSlot" type="Button" parent="Margin/VerticalSplit/Header/VBox/Top"] +unique_name_in_owner = true +layout_mode = 2 +tooltip_text = "Opens the slot save file in the Script Editor" +text = "Edit Slot" + +[node name="HeadEditCommon" type="Button" parent="Margin/VerticalSplit/Header/VBox/Top"] +unique_name_in_owner = true +layout_mode = 2 +tooltip_text = "Opens the common save in the Script Editor" +text = "Edit Common" + [node name="HeadInfo" type="Button" parent="Margin/VerticalSplit/Header/VBox/Top"] unique_name_in_owner = true layout_mode = 2 @@ -143,6 +155,8 @@ dialog_autowrap = true [connection signal="timeout" from="CompileTimer" to="." method="_on_compile_timer_timeout"] [connection signal="pressed" from="Margin/VerticalSplit/Header/VBox/Top/HeadLoad" to="." method="_on_head_load_pressed"] [connection signal="pressed" from="Margin/VerticalSplit/Header/VBox/Top/HeadSave" to="." method="_on_head_save_pressed"] +[connection signal="pressed" from="Margin/VerticalSplit/Header/VBox/Top/HeadEditSlot" to="." method="_on_head_edit_slot_pressed"] +[connection signal="pressed" from="Margin/VerticalSplit/Header/VBox/Top/HeadEditCommon" to="." method="_on_head_edit_common_pressed"] [connection signal="pressed" from="Margin/VerticalSplit/Header/VBox/Top/HeadInfo" to="." method="_on_head_info_pressed"] [connection signal="pressed" from="Margin/VerticalSplit/Header/VBox/Bottom/HeadClose" to="." method="_on_head_close_pressed"] [connection signal="text_changed" from="Margin/VerticalSplit/CodeEdit" to="." method="_on_code_edit_text_changed"] From 0934833e2571d743f3d72d3809ce64c9b3f0ca05 Mon Sep 17 00:00:00 2001 From: Amethyst-szs Date: Mon, 18 Dec 2023 10:28:29 -0800 Subject: [PATCH 23/30] Added search box to save editor --- addons/savedata-dx/view/code_edit.gd | 26 +++++++++ addons/savedata-dx/view/save_view.gd | 77 +++++++++++++++++++++++--- addons/savedata-dx/view/save_view.tscn | 71 ++++++++++++++++-------- 3 files changed, 142 insertions(+), 32 deletions(-) create mode 100644 addons/savedata-dx/view/code_edit.gd diff --git a/addons/savedata-dx/view/code_edit.gd b/addons/savedata-dx/view/code_edit.gd new file mode 100644 index 0000000..6e061b6 --- /dev/null +++ b/addons/savedata-dx/view/code_edit.gd @@ -0,0 +1,26 @@ +@tool +extends CodeEdit + +var highlight_color: Color = Color.BLACK + +func _ready(): + var settings = EditorInterface.get_editor_settings() + highlight_color = settings.get_setting("text_editor/theme/highlighting/background_color") + highlight_color = highlight_color.lightened(0.15) + +func setup_search_highlight(search: String): + var index_list: Array[int] = [] + + for line in range(get_line_count()): + var text: String = get_line(line) + if text.findn(search) != -1: + index_list.push_back(line) + set_line_background_color(line, highlight_color) + else: + set_line_background_color(line, Color(0, 0, 0, 0)) + + return index_list + +func remove_search_highlight(): + for line in range(get_line_count()): + set_line_background_color(line, Color(0, 0, 0, 0)) diff --git a/addons/savedata-dx/view/save_view.gd b/addons/savedata-dx/view/save_view.gd index 7094ce5..ae07801 100644 --- a/addons/savedata-dx/view/save_view.gd +++ b/addons/savedata-dx/view/save_view.gd @@ -14,6 +14,8 @@ var editor_plugin: EditorPlugin const root_slot_path: String = "res://addons/savedata-dx/data_slot.gd" const root_common_path: String = "res://addons/savedata-dx/data_common.gd" +const max_search_results: int = 50 + # Header buttons @onready var head_button_load := %HeadLoad @onready var head_button_save := %HeadSave @@ -21,7 +23,10 @@ const root_common_path: String = "res://addons/savedata-dx/data_common.gd" @onready var head_button_edit_common := %HeadEditCommon @onready var head_button_info := %HeadInfo @onready var head_button_close := %HeadClose -@onready var head_file_name := %OpenFileTextLabel + +# Search window +@onready var search_box := %SearchBox +@onready var search_result_box := %SearchResultBox # Code editor @onready var code_editor := %CodeEdit @@ -37,12 +42,7 @@ const root_common_path: String = "res://addons/savedata-dx/data_common.gd" #region State Variables # Current editor information -var open_file_path: String: - set (value): - head_file_name.text = value - open_file_path = value - get: - return open_file_path +var open_file_path: String var unsaved_changes: bool = false: set (value): @@ -52,8 +52,6 @@ var unsaved_changes: bool = false: head_button_save.text = " Save" unsaved_changes = value - get: - return unsaved_changes #endregion @@ -63,6 +61,9 @@ func _ready() -> void: if not is_instance_valid(editor_plugin): return + # Disable save button + code_editor_close() + # Ensure various folders DirAccess.make_dir_recursive_absolute(SaveAccessorPlugin.SAVE_DIR) DirAccess.make_dir_recursive_absolute("res://addons/savedata-dx/slot/") @@ -143,6 +144,58 @@ func _on_inspector_select_file(path: String) -> void: #endregion +#region JSON Searching + +func update_text_search(text: String = ""): + # Clear out the list of search results + search_box.clear() + + for child in search_result_box.get_children(): + search_result_box.remove_child(child) + + if open_file_path.is_empty() or text.is_empty(): + code_editor.remove_search_highlight() + return + + var index_list: Array[int] = code_editor.setup_search_highlight(text) + + # Ensure there is at least one index in list + if index_list.is_empty(): + return + + # Load in the new search results + for index in range(min(index_list.size(), max_search_results)): + var button := Button.new() + var line_text: String = code_editor.get_line(index_list[index]) + line_text = line_text.replace("\t", "") + line_text = line_text.replace("\"", "") + line_text = line_text.replace(",", "") + + button.size_flags_horizontal = Control.SIZE_EXPAND_FILL + button.text = line_text + + button.pressed.connect(_jump_to_search_result.bind(index_list[index])) + search_result_box.add_child(button) + + # Add notice to bottom if not every result was given a button + if index_list.size() > max_search_results: + var label := Label.new() + label.text = "...and %s more results" % [index_list.size() - max_search_results] + label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + + search_result_box.add_child(label) + +func _on_search_box_submitted(new_text: String): + update_text_search(new_text) + +func _on_search_button(): + update_text_search(search_box.text) + +func _jump_to_search_result(line: int): + code_editor.set_caret_line(line) + +#endregion + #region Utility func _on_code_edit_text_changed(): @@ -174,6 +227,8 @@ func code_editor_close(text: String = "Open a file or change edit mode in the to head_button_save.disabled = true code_error_footer.visible = false + update_text_search() + code_editor.clear_undo_history() func code_editor_open() -> void: @@ -185,6 +240,8 @@ func code_editor_open() -> void: head_button_save.disabled = false code_error_footer.visible = false + update_text_search() + code_editor.clear_undo_history() func code_editor_open_file(path: String) -> void: @@ -217,6 +274,8 @@ func code_editor_open_file(path: String) -> void: code_editor.placeholder_text = "" head_button_save.disabled = false + update_text_search() + code_editor.clear_undo_history() func apply_theme() -> void: diff --git a/addons/savedata-dx/view/save_view.tscn b/addons/savedata-dx/view/save_view.tscn index f82a4f2..4504ad6 100644 --- a/addons/savedata-dx/view/save_view.tscn +++ b/addons/savedata-dx/view/save_view.tscn @@ -1,7 +1,8 @@ -[gd_scene load_steps=4 format=3 uid="uid://cddem2i73sbap"] +[gd_scene load_steps=5 format=3 uid="uid://cddem2i73sbap"] [ext_resource type="Script" path="res://addons/savedata-dx/view/save_view.gd" id="1_sit27"] [ext_resource type="CodeHighlighter" uid="uid://cva1xputdk2d7" path="res://addons/savedata-dx/view/highlighter.tres" id="2_cnvvi"] +[ext_resource type="Script" path="res://addons/savedata-dx/view/code_edit.gd" id="3_yjeu0"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_hnoec"] bg_color = Color(0.294118, 0, 0.0313726, 1) @@ -51,6 +52,14 @@ layout_mode = 2 size_flags_vertical = 4 theme_override_constants/separation = 3 +[node name="HeadClose" type="Button" parent="Margin/VerticalSplit/Header/VBox/Top"] +unique_name_in_owner = true +layout_mode = 2 +tooltip_text = "Close current file or return to root +(Ctrl+W)" +disabled = true +text = " Close" + [node name="HeadLoad" type="Button" parent="Margin/VerticalSplit/Header/VBox/Top"] unique_name_in_owner = true layout_mode = 2 @@ -85,30 +94,14 @@ size_flags_vertical = 4 text = "Help " icon_alignment = 2 -[node name="HSeparator" type="HSeparator" parent="Margin/VerticalSplit/Header/VBox"] -layout_mode = 2 - -[node name="Bottom" type="HBoxContainer" parent="Margin/VerticalSplit/Header/VBox"] -layout_mode = 2 - -[node name="HeadClose" type="Button" parent="Margin/VerticalSplit/Header/VBox/Bottom"] -unique_name_in_owner = true -layout_mode = 2 -tooltip_text = "Close current file or return to root -(Ctrl+W)" -disabled = true -text = " Close" -flat = true - -[node name="OpenFileTextLabel" type="Label" parent="Margin/VerticalSplit/Header/VBox/Bottom"] -unique_name_in_owner = true +[node name="CodeSplit" type="HSplitContainer" parent="Margin/VerticalSplit"] layout_mode = 2 -theme_override_colors/font_color = Color(0.658824, 0.658824, 0.658824, 1) -vertical_alignment = 1 +size_flags_vertical = 3 -[node name="CodeEdit" type="CodeEdit" parent="Margin/VerticalSplit"] +[node name="CodeEdit" type="CodeEdit" parent="Margin/VerticalSplit/CodeSplit"] unique_name_in_owner = true layout_mode = 2 +size_flags_horizontal = 3 size_flags_vertical = 3 minimap_draw = true minimap_width = 60 @@ -120,6 +113,36 @@ draw_tabs = true gutters_draw_line_numbers = true auto_brace_completion_enabled = true auto_brace_completion_highlight_matching = true +script = ExtResource("3_yjeu0") + +[node name="Search" type="VBoxContainer" parent="Margin/VerticalSplit/CodeSplit"] +layout_mode = 2 + +[node name="SearchBox" type="LineEdit" parent="Margin/VerticalSplit/CodeSplit/Search"] +unique_name_in_owner = true +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 +placeholder_text = "Search Query" +alignment = 1 +max_length = 16 + +[node name="SearchButton" type="Button" parent="Margin/VerticalSplit/CodeSplit/Search"] +layout_mode = 2 +text = "Search" + +[node name="HSeparator" type="HSeparator" parent="Margin/VerticalSplit/CodeSplit/Search"] +layout_mode = 2 +theme_override_constants/separation = 12 + +[node name="Scroll" type="ScrollContainer" parent="Margin/VerticalSplit/CodeSplit/Search"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="SearchResultBox" type="VBoxContainer" parent="Margin/VerticalSplit/CodeSplit/Search/Scroll"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 [node name="ErrorFooter" type="PanelContainer" parent="Margin/VerticalSplit"] unique_name_in_owner = true @@ -153,11 +176,13 @@ Check the error display for additional info" dialog_autowrap = true [connection signal="timeout" from="CompileTimer" to="." method="_on_compile_timer_timeout"] +[connection signal="pressed" from="Margin/VerticalSplit/Header/VBox/Top/HeadClose" to="." method="_on_head_close_pressed"] [connection signal="pressed" from="Margin/VerticalSplit/Header/VBox/Top/HeadLoad" to="." method="_on_head_load_pressed"] [connection signal="pressed" from="Margin/VerticalSplit/Header/VBox/Top/HeadSave" to="." method="_on_head_save_pressed"] [connection signal="pressed" from="Margin/VerticalSplit/Header/VBox/Top/HeadEditSlot" to="." method="_on_head_edit_slot_pressed"] [connection signal="pressed" from="Margin/VerticalSplit/Header/VBox/Top/HeadEditCommon" to="." method="_on_head_edit_common_pressed"] [connection signal="pressed" from="Margin/VerticalSplit/Header/VBox/Top/HeadInfo" to="." method="_on_head_info_pressed"] -[connection signal="pressed" from="Margin/VerticalSplit/Header/VBox/Bottom/HeadClose" to="." method="_on_head_close_pressed"] -[connection signal="text_changed" from="Margin/VerticalSplit/CodeEdit" to="." method="_on_code_edit_text_changed"] +[connection signal="text_changed" from="Margin/VerticalSplit/CodeSplit/CodeEdit" to="." method="_on_code_edit_text_changed"] +[connection signal="text_submitted" from="Margin/VerticalSplit/CodeSplit/Search/SearchBox" to="." method="_on_search_box_submitted"] +[connection signal="pressed" from="Margin/VerticalSplit/CodeSplit/Search/SearchButton" to="." method="_on_search_button"] [connection signal="file_selected" from="InspectorFileDialog" to="." method="_on_inspector_select_file"] From c7c91f81ff8f6727f15541869eaa0db090a6de62 Mon Sep 17 00:00:00 2001 From: Amethyst-szs Date: Mon, 18 Dec 2023 10:32:25 -0800 Subject: [PATCH 24/30] Small cleanup --- addons/savedata-dx/plugin.gd | 8 +++----- addons/savedata-dx/view/save_view.gd | 4 +--- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/addons/savedata-dx/plugin.gd b/addons/savedata-dx/plugin.gd index be9033c..9944e71 100644 --- a/addons/savedata-dx/plugin.gd +++ b/addons/savedata-dx/plugin.gd @@ -5,10 +5,8 @@ const SaveView = preload("./view/save_view.tscn") var save_view func _enter_tree(): - # Ensure various folders + # Ensure the save folder in the user dir DirAccess.make_dir_recursive_absolute(SaveAccessorPlugin.SAVE_DIR) - DirAccess.make_dir_recursive_absolute("res://addons/savedata-dx/slot/") - DirAccess.make_dir_recursive_absolute("res://addons/savedata-dx/common/") # Setup singletons for accessing and holding save data add_autoload_singleton("SaveAccessor", "res://addons/savedata-dx/backend/save_accessor.gd") @@ -21,7 +19,7 @@ func _enter_tree(): # Instantiate save view main screen and hide from view save_view = SaveView.instantiate() save_view.editor_plugin = self - get_editor_interface().get_editor_main_screen().add_child(save_view) + EditorInterface.get_editor_main_screen().add_child(save_view) _make_visible(false) func _exit_tree(): @@ -41,7 +39,7 @@ func _make_visible(visible): save_view.visible = visible func _get_plugin_name(): - return "Saves" + return "Save" func _get_plugin_icon(): return get_editor_interface().get_base_control().get_theme_icon("Save", "EditorIcons") diff --git a/addons/savedata-dx/view/save_view.gd b/addons/savedata-dx/view/save_view.gd index ae07801..0c76392 100644 --- a/addons/savedata-dx/view/save_view.gd +++ b/addons/savedata-dx/view/save_view.gd @@ -64,10 +64,8 @@ func _ready() -> void: # Disable save button code_editor_close() - # Ensure various folders + # Ensure the save directory in user folder DirAccess.make_dir_recursive_absolute(SaveAccessorPlugin.SAVE_DIR) - DirAccess.make_dir_recursive_absolute("res://addons/savedata-dx/slot/") - DirAccess.make_dir_recursive_absolute("res://addons/savedata-dx/common/") apply_theme() From a27799539d105b8fc22efecf22527dc5bbd63e29 Mon Sep 17 00:00:00 2001 From: Amethyst-szs Date: Mon, 18 Dec 2023 10:36:57 -0800 Subject: [PATCH 25/30] Changed file extension, remove pointless warning --- addons/savedata-dx/backend/save_accessor.gd | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/addons/savedata-dx/backend/save_accessor.gd b/addons/savedata-dx/backend/save_accessor.gd index c11cd80..3625518 100644 --- a/addons/savedata-dx/backend/save_accessor.gd +++ b/addons/savedata-dx/backend/save_accessor.gd @@ -23,7 +23,7 @@ const SAVE_COMMON_NAME: String = "common" ## Name of the automatic save file const SAVE_AUTO_NAME: String = "auto" ## File extension used by save files -const SAVE_EXTENSION_NAME: String = ".bin" +const SAVE_EXTENSION_NAME: String = ".save" ## Encryption key, can be changed but will break all existing saves if changed const KEY: String = "no@NqlqGu8PTG#weQ77t$%bBQ9$HG5itZ#8#Xnbd%&L$y5Sd" @@ -294,10 +294,6 @@ func _read_backend(path: String) -> Dictionary: printerr("Cannot parse %s as json string, data is null! (%s)" % [path, content]) return {} - # Print message saying that the dictionary is empty if needed - if data.is_empty(): - push_warning("File at %s was parsed correctly, but contains no data" % [path]) - # Return the JSON data to then be converted into a object later return data From aa2f07f8db6089aa6e33db8b4fe8e113dfd6eb88 Mon Sep 17 00:00:00 2001 From: Amethyst-szs Date: Sat, 23 Dec 2023 09:46:02 -0800 Subject: [PATCH 26/30] WIP settings menu --- addons/savedata-dx/backend/save_accessor.gd | 14 ++- addons/savedata-dx/settings.gd | 36 ++++++ addons/savedata-dx/view/code_edit.gd | 4 + addons/savedata-dx/view/save_view.gd | 11 ++ addons/savedata-dx/view/save_view.tscn | 22 +++- addons/savedata-dx/view/settings_view.gd | 87 ++++++++++++++ addons/savedata-dx/view/settings_view.tscn | 127 ++++++++++++++++++++ 7 files changed, 299 insertions(+), 2 deletions(-) create mode 100644 addons/savedata-dx/settings.gd create mode 100644 addons/savedata-dx/view/settings_view.gd create mode 100644 addons/savedata-dx/view/settings_view.tscn diff --git a/addons/savedata-dx/backend/save_accessor.gd b/addons/savedata-dx/backend/save_accessor.gd index 3625518..dfb16a1 100644 --- a/addons/savedata-dx/backend/save_accessor.gd +++ b/addons/savedata-dx/backend/save_accessor.gd @@ -9,9 +9,12 @@ class_name SaveAccessorPlugin #region Imports # Include datatype parser for converting Object into JSON -const datatype_dict_parser = preload("res://addons/savedata-dx/backend/datatype_parser.gd") +const datatype_dict_parser: Script = preload("res://addons/savedata-dx/backend/datatype_parser.gd") var dict_parse = datatype_dict_parser.new() +# Include settings script for accessing and modifying save data settings +var settings: Script = preload("res://addons/savedata-dx/settings.gd") + #endregion #region Constants @@ -165,6 +168,15 @@ func read_common() -> void: #endregion +#region Initalization + +func _init(): + # Prepare the settings menu if this is in the editor + if Engine.is_editor_hint(): + settings.prepare() + +#endregion + #region Backend functions called by thread ## Not intended for the end user. diff --git a/addons/savedata-dx/settings.gd b/addons/savedata-dx/settings.gd new file mode 100644 index 0000000..3580385 --- /dev/null +++ b/addons/savedata-dx/settings.gd @@ -0,0 +1,36 @@ +extends RefCounted + +const DEFAULT_SETTINGS = { + "SAVE_DIR": "user://sv/", + "SAVE_COMMON_NAME": "common", + "SAVE_AUTO_NAME": "auto", + "SAVE_EXTENSION_NAME": ".save", + "KEY": "-G{=P1~Sy?BLty>7iBFI*G:w0#gI;nvP.[|x2F~H|PMAI&6mMLSW-Y^T%}9wYa+M", +} + +const valid_key_characters: String = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890~`!@#$%^&*()_-+={[}]|:;<,>.?" + +static func prepare() -> void: + # Set up defaults + for setting in DEFAULT_SETTINGS: + if ProjectSettings.has_setting("savedata/general/%s" % setting): + ProjectSettings.set_initial_value("savedata/general/%s" % setting, DEFAULT_SETTINGS[setting]) + ProjectSettings.save() + +static func set_setting(key: String, value) -> void: + ProjectSettings.set_setting("savedata/general/%s" % key, value) + ProjectSettings.set_initial_value("savedata/general/%s" % key, DEFAULT_SETTINGS[key]) + ProjectSettings.save() + +static func get_setting(key: String): + if ProjectSettings.has_setting("savedata/general/%s" % key): + return ProjectSettings.get_setting("savedata/general/%s" % key) + else: + return DEFAULT_SETTINGS[key] + +static func get_settings(only_keys: PackedStringArray = []) -> Dictionary: + var settings: Dictionary = {} + for key in DEFAULT_SETTINGS.keys(): + if only_keys.is_empty() or key in only_keys: + settings[key] = get_setting(key) + return settings diff --git a/addons/savedata-dx/view/code_edit.gd b/addons/savedata-dx/view/code_edit.gd index 6e061b6..23633b9 100644 --- a/addons/savedata-dx/view/code_edit.gd +++ b/addons/savedata-dx/view/code_edit.gd @@ -4,6 +4,10 @@ extends CodeEdit var highlight_color: Color = Color.BLACK func _ready(): + if not Engine.is_editor_hint(): + queue_free() + return + var settings = EditorInterface.get_editor_settings() highlight_color = settings.get_setting("text_editor/theme/highlighting/background_color") highlight_color = highlight_color.lightened(0.15) diff --git a/addons/savedata-dx/view/save_view.gd b/addons/savedata-dx/view/save_view.gd index 0c76392..e7374cc 100644 --- a/addons/savedata-dx/view/save_view.gd +++ b/addons/savedata-dx/view/save_view.gd @@ -21,6 +21,7 @@ const max_search_results: int = 50 @onready var head_button_save := %HeadSave @onready var head_button_edit_slot := %HeadEditSlot @onready var head_button_edit_common := %HeadEditCommon +@onready var head_button_settings := %HeadSettings @onready var head_button_info := %HeadInfo @onready var head_button_close := %HeadClose @@ -36,6 +37,8 @@ const max_search_results: int = 50 # Dialog popups @onready var inspector_file_dialog := $InspectorFileDialog @onready var inspector_save_fail_dialog := $InspectorSaveFailDialog +@onready var settings_dialog := $SettingsDialog +@onready var settings_view := %SettingsView #endregion @@ -67,6 +70,10 @@ func _ready() -> void: # Ensure the save directory in user folder DirAccess.make_dir_recursive_absolute(SaveAccessorPlugin.SAVE_DIR) + # Setup settings menu dialog + settings_dialog.theme = EditorInterface.get_editor_theme() + settings_view.main_view = self + apply_theme() func _input(event: InputEvent) -> void: @@ -114,6 +121,9 @@ func _on_head_edit_common_pressed(): EditorInterface.edit_script(script) EditorInterface.set_main_screen_editor("Script") +func _on_head_settings_pressed(): + settings_dialog.popup() + func _on_head_info_pressed(): OS.shell_open("https://github.com/Amethyst-szs/godot-savedata-dx/wiki") @@ -286,6 +296,7 @@ func apply_theme() -> void: head_button_save.icon = get_theme_icon("Save", "EditorIcons") head_button_edit_slot.icon = get_theme_icon("Edit", "EditorIcons") head_button_edit_common.icon = get_theme_icon("EditInternal", "EditorIcons") + head_button_settings.icon = get_theme_icon("Tools", "EditorIcons") head_button_info.icon = get_theme_icon("Help", "EditorIcons") head_button_close.icon = get_theme_icon("Back", "EditorIcons") diff --git a/addons/savedata-dx/view/save_view.tscn b/addons/savedata-dx/view/save_view.tscn index 4504ad6..973d57b 100644 --- a/addons/savedata-dx/view/save_view.tscn +++ b/addons/savedata-dx/view/save_view.tscn @@ -1,8 +1,9 @@ -[gd_scene load_steps=5 format=3 uid="uid://cddem2i73sbap"] +[gd_scene load_steps=6 format=3 uid="uid://cddem2i73sbap"] [ext_resource type="Script" path="res://addons/savedata-dx/view/save_view.gd" id="1_sit27"] [ext_resource type="CodeHighlighter" uid="uid://cva1xputdk2d7" path="res://addons/savedata-dx/view/highlighter.tres" id="2_cnvvi"] [ext_resource type="Script" path="res://addons/savedata-dx/view/code_edit.gd" id="3_yjeu0"] +[ext_resource type="PackedScene" uid="uid://mo4wx8of0sdp" path="res://addons/savedata-dx/view/settings_view.tscn" id="4_j10q2"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_hnoec"] bg_color = Color(0.294118, 0, 0.0313726, 1) @@ -86,6 +87,12 @@ layout_mode = 2 tooltip_text = "Opens the common save in the Script Editor" text = "Edit Common" +[node name="HeadSettings" type="Button" parent="Margin/VerticalSplit/Header/VBox/Top"] +unique_name_in_owner = true +layout_mode = 2 +tooltip_text = "Opens the common save in the Script Editor" +text = "Settings" + [node name="HeadInfo" type="Button" parent="Margin/VerticalSplit/Header/VBox/Top"] unique_name_in_owner = true layout_mode = 2 @@ -175,12 +182,25 @@ dialog_text = "The save slot open in the inspector could not be saved! Check the error display for additional info" dialog_autowrap = true +[node name="SettingsDialog" type="Popup" parent="."] +disable_3d = true +title = "SaveData-DX Settings Menu" +initial_position = 4 +size = Vector2i(720, 480) +unresizable = false +borderless = false +min_size = Vector2i(360, 240) + +[node name="SettingsView" parent="SettingsDialog" instance=ExtResource("4_j10q2")] +unique_name_in_owner = true + [connection signal="timeout" from="CompileTimer" to="." method="_on_compile_timer_timeout"] [connection signal="pressed" from="Margin/VerticalSplit/Header/VBox/Top/HeadClose" to="." method="_on_head_close_pressed"] [connection signal="pressed" from="Margin/VerticalSplit/Header/VBox/Top/HeadLoad" to="." method="_on_head_load_pressed"] [connection signal="pressed" from="Margin/VerticalSplit/Header/VBox/Top/HeadSave" to="." method="_on_head_save_pressed"] [connection signal="pressed" from="Margin/VerticalSplit/Header/VBox/Top/HeadEditSlot" to="." method="_on_head_edit_slot_pressed"] [connection signal="pressed" from="Margin/VerticalSplit/Header/VBox/Top/HeadEditCommon" to="." method="_on_head_edit_common_pressed"] +[connection signal="pressed" from="Margin/VerticalSplit/Header/VBox/Top/HeadSettings" to="." method="_on_head_settings_pressed"] [connection signal="pressed" from="Margin/VerticalSplit/Header/VBox/Top/HeadInfo" to="." method="_on_head_info_pressed"] [connection signal="text_changed" from="Margin/VerticalSplit/CodeSplit/CodeEdit" to="." method="_on_code_edit_text_changed"] [connection signal="text_submitted" from="Margin/VerticalSplit/CodeSplit/Search/SearchBox" to="." method="_on_search_box_submitted"] diff --git a/addons/savedata-dx/view/settings_view.gd b/addons/savedata-dx/view/settings_view.gd new file mode 100644 index 0000000..616ddc2 --- /dev/null +++ b/addons/savedata-dx/view/settings_view.gd @@ -0,0 +1,87 @@ +@tool +extends Control + +var is_setup_complete: bool = false + +var settings = null +var main_view: Control = null: + set(value): + main_view = value + settings = value.accessor_inst.settings + prepare() + +func prepare(): + var use_theme: Theme = get_parent().theme + setup_theme(self, use_theme) + + setup_line_edit(%Directory, "SAVE_DIR") + setup_line_edit(%CommonName, "SAVE_COMMON_NAME") + setup_line_edit(%AutoName, "SAVE_AUTO_NAME") + setup_line_edit(%Extension, "SAVE_EXTENSION_NAME") + + %EncryptionReroll.icon = get_theme_icon("Reload", "EditorIcons") + %EncryptionKey.text = settings.get_setting("KEY") + + is_setup_complete = true + +func setup_line_edit(node: LineEdit, key: String): + var data: String = settings.get_setting(key) + if data != settings.DEFAULT_SETTINGS[key]: + node.text = data + +func setup_theme(node, use_theme: Theme): + if not node is Control: + return + + node.theme = use_theme + for child in node.get_children(): + if not child is Control: + continue + + child.theme = use_theme + if child.get_child_count() > 0: + setup_theme(child, use_theme) + +func _on_directory_text_changed(new_text: String): + var key: String = "SAVE_DIR" + if is_setup_complete: + if not new_text.is_empty(): + settings.set_setting(key, new_text) + else: + settings.set_setting(key, settings.DEFAULT_SETTINGS[key]) + +func _on_common_name_text_changed(new_text: String): + var key: String = "SAVE_COMMON_NAME" + if is_setup_complete: + if not new_text.is_empty(): + settings.set_setting(key, new_text) + else: + settings.set_setting(key, settings.DEFAULT_SETTINGS[key]) + +func _on_auto_name_text_changed(new_text: String): + var key: String = "SAVE_AUTO_NAME" + if is_setup_complete: + if not new_text.is_empty(): + settings.set_setting(key, new_text) + else: + settings.set_setting(key, settings.DEFAULT_SETTINGS[key]) + +func _on_extension_text_changed(new_text: String): + var key: String = "SAVE_EXTENSION_NAME" + if is_setup_complete: + if not new_text.is_empty(): + settings.set_setting(key, new_text) + else: + settings.set_setting(key, settings.DEFAULT_SETTINGS[key]) + +func _on_encryption_reroll_pressed(): + settings.set_setting("KEY", _key_generator()) + %EncryptionKey.text = settings.get_setting("KEY") + +func _key_generator() -> String: + var key: String = "" + for i in range(64): + var rand_idx: int = randi() % settings.valid_key_characters.length() + key += settings.valid_key_characters[rand_idx] + + return key diff --git a/addons/savedata-dx/view/settings_view.tscn b/addons/savedata-dx/view/settings_view.tscn new file mode 100644 index 0000000..fa297b3 --- /dev/null +++ b/addons/savedata-dx/view/settings_view.tscn @@ -0,0 +1,127 @@ +[gd_scene load_steps=2 format=3 uid="uid://mo4wx8of0sdp"] + +[ext_resource type="Script" path="res://addons/savedata-dx/view/settings_view.gd" id="1_lo05t"] + +[node name="SettingsView" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +script = ExtResource("1_lo05t") + +[node name="Root" type="MarginContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="Tabs" type="TabContainer" parent="Root"] +layout_mode = 2 +current_tab = 1 + +[node name="File Format" type="MarginContainer" parent="Root/Tabs"] +visible = false +layout_mode = 2 +theme_override_constants/margin_left = 8 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 8 +theme_override_constants/margin_bottom = 10 + +[node name="ListScroll" type="ScrollContainer" parent="Root/Tabs/File Format"] +layout_mode = 2 +horizontal_scroll_mode = 0 + +[node name="List" type="VBoxContainer" parent="Root/Tabs/File Format/ListScroll"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="DirectoryHeader" type="Label" parent="Root/Tabs/File Format/ListScroll/List"] +layout_mode = 2 +text = "Save Directory:" + +[node name="Directory" type="LineEdit" parent="Root/Tabs/File Format/ListScroll/List"] +unique_name_in_owner = true +layout_mode = 2 +placeholder_text = "user://sv/" +caret_blink = true + +[node name="HSeparator" type="HSeparator" parent="Root/Tabs/File Format/ListScroll/List"] +layout_mode = 2 +theme_override_constants/separation = 10 + +[node name="CommonNameHeader" type="Label" parent="Root/Tabs/File Format/ListScroll/List"] +layout_mode = 2 +text = "Common File Name:" + +[node name="CommonName" type="LineEdit" parent="Root/Tabs/File Format/ListScroll/List"] +unique_name_in_owner = true +layout_mode = 2 +placeholder_text = "common" +caret_blink = true + +[node name="AutoNameHeader" type="Label" parent="Root/Tabs/File Format/ListScroll/List"] +layout_mode = 2 +text = "Autosave File Name:" + +[node name="AutoName" type="LineEdit" parent="Root/Tabs/File Format/ListScroll/List"] +unique_name_in_owner = true +layout_mode = 2 +placeholder_text = "auto" +caret_blink = true + +[node name="ExtensionHeader" type="Label" parent="Root/Tabs/File Format/ListScroll/List"] +layout_mode = 2 +text = "File Extension:" + +[node name="Extension" type="LineEdit" parent="Root/Tabs/File Format/ListScroll/List"] +unique_name_in_owner = true +layout_mode = 2 +placeholder_text = ".save" +caret_blink = true + +[node name="Encryption" type="MarginContainer" parent="Root/Tabs"] +layout_mode = 2 +theme_override_constants/margin_left = 8 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 8 +theme_override_constants/margin_bottom = 10 + +[node name="ListScroll" type="ScrollContainer" parent="Root/Tabs/Encryption"] +layout_mode = 2 +horizontal_scroll_mode = 0 + +[node name="List" type="VBoxContainer" parent="Root/Tabs/Encryption/ListScroll"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="EncryptionHeader" type="Label" parent="Root/Tabs/Encryption/ListScroll/List"] +layout_mode = 2 +text = "Encryption Key ( Changing this key will break all current save files!!! )" + +[node name="Field" type="HBoxContainer" parent="Root/Tabs/Encryption/ListScroll/List"] +layout_mode = 2 + +[node name="EncryptionReroll" type="Button" parent="Root/Tabs/Encryption/ListScroll/List/Field"] +unique_name_in_owner = true +layout_mode = 2 +text = "Reroll" + +[node name="EncryptionKey" type="LineEdit" parent="Root/Tabs/Encryption/ListScroll/List/Field"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +editable = false + +[connection signal="text_changed" from="Root/Tabs/File Format/ListScroll/List/Directory" to="." method="_on_directory_text_changed"] +[connection signal="text_changed" from="Root/Tabs/File Format/ListScroll/List/CommonName" to="." method="_on_common_name_text_changed"] +[connection signal="text_changed" from="Root/Tabs/File Format/ListScroll/List/AutoName" to="." method="_on_auto_name_text_changed"] +[connection signal="text_changed" from="Root/Tabs/File Format/ListScroll/List/Extension" to="." method="_on_extension_text_changed"] +[connection signal="pressed" from="Root/Tabs/Encryption/ListScroll/List/Field/EncryptionReroll" to="." method="_on_encryption_reroll_pressed"] From 5b22efc71725aa2501eba2fa901b2f1d156ce0b8 Mon Sep 17 00:00:00 2001 From: Amethyst-szs Date: Sat, 23 Dec 2023 10:00:12 -0800 Subject: [PATCH 27/30] Finished moving accessor consts to settings menu --- addons/savedata-dx/backend/save_accessor.gd | 41 +++++++++++---------- addons/savedata-dx/plugin.gd | 3 -- addons/savedata-dx/view/save_view.gd | 8 +++- 3 files changed, 28 insertions(+), 24 deletions(-) diff --git a/addons/savedata-dx/backend/save_accessor.gd b/addons/savedata-dx/backend/save_accessor.gd index dfb16a1..7ee200b 100644 --- a/addons/savedata-dx/backend/save_accessor.gd +++ b/addons/savedata-dx/backend/save_accessor.gd @@ -17,18 +17,23 @@ var settings: Script = preload("res://addons/savedata-dx/settings.gd") #endregion -#region Constants +#region Settings ## Directory to store save files -const SAVE_DIR: String = "user://sv/" +var SAVE_DIR: String = "" ## Name of the common file -const SAVE_COMMON_NAME: String = "common" +var SAVE_COMMON_NAME: String = "" ## Name of the automatic save file -const SAVE_AUTO_NAME: String = "auto" +var SAVE_AUTO_NAME: String = "" ## File extension used by save files -const SAVE_EXTENSION_NAME: String = ".save" -## Encryption key, can be changed but will break all existing saves if changed -const KEY: String = "no@NqlqGu8PTG#weQ77t$%bBQ9$HG5itZ#8#Xnbd%&L$y5Sd" +var SAVE_EXTENSION_NAME: String = "" + +func setup_settings_data(): + SAVE_DIR = settings.get_setting("SAVE_DIR") + SAVE_COMMON_NAME = settings.get_setting("SAVE_COMMON_NAME") + SAVE_AUTO_NAME = settings.get_setting("SAVE_AUTO_NAME") + SAVE_EXTENSION_NAME = settings.get_setting("SAVE_EXTENSION_NAME") + print(SAVE_DIR, SAVE_COMMON_NAME, SAVE_AUTO_NAME, SAVE_EXTENSION_NAME) #endregion @@ -168,15 +173,6 @@ func read_common() -> void: #endregion -#region Initalization - -func _init(): - # Prepare the settings menu if this is in the editor - if Engine.is_editor_hint(): - settings.prepare() - -#endregion - #region Backend functions called by thread ## Not intended for the end user. @@ -254,7 +250,7 @@ func _write_backend(name: String, object) -> bool: var file_path = SAVE_DIR + name + SAVE_EXTENSION_NAME # Attempt to open new file and print an error if it fails - var file = FileAccess.open_encrypted_with_pass(file_path, FileAccess.WRITE, KEY) + var file = FileAccess.open_encrypted_with_pass(file_path, FileAccess.WRITE, settings.get_setting("KEY")) if file == null: printerr("FileAccess open error: " + str(FileAccess.get_open_error())) return false @@ -276,7 +272,7 @@ func _write_backend_with_json_string(path: String, json_string: String) -> bool: DirAccess.make_dir_absolute(SAVE_DIR) # Attempt to open new file and print an error if it fails - var file = FileAccess.open_encrypted_with_pass(path, FileAccess.WRITE, KEY) + var file = FileAccess.open_encrypted_with_pass(path, FileAccess.WRITE, settings.get_setting("KEY")) if file == null: printerr("FileAccess open error: " + str(FileAccess.get_open_error())) return false @@ -318,7 +314,7 @@ func _read_backend_raw_data(path: String) -> String: return "" # Open the file and return if something goes wrong (another program controlling it for example) - var file = FileAccess.open_encrypted_with_pass(path, FileAccess.READ, KEY) + var file = FileAccess.open_encrypted_with_pass(path, FileAccess.READ, settings.get_setting("KEY")) if file == null: printerr("FileAccess open error: " + str(FileAccess.get_open_error())) return "" @@ -376,6 +372,13 @@ var is_thread_busy: bool = false: var is_thread_terminate: bool = false func _ready(): + # Prepare copy of frequently used settings data + setup_settings_data() + + # Prepare the settings menu if this is in the editor + if Engine.is_editor_hint(): + settings.prepare() + # Setup thread and and its components thread_semaphore = Semaphore.new() diff --git a/addons/savedata-dx/plugin.gd b/addons/savedata-dx/plugin.gd index 9944e71..598b536 100644 --- a/addons/savedata-dx/plugin.gd +++ b/addons/savedata-dx/plugin.gd @@ -5,9 +5,6 @@ const SaveView = preload("./view/save_view.tscn") var save_view func _enter_tree(): - # Ensure the save folder in the user dir - DirAccess.make_dir_recursive_absolute(SaveAccessorPlugin.SAVE_DIR) - # Setup singletons for accessing and holding save data add_autoload_singleton("SaveAccessor", "res://addons/savedata-dx/backend/save_accessor.gd") add_autoload_singleton("SaveHolder", "res://addons/savedata-dx/backend/save_holder.gd") diff --git a/addons/savedata-dx/view/save_view.gd b/addons/savedata-dx/view/save_view.gd index e7374cc..8222a78 100644 --- a/addons/savedata-dx/view/save_view.gd +++ b/addons/savedata-dx/view/save_view.gd @@ -5,7 +5,7 @@ extends Control # SaveData accessor script const accessor_script = preload("res://addons/savedata-dx/backend/save_accessor.gd") -var accessor_inst = accessor_script.new() +var accessor_inst = null # Reference to editor plugin var editor_plugin: EditorPlugin @@ -64,11 +64,15 @@ func _ready() -> void: if not is_instance_valid(editor_plugin): return + # Setup save accessor + accessor_inst = accessor_script.new() + add_child(accessor_inst) + # Disable save button code_editor_close() # Ensure the save directory in user folder - DirAccess.make_dir_recursive_absolute(SaveAccessorPlugin.SAVE_DIR) + DirAccess.make_dir_recursive_absolute(accessor_inst.SAVE_DIR) # Setup settings menu dialog settings_dialog.theme = EditorInterface.get_editor_theme() From ee530c20910ee93a6442f0f36aea3f2837859e54 Mon Sep 17 00:00:00 2001 From: Amethyst-szs Date: Sat, 23 Dec 2023 10:09:38 -0800 Subject: [PATCH 28/30] Fix up the menu a little bit --- addons/savedata-dx/view/settings_view.tscn | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/addons/savedata-dx/view/settings_view.tscn b/addons/savedata-dx/view/settings_view.tscn index fa297b3..ac365eb 100644 --- a/addons/savedata-dx/view/settings_view.tscn +++ b/addons/savedata-dx/view/settings_view.tscn @@ -23,10 +23,8 @@ grow_vertical = 2 [node name="Tabs" type="TabContainer" parent="Root"] layout_mode = 2 -current_tab = 1 [node name="File Format" type="MarginContainer" parent="Root/Tabs"] -visible = false layout_mode = 2 theme_override_constants/margin_left = 8 theme_override_constants/margin_top = 10 @@ -87,6 +85,7 @@ placeholder_text = ".save" caret_blink = true [node name="Encryption" type="MarginContainer" parent="Root/Tabs"] +visible = false layout_mode = 2 theme_override_constants/margin_left = 8 theme_override_constants/margin_top = 10 @@ -104,7 +103,7 @@ size_flags_vertical = 3 [node name="EncryptionHeader" type="Label" parent="Root/Tabs/Encryption/ListScroll/List"] layout_mode = 2 -text = "Encryption Key ( Changing this key will break all current save files!!! )" +text = "Encryption Key: (Changing this key will break all current save files!!!)" [node name="Field" type="HBoxContainer" parent="Root/Tabs/Encryption/ListScroll/List"] layout_mode = 2 From 0acf699f176b86d0017e4ef8175ffb410a89ad43 Mon Sep 17 00:00:00 2001 From: Amethyst-szs Date: Sat, 23 Dec 2023 10:14:26 -0800 Subject: [PATCH 29/30] Remove accidental debug log --- addons/savedata-dx/backend/save_accessor.gd | 1 - 1 file changed, 1 deletion(-) diff --git a/addons/savedata-dx/backend/save_accessor.gd b/addons/savedata-dx/backend/save_accessor.gd index 7ee200b..901ee50 100644 --- a/addons/savedata-dx/backend/save_accessor.gd +++ b/addons/savedata-dx/backend/save_accessor.gd @@ -33,7 +33,6 @@ func setup_settings_data(): SAVE_COMMON_NAME = settings.get_setting("SAVE_COMMON_NAME") SAVE_AUTO_NAME = settings.get_setting("SAVE_AUTO_NAME") SAVE_EXTENSION_NAME = settings.get_setting("SAVE_EXTENSION_NAME") - print(SAVE_DIR, SAVE_COMMON_NAME, SAVE_AUTO_NAME, SAVE_EXTENSION_NAME) #endregion From 3b9399e0427ebc22735ae5a82afdc62c23472705 Mon Sep 17 00:00:00 2001 From: Amethyst-szs Date: Sun, 14 Apr 2024 08:33:25 -0700 Subject: [PATCH 30/30] SaveAccessor bugfix --- addons/savedata-dx/backend/save_accessor.gd | 31 ++++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/addons/savedata-dx/backend/save_accessor.gd b/addons/savedata-dx/backend/save_accessor.gd index 901ee50..4088df2 100644 --- a/addons/savedata-dx/backend/save_accessor.gd +++ b/addons/savedata-dx/backend/save_accessor.gd @@ -189,6 +189,8 @@ func _write_slot_thread_func(index: int, is_auto_slot: bool) -> void: call_deferred("emit_signal", "save_slot_complete") else: call_deferred("emit_signal", "save_error") + + thread_semaphore.post() ## Not intended for the end user. ## Functionality for save slot reading, handled on a seperate thread @@ -212,6 +214,8 @@ func _read_slot_thread_func(index: int, is_auto_slot: bool) -> void: # Tell the signal that the load is finished call_deferred("emit_signal", "load_slot_complete") + + thread_semaphore.post() ## Not intended for the end user. ## Functionality for common writing, handled on a seperate thread @@ -221,6 +225,8 @@ func _write_common_thread_func() -> void: call_deferred("emit_signal", "save_common_complete") else: call_deferred("emit_signal", "save_error") + + thread_semaphore.post() ## Not intended for the end user. ## Functionality for common reading, handled on a seperate thread @@ -228,6 +234,7 @@ func _read_common_thread_func() -> void: # Get dictionary from file in save directory var dict: Dictionary = _read_backend_by_name(SAVE_COMMON_NAME) if dict.is_empty(): + printerr("SaveAccessor common data load error") call_deferred("emit_signal", "load_error") return @@ -237,6 +244,8 @@ func _read_common_thread_func() -> void: # Tell the signal that the load is finished call_deferred("emit_signal", "load_slot_complete") + + thread_semaphore.post() # Backend functions handling reading and writing of data @@ -363,8 +372,10 @@ var is_thread_busy: bool = false: is_thread_busy = value if value: + print("SaveAccessor thread now busy") call_deferred("emit_signal", "thread_busy") else: + print("SaveAccessor thread completed work") call_deferred("emit_signal", "thread_complete") ## Should the thread be terminated @@ -393,13 +404,17 @@ func _thread_func(): match(thread_request.type): ThreadRequestType.WRITE_SLOT: - await _write_slot_thread_func(thread_request.slot_id, thread_request.is_slot_auto) + _write_slot_thread_func(thread_request.slot_id, thread_request.is_slot_auto) + thread_semaphore.wait() ThreadRequestType.READ_SLOT: - await _read_slot_thread_func(thread_request.slot_id, thread_request.is_slot_auto) + _read_slot_thread_func(thread_request.slot_id, thread_request.is_slot_auto) + thread_semaphore.wait() ThreadRequestType.WRITE_COMMON: - await _write_common_thread_func() + _write_common_thread_func() + thread_semaphore.wait() ThreadRequestType.READ_COMMON: - await _read_common_thread_func() + _read_common_thread_func() + thread_semaphore.wait() is_thread_busy = false @@ -429,7 +444,7 @@ func _object_to_dict(obj: Object) -> Dictionary: # Iterate through all members for member in members: member_index += 1 - if member_index <= 3: + if member_index <= 2: continue # Write the member to dictionary depending on type of member @@ -488,8 +503,10 @@ func _dict_to_object(dict: Dictionary, obj_ref: Array) -> void: # Perform different behavior depending on the type of this member match(typeof(member[0])): TYPE_NIL: # Make sure this property exists on the object - push_warning("Loading SaveData: Property \"%s\" does not exist on object" - % [dict.keys()[key]]) + if not dict.keys()[key].contains(".gd"): + push_warning("Loading SaveData: Property \"%s\" does not exist on object" + % [dict.keys()[key]]) + continue TYPE_OBJECT: # Call self again with sub-object _dict_to_object(dict.values()[key], member)