├── images ├── doc1.png ├── doc2.png ├── doc3.png ├── SaveEngine.png ├── doc1.png.import ├── doc2.png.import ├── doc3.png.import ├── SaveEngine.png.import ├── Add.svg.import ├── Load.svg.import ├── Save.svg.import ├── Player.svg.import ├── Remove.svg.import ├── SaveEngine.svg.import ├── Add.svg ├── Save.svg ├── Remove.svg ├── Load.svg ├── Player.svg └── SaveEngine.svg ├── .gitattributes ├── addons └── saveEngine │ ├── scripts │ ├── SaveFileScene.gd │ ├── SaveFileGlobal.gd │ ├── SaveFileNode.gd │ ├── saveSettings.gd │ ├── SaveFile.gd │ ├── SaveElementBase.gd │ ├── SaveElement3D.gd │ ├── SaveElement2D.gd │ ├── SaveAgent.gd │ ├── SerializationHelper.gd │ └── SaveService.gd │ ├── plugin.cfg │ ├── saveEngine.gd │ ├── SaveAgent.svg.import │ ├── SaveElement2D.svg.import │ ├── SaveElement3D.svg.import │ ├── SaveAgent.svg │ ├── SaveElement2D.svg │ └── SaveElement3D.svg ├── .gitignore ├── example ├── game.gd ├── player.gd ├── example.gd ├── Player.tscn ├── save_slot.gd ├── example.tscn ├── Game.tscn └── SaveSlot.tscn ├── icon.svg.import ├── LICENSE ├── icon.svg ├── project.godot └── README.md /images/doc1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiaNCI-Studio/SaveEngine/HEAD/images/doc1.png -------------------------------------------------------------------------------- /images/doc2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiaNCI-Studio/SaveEngine/HEAD/images/doc2.png -------------------------------------------------------------------------------- /images/doc3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiaNCI-Studio/SaveEngine/HEAD/images/doc3.png -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Normalize EOL for all files that Git considers text files. 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /images/SaveEngine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiaNCI-Studio/SaveEngine/HEAD/images/SaveEngine.png -------------------------------------------------------------------------------- /addons/saveEngine/scripts/SaveFileScene.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | class_name SaveFileScene 3 | 4 | @export var ScenePath : String = "" 5 | @export var SceneNodes : Array[SaveFileNode] = [] 6 | -------------------------------------------------------------------------------- /addons/saveEngine/scripts/SaveFileGlobal.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | class_name SaveFileGlobal 3 | 4 | @export var GlobalId : String = "" 5 | @export var GlobalClassName : String = "" 6 | @export var GlobalData : Dictionary = {} 7 | -------------------------------------------------------------------------------- /addons/saveEngine/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="Save Engine" 4 | description="Save and load game mechanics 5 | With autosave and Multiple save slots 6 | See examples and docs on github for usage. 7 | " 8 | author="CiaNCI" 9 | version="1.0" 10 | script="saveEngine.gd" 11 | -------------------------------------------------------------------------------- /addons/saveEngine/scripts/SaveFileNode.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | class_name SaveFileNode 3 | 4 | @export var NodeTreePath : String = "" 5 | @export var ParentTreePath : String = "" 6 | @export var NodeFilePath : String = "" 7 | @export var NodeScriptPath : String = "" 8 | @export var NodeProperties : Dictionary = {} 9 | -------------------------------------------------------------------------------- /addons/saveEngine/scripts/saveSettings.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | class_name SaveSettigns 3 | 4 | @export var SaveFilesPrefix : String = "" 5 | @export var StaticScriptsToSave : Array[String] = [] 6 | @export var SaveFolderPath : String = "user://Saves/" 7 | @export var HashSalt : String = "b3203daa0aeb25a333b3c1d5d2db7f57" 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Godot 4+ specific ignores 2 | .godot/ 3 | 4 | # Godot-specific ignores 5 | .import/ 6 | export.cfg 7 | export_presets.cfg 8 | 9 | # Imported translations (automatically generated from CSV files) 10 | *.translation 11 | 12 | # Mono-specific ignores 13 | .mono/ 14 | data_*/ 15 | mono_crash.*.json 16 | -------------------------------------------------------------------------------- /example/game.gd: -------------------------------------------------------------------------------- 1 | extends Node2D 2 | 3 | @onready var saving_label: Label = $CanvasLayer/SavingLabel 4 | @onready var save_agent: SaveAgent = $SaveAgent 5 | 6 | func _ready() -> void: 7 | pass # Replace with function body. 8 | 9 | func _process(delta: float) -> void: 10 | pass 11 | 12 | func _on_save_agent_start_auto_saving() -> void: 13 | saving_label.visible = true 14 | 15 | func _on_save_agent_finish_auto_saving() -> void: 16 | await get_tree().create_timer(2).timeout 17 | saving_label.visible = false 18 | -------------------------------------------------------------------------------- /addons/saveEngine/scripts/SaveFile.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | class_name SaveFile 3 | 4 | @export var Version : String = "1.0" 5 | @export var SlotId : String = "" 6 | @export var Name : String = "" 7 | @export var DateTime : Dictionary = {} 8 | @export var LastTime : int = 0 9 | @export var GameTime : int = 0 10 | @export var Hash : String = "" 11 | @export var CurrentSceneId : String = "" 12 | @export var MetaData : Dictionary = {} 13 | @export var GlobalData : Array[SaveFileGlobal] = [] 14 | @export var ScenesData : Array[SaveFileScene] = [] 15 | -------------------------------------------------------------------------------- /addons/saveEngine/scripts/SaveElementBase.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | class_name SaveElementBase 3 | 4 | @export var SaveTransform : bool = true 5 | @export var SaveVisibility : bool = true 6 | @export var SaveProperties : Array[String] = [] 7 | 8 | func _enter_tree() -> void: 9 | add_to_group(SaveService.PERSISTENCE_GROUP) 10 | 11 | func Load(data : SaveFileNode): 12 | var parent = get_parent() 13 | for property in data.NodeProperties: 14 | if property.contains(":"): 15 | var nodePath = property.split(":")[0] 16 | var propertyPath = ":".join(property.split(":").slice(1)) 17 | var node = parent.get_node(nodePath) 18 | node.set(propertyPath, SaveSerializationHelper.DeserializeVariable(data.NodeProperties[property])) 19 | else: 20 | parent.set(property, SaveSerializationHelper.DeserializeVariable(data.NodeProperties[property])) 21 | 22 | -------------------------------------------------------------------------------- /addons/saveEngine/saveEngine.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | 4 | func _enter_tree() -> void: 5 | add_autoload_singleton("SaveService", "res://addons/saveEngine/scripts/SaveService.gd") 6 | add_custom_type("SaveAgent", "Node", preload("res://addons/saveEngine/scripts/SaveAgent.gd"), preload("res://addons/saveEngine/SaveAgent.svg")) 7 | add_custom_type("SaveElement3D", "Node3D", preload("res://addons/saveEngine/scripts/SaveElement3D.gd"), preload("res://addons/saveEngine/SaveElement3D.svg")) 8 | add_custom_type("SaveElement2D", "Node2D", preload("res://addons/saveEngine/scripts/SaveElement2D.gd"), preload("res://addons/saveEngine/SaveElement2D.svg")) 9 | 10 | func _exit_tree() -> void: 11 | remove_autoload_singleton("SaveService") 12 | remove_custom_type("SaveAgent") 13 | remove_custom_type("SaveElement3D") 14 | remove_custom_type("SaveElement2D") 15 | 16 | -------------------------------------------------------------------------------- /images/doc1.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://7nq8vu0t5alc" 6 | path="res://.godot/imported/doc1.png-8f44361e1b0dd535e64afad5631c0d49.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://images/doc1.png" 14 | dest_files=["res://.godot/imported/doc1.png-8f44361e1b0dd535e64afad5631c0d49.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /images/doc2.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://bncjbwqlymqxg" 6 | path="res://.godot/imported/doc2.png-c4b016087a2134fade7982abcd0fc0be.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://images/doc2.png" 14 | dest_files=["res://.godot/imported/doc2.png-c4b016087a2134fade7982abcd0fc0be.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /images/doc3.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://i7icamqhq4wg" 6 | path="res://.godot/imported/doc3.png-c1615d96242ad5edc8967a75ac699cf6.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://images/doc3.png" 14 | dest_files=["res://.godot/imported/doc3.png-c1615d96242ad5edc8967a75ac699cf6.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /images/SaveEngine.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://dkcr01oovji8b" 6 | path="res://.godot/imported/SaveEngine.png-6089c14964272ccddfade94ab2035c9a.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://images/SaveEngine.png" 14 | dest_files=["res://.godot/imported/SaveEngine.png-6089c14964272ccddfade94ab2035c9a.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /icon.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://cw8q6bmk2idax" 6 | path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://icon.svg" 14 | dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /images/Add.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://tgoufab05dru" 6 | path="res://.godot/imported/Add.svg-503dba5da9a4bfa3df52926ee0d1e396.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://images/Add.svg" 14 | dest_files=["res://.godot/imported/Add.svg-503dba5da9a4bfa3df52926ee0d1e396.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /images/Load.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://crobxyirk3dqt" 6 | path="res://.godot/imported/Load.svg-d825c099a5f750096215e3e117017d3c.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://images/Load.svg" 14 | dest_files=["res://.godot/imported/Load.svg-d825c099a5f750096215e3e117017d3c.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /images/Save.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://btq06kdtsv3l4" 6 | path="res://.godot/imported/Save.svg-e1ffbbdd75597985253bffc2afbd25df.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://images/Save.svg" 14 | dest_files=["res://.godot/imported/Save.svg-e1ffbbdd75597985253bffc2afbd25df.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /images/Player.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://sduh5pg5rbab" 6 | path="res://.godot/imported/Player.svg-b3f79b95310d05bbf9b6d15db3289960.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://images/Player.svg" 14 | dest_files=["res://.godot/imported/Player.svg-b3f79b95310d05bbf9b6d15db3289960.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /images/Remove.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://d0x7lwfg088xo" 6 | path="res://.godot/imported/Remove.svg-d5f75f24e1f623e211f1b9bade42bc59.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://images/Remove.svg" 14 | dest_files=["res://.godot/imported/Remove.svg-d5f75f24e1f623e211f1b9bade42bc59.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /images/SaveEngine.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://bfy8p1fh0oqkv" 6 | path="res://.godot/imported/SaveEngine.svg-6d139854abefd0fb26a7471bdb8bbc22.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://images/SaveEngine.svg" 14 | dest_files=["res://.godot/imported/SaveEngine.svg-6d139854abefd0fb26a7471bdb8bbc22.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/saveEngine/SaveAgent.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://dyqomj7kevuxb" 6 | path="res://.godot/imported/SaveAgent.svg-3cc5ce4c56caa4969141a686a81b7915.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/saveEngine/SaveAgent.svg" 14 | dest_files=["res://.godot/imported/SaveAgent.svg-3cc5ce4c56caa4969141a686a81b7915.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/saveEngine/SaveElement2D.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://dquajgwn2quu5" 6 | path="res://.godot/imported/SaveElement2D.svg-678cee46d199b92aacdeef575a48b642.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/saveEngine/SaveElement2D.svg" 14 | dest_files=["res://.godot/imported/SaveElement2D.svg-678cee46d199b92aacdeef575a48b642.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/saveEngine/SaveElement3D.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://dr7vk6xtly7vv" 6 | path="res://.godot/imported/SaveElement3D.svg-ef99cb334e841d04753b1076b571b69f.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/saveEngine/SaveElement3D.svg" 14 | dest_files=["res://.godot/imported/SaveElement3D.svg-ef99cb334e841d04753b1076b571b69f.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 CiaNCI Studio 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /example/player.gd: -------------------------------------------------------------------------------- 1 | extends CharacterBody2D 2 | 3 | const SPEED = 300.0 4 | const JUMP_VELOCITY = -400.0 5 | 6 | @export var Hp : int = 100 7 | @onready var sprite: Sprite2D = $Sprite 8 | @onready var hp_label: Label = $CanvasLayer/HpLabel 9 | 10 | 11 | func _physics_process(delta: float) -> void: 12 | 13 | hp_label.text = str(Hp) 14 | 15 | # Add the gravity. 16 | if not is_on_floor(): 17 | velocity += get_gravity() * delta 18 | 19 | # Handle jump. 20 | if Input.is_action_just_pressed("jump") and is_on_floor(): 21 | velocity.y = JUMP_VELOCITY 22 | 23 | # Get the input direction and handle the movement/deceleration. 24 | # As good practice, you should replace UI actions with custom gameplay actions. 25 | var direction := Input.get_axis("left", "right") 26 | if direction: 27 | velocity.x = direction * SPEED 28 | if velocity.x > 0: 29 | sprite.scale.x = -1 30 | else: 31 | sprite.scale.x = 1 32 | else: 33 | velocity.x = move_toward(velocity.x, 0, SPEED) 34 | 35 | move_and_slide() 36 | 37 | 38 | func _on_contact_area_body_entered(body: Node2D) -> void: 39 | if body.is_in_group("enemy"): 40 | Hp = Hp - 10 41 | hp_label.text = str(Hp) 42 | 43 | -------------------------------------------------------------------------------- /example/example.gd: -------------------------------------------------------------------------------- 1 | extends Control 2 | 3 | @onready var slots_stack: VBoxContainer = $Scroll/SlotsStack 4 | @onready var new_game_name: TextEdit = $HBoxContainer/NewGameName 5 | 6 | var saveSlots : Array[SaveFile] = [] 7 | 8 | func _ready() -> void: 9 | LoadSlots() 10 | 11 | func LoadSlots(): 12 | saveSlots = SaveService.GetSlots() 13 | for child in slots_stack.get_children(): 14 | child.queue_free() 15 | for slot in saveSlots: 16 | var slotView = load("res://example/SaveSlot.tscn").instantiate() as SaveSlot 17 | slotView.saveSlot = slot 18 | slotView.Load.connect(LoadGame) 19 | slotView.Delete.connect(DeleteSlot) 20 | slots_stack.add_child(slotView) 21 | 22 | func LoadGame(slot : SaveFile): 23 | SaveService.LoadGame(slot.SlotId) 24 | get_tree().change_scene_to_file(SaveService.CurrentLoadedSlot.CurrentSceneId) 25 | 26 | func DeleteSlot(slot : SaveFile): 27 | SaveService.DeleteSlot(slot.SlotId) 28 | LoadSlots() 29 | 30 | func NewGame(): 31 | var name = new_game_name.text.strip_edges() 32 | if name.is_empty(): 33 | return 34 | if saveSlots.any(func(item : SaveFile): return item.Name == name): 35 | return 36 | var slotId = name.replace(" ", "_") 37 | SaveService.NewSlot(slotId, name, "res://example/Game.tscn") 38 | LoadSlots() 39 | 40 | func _on_new_game_pressed() -> void: 41 | NewGame() 42 | -------------------------------------------------------------------------------- /images/Add.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 33 | 38 | 39 | -------------------------------------------------------------------------------- /images/Save.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 33 | 38 | 39 | -------------------------------------------------------------------------------- /example/Player.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=6 format=3 uid="uid://bkejndsxx5kat"] 2 | 3 | [ext_resource type="Script" path="res://example/player.gd" id="1_b2lvn"] 4 | [ext_resource type="Texture2D" uid="uid://sduh5pg5rbab" path="res://images/Player.svg" id="1_w5kk0"] 5 | [ext_resource type="Script" path="res://addons/saveEngine/scripts/SaveElement2D.gd" id="3_necci"] 6 | 7 | [sub_resource type="CapsuleShape2D" id="CapsuleShape2D_1qdt6"] 8 | radius = 29.0 9 | height = 74.0 10 | 11 | [sub_resource type="CapsuleShape2D" id="CapsuleShape2D_iilbe"] 12 | radius = 32.0 13 | height = 78.0 14 | 15 | [node name="Player" type="CharacterBody2D"] 16 | script = ExtResource("1_b2lvn") 17 | 18 | [node name="CollisionShape2D" type="CollisionShape2D" parent="."] 19 | shape = SubResource("CapsuleShape2D_1qdt6") 20 | 21 | [node name="Sprite" type="Sprite2D" parent="."] 22 | texture = ExtResource("1_w5kk0") 23 | 24 | [node name="ContactArea" type="Area2D" parent="."] 25 | 26 | [node name="CollisionShape2D" type="CollisionShape2D" parent="ContactArea"] 27 | shape = SubResource("CapsuleShape2D_iilbe") 28 | 29 | [node name="CanvasLayer" type="CanvasLayer" parent="."] 30 | 31 | [node name="HpLabel" type="Label" parent="CanvasLayer"] 32 | offset_left = 13.0 33 | offset_top = 14.0 34 | offset_right = 53.0 35 | offset_bottom = 37.0 36 | theme_override_font_sizes/font_size = 52 37 | text = "100" 38 | 39 | [node name="SaveElement2D" type="Node" parent="."] 40 | script = ExtResource("3_necci") 41 | SaveProperties = Array[String](["Hp"]) 42 | 43 | [connection signal="body_entered" from="ContactArea" to="." method="_on_contact_area_body_entered"] 44 | -------------------------------------------------------------------------------- /images/Remove.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 33 | 38 | 39 | -------------------------------------------------------------------------------- /example/save_slot.gd: -------------------------------------------------------------------------------- 1 | extends Control 2 | class_name SaveSlot 3 | 4 | signal Load(slot : SaveFile) 5 | signal Delete(slot : SaveFile) 6 | 7 | @export var saveSlot : SaveFile 8 | @onready var name_label: Label = $HBoxContainer/VBoxContainer/NameGroup/NameLabel 9 | @onready var date_label: Label = $HBoxContainer/VBoxContainer/DateGroup/DateLabel 10 | @onready var game_time_label: Label = $HBoxContainer/VBoxContainer/GameTimeGroup/GameTimeLabel 11 | @onready var slot_image: TextureRect = $HBoxContainer/ImageMargin/SlotImage 12 | 13 | func _ready() -> void: 14 | LoadData() 15 | 16 | func LoadData(): 17 | name_label.text = saveSlot.Name 18 | date_label.text = Time.get_datetime_string_from_datetime_dict(saveSlot.DateTime, true) 19 | game_time_label.text = FormatGameTime() 20 | if saveSlot.MetaData.has("image"): 21 | slot_image.texture = load(saveSlot.MetaData["image"]) 22 | 23 | func _on_load_game_pressed() -> void: 24 | Load.emit(saveSlot) 25 | 26 | func _on_delete_slot_pressed() -> void: 27 | Delete.emit(saveSlot) 28 | 29 | func FormatGameTime() -> String: 30 | var result = "" 31 | var fullSeconds = saveSlot.GameTime 32 | var hours = fullSeconds / 3600 33 | fullSeconds = fullSeconds - hours * 3600 34 | var minutes = fullSeconds / 60 35 | fullSeconds = fullSeconds - minutes * 60 36 | var seconds = fullSeconds 37 | if hours < 10: 38 | result = result + "0" + str(hours) + ":" 39 | else: 40 | result = result + str(hours) + ":" 41 | if minutes < 10: 42 | result = result + "0" + str(minutes) + ":" 43 | else: 44 | result = result + str(minutes) + ":" 45 | if seconds < 10: 46 | result = result + "0" + str(seconds) 47 | else: 48 | result = result + str(seconds) 49 | return result 50 | -------------------------------------------------------------------------------- /addons/saveEngine/scripts/SaveElement3D.gd: -------------------------------------------------------------------------------- 1 | extends SaveElementBase 2 | class_name SaveElement3D 3 | 4 | func Serialize() -> SaveFileNode: 5 | var parent = get_parent() 6 | var result := SaveFileNode.new() 7 | result.NodeTreePath = parent.get_path() 8 | result.ParentTreePath = parent.get_parent().get_path() 9 | var script = parent.get_script() 10 | if script: 11 | result.NodeScriptPath = script.resource_path 12 | result.NodeFilePath = parent.scene_file_path 13 | if SaveTransform: 14 | result.NodeProperties.get_or_add("position", SaveSerializationHelper.SerializeVariable(parent.position)) 15 | result.NodeProperties.get_or_add("rotation", SaveSerializationHelper.SerializeVariable(parent.rotation)) 16 | result.NodeProperties.get_or_add("scale", SaveSerializationHelper.SerializeVariable(parent.scale)) 17 | result.NodeProperties.get_or_add("top_level", SaveSerializationHelper.SerializeVariable(parent.top_level)) 18 | result.NodeProperties.get_or_add("rotation_edit_mode", SaveSerializationHelper.SerializeVariable(parent.rotation_edit_mode)) 19 | result.NodeProperties.get_or_add("rotation_order", SaveSerializationHelper.SerializeVariable(parent.rotation_order)) 20 | if SaveVisibility: 21 | result.NodeProperties.get_or_add("visible", SaveSerializationHelper.SerializeVariable(parent.visible)) 22 | for property in SaveProperties: 23 | if property.contains(":"): 24 | var nodePath = property.split(":")[0] 25 | var propertyPath = property.split(":")[1] 26 | var node = parent.get_node(nodePath) 27 | result.NodeProperties.get_or_add(property, SaveSerializationHelper.SerializeVariable(node.get(propertyPath))) 28 | else: 29 | result.NodeProperties.get_or_add(property, SaveSerializationHelper.SerializeVariable(parent.get(property))) 30 | 31 | return result 32 | -------------------------------------------------------------------------------- /example/example.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=3 uid="uid://soopr2v4ywxe"] 2 | 3 | [ext_resource type="Script" path="res://example/example.gd" id="1_efwug"] 4 | [ext_resource type="Texture2D" uid="uid://tgoufab05dru" path="res://images/Add.svg" id="2_7cmod"] 5 | 6 | [node name="Test" type="Control"] 7 | layout_mode = 3 8 | anchors_preset = 15 9 | anchor_right = 1.0 10 | anchor_bottom = 1.0 11 | grow_horizontal = 2 12 | grow_vertical = 2 13 | script = ExtResource("1_efwug") 14 | 15 | [node name="Scroll" type="ScrollContainer" parent="."] 16 | layout_mode = 1 17 | anchors_preset = 8 18 | anchor_left = 0.5 19 | anchor_top = 0.5 20 | anchor_right = 0.5 21 | anchor_bottom = 0.5 22 | offset_left = -314.0 23 | offset_top = -193.0 24 | offset_right = 314.0 25 | offset_bottom = 193.0 26 | grow_horizontal = 2 27 | grow_vertical = 2 28 | 29 | [node name="SlotsStack" type="VBoxContainer" parent="Scroll"] 30 | layout_mode = 2 31 | size_flags_horizontal = 3 32 | size_flags_vertical = 3 33 | 34 | [node name="HBoxContainer" type="HBoxContainer" parent="."] 35 | layout_mode = 1 36 | anchors_preset = 8 37 | anchor_left = 0.5 38 | anchor_top = 0.5 39 | anchor_right = 0.5 40 | anchor_bottom = 0.5 41 | offset_left = -387.0 42 | offset_top = 201.0 43 | offset_right = 305.0 44 | offset_bottom = 241.0 45 | grow_horizontal = 2 46 | grow_vertical = 2 47 | alignment = 2 48 | 49 | [node name="Label" type="Label" parent="HBoxContainer"] 50 | layout_mode = 2 51 | text = "New Game Name:" 52 | 53 | [node name="NewGameName" type="TextEdit" parent="HBoxContainer"] 54 | layout_mode = 2 55 | size_flags_horizontal = 3 56 | scroll_fit_content_height = true 57 | 58 | [node name="NewGame" type="Button" parent="HBoxContainer"] 59 | layout_mode = 2 60 | icon = ExtResource("2_7cmod") 61 | flat = true 62 | 63 | [connection signal="pressed" from="HBoxContainer/NewGame" to="." method="_on_new_game_pressed"] 64 | -------------------------------------------------------------------------------- /images/Load.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 33 | 38 | 39 | -------------------------------------------------------------------------------- /addons/saveEngine/scripts/SaveElement2D.gd: -------------------------------------------------------------------------------- 1 | extends SaveElementBase 2 | class_name SaveElement2D 3 | 4 | func Serialize() -> SaveFileNode: 5 | var parent = get_parent() 6 | var result := SaveFileNode.new() 7 | result.NodeTreePath = parent.get_path() 8 | result.ParentTreePath = parent.get_parent().get_path() 9 | var script = parent.get_script() 10 | if script: 11 | result.NodeScriptPath = script.resource_path 12 | result.NodeFilePath = parent.scene_file_path 13 | if SaveTransform: 14 | result.NodeProperties.get_or_add("position", SaveSerializationHelper.SerializeVariable(parent.position)) 15 | result.NodeProperties.get_or_add("rotation", SaveSerializationHelper.SerializeVariable(parent.rotation)) 16 | result.NodeProperties.get_or_add("scale", SaveSerializationHelper.SerializeVariable(parent.scale)) 17 | result.NodeProperties.get_or_add("skew", SaveSerializationHelper.SerializeVariable(parent.skew)) 18 | if SaveVisibility: 19 | result.NodeProperties.get_or_add("visible", SaveSerializationHelper.SerializeVariable(parent.visible)) 20 | result.NodeProperties.get_or_add("modulate", SaveSerializationHelper.SerializeVariable(parent.modulate)) 21 | result.NodeProperties.get_or_add("self_modulate", SaveSerializationHelper.SerializeVariable(parent.self_modulate)) 22 | result.NodeProperties.get_or_add("show_behind_parent", SaveSerializationHelper.SerializeVariable(parent.show_behind_parent)) 23 | result.NodeProperties.get_or_add("light_mask", SaveSerializationHelper.SerializeVariable(parent.light_mask)) 24 | result.NodeProperties.get_or_add("visibility_layer", SaveSerializationHelper.SerializeVariable(parent.visibility_layer)) 25 | 26 | for property in SaveProperties: 27 | if property.contains(":"): 28 | var nodePath = property.split(":")[0] 29 | var propertyPath = property.split(":")[1] 30 | var node = parent.get_node(nodePath) 31 | result.NodeProperties.get_or_add(property, SaveSerializationHelper.SerializeVariable(node.get(propertyPath))) 32 | else: 33 | result.NodeProperties.get_or_add(property, SaveSerializationHelper.SerializeVariable(parent.get(property))) 34 | 35 | return result 36 | -------------------------------------------------------------------------------- /icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 33 | 38 | 39 | -------------------------------------------------------------------------------- /addons/saveEngine/SaveAgent.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 33 | 38 | 39 | -------------------------------------------------------------------------------- /addons/saveEngine/SaveElement2D.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 33 | 38 | 39 | -------------------------------------------------------------------------------- /addons/saveEngine/SaveElement3D.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 33 | 38 | 39 | -------------------------------------------------------------------------------- /images/Player.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 33 | 38 | 39 | -------------------------------------------------------------------------------- /example/Game.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=8 format=3 uid="uid://gl11optf4etu"] 2 | 3 | [ext_resource type="PackedScene" uid="uid://bkejndsxx5kat" path="res://example/Player.tscn" id="1_jpn4t"] 4 | [ext_resource type="Script" path="res://example/game.gd" id="1_oyjxt"] 5 | [ext_resource type="Texture2D" uid="uid://sduh5pg5rbab" path="res://images/Player.svg" id="2_oydj2"] 6 | [ext_resource type="Script" path="res://addons/saveEngine/scripts/SaveAgent.gd" id="3_sq3qb"] 7 | 8 | [sub_resource type="QuadMesh" id="QuadMesh_2hhh8"] 9 | 10 | [sub_resource type="RectangleShape2D" id="RectangleShape2D_3dfmn"] 11 | size = Vector2(1.00043, 0.983957) 12 | 13 | [sub_resource type="RectangleShape2D" id="RectangleShape2D_5bbrv"] 14 | size = Vector2(100, 110) 15 | 16 | [node name="Game" type="Node2D"] 17 | script = ExtResource("1_oyjxt") 18 | 19 | [node name="Floor" type="StaticBody2D" parent="."] 20 | position = Vector2(580, 600) 21 | 22 | [node name="MeshInstance2D" type="MeshInstance2D" parent="Floor"] 23 | position = Vector2(-3, 0) 24 | scale = Vector2(1151.5, 93.5) 25 | mesh = SubResource("QuadMesh_2hhh8") 26 | 27 | [node name="CollisionShape2D" type="CollisionShape2D" parent="Floor"] 28 | position = Vector2(-3, 0) 29 | scale = Vector2(1151.5, 93.5) 30 | shape = SubResource("RectangleShape2D_3dfmn") 31 | 32 | [node name="Player" parent="." instance=ExtResource("1_jpn4t")] 33 | position = Vector2(223, 425) 34 | 35 | [node name="Enemy" type="StaticBody2D" parent="." groups=["enemy"]] 36 | position = Vector2(1096, 502) 37 | 38 | [node name="CollisionShape2D" type="CollisionShape2D" parent="Enemy"] 39 | position = Vector2(4, -5) 40 | shape = SubResource("RectangleShape2D_5bbrv") 41 | 42 | [node name="Sprite2D" type="Sprite2D" parent="Enemy"] 43 | modulate = Color(1, 0, 0, 1) 44 | scale = Vector2(1.48761, 1.58366) 45 | texture = ExtResource("2_oydj2") 46 | 47 | [node name="Enemy2" type="StaticBody2D" parent="." groups=["enemy"]] 48 | position = Vector2(48, 502) 49 | 50 | [node name="CollisionShape2D" type="CollisionShape2D" parent="Enemy2"] 51 | position = Vector2(4, -5) 52 | shape = SubResource("RectangleShape2D_5bbrv") 53 | 54 | [node name="Sprite2D" type="Sprite2D" parent="Enemy2"] 55 | modulate = Color(1, 0, 0, 1) 56 | scale = Vector2(-1.488, 1.584) 57 | texture = ExtResource("2_oydj2") 58 | 59 | [node name="SaveAgent" type="Node" parent="."] 60 | script = ExtResource("3_sq3qb") 61 | AutoSavePeriod = 30 62 | 63 | [node name="CanvasLayer" type="CanvasLayer" parent="."] 64 | 65 | [node name="SavingLabel" type="Label" parent="CanvasLayer"] 66 | visible = false 67 | anchors_preset = 1 68 | anchor_left = 1.0 69 | anchor_right = 1.0 70 | offset_left = -158.0 71 | offset_top = 19.0 72 | offset_right = -31.0 73 | offset_bottom = 64.0 74 | grow_horizontal = 0 75 | theme_override_font_sizes/font_size = 32 76 | text = "Saving..." 77 | 78 | [connection signal="FinishAutoSaving" from="SaveAgent" to="." method="_on_save_agent_finish_auto_saving"] 79 | [connection signal="StartAutoSaving" from="SaveAgent" to="." method="_on_save_agent_start_auto_saving"] 80 | -------------------------------------------------------------------------------- /project.godot: -------------------------------------------------------------------------------- 1 | ; Engine configuration file. 2 | ; It's best edited using the editor UI and not directly, 3 | ; since the parameters that go here are not all obvious. 4 | ; 5 | ; Format: 6 | ; [section] ; section goes between [] 7 | ; param=value ; assign values to parameters 8 | 9 | config_version=5 10 | 11 | [application] 12 | 13 | config/name="Save Engine" 14 | run/main_scene="res://example/example.tscn" 15 | config/features=PackedStringArray("4.3", "Forward Plus") 16 | 17 | [autoload] 18 | 19 | SaveService="*res://addons/saveEngine/scripts/SaveService.gd" 20 | 21 | [editor_plugins] 22 | 23 | enabled=PackedStringArray("res://addons/saveEngine/plugin.cfg") 24 | 25 | [global_group] 26 | 27 | enemy="" 28 | 29 | [input] 30 | 31 | jump={ 32 | "deadzone": 0.5, 33 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":32,"key_label":0,"unicode":32,"location":0,"echo":false,"script":null) 34 | , Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":1,"canceled":false,"pressed":false,"double_click":false,"script":null) 35 | , Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":119,"location":0,"echo":false,"script":null) 36 | ] 37 | } 38 | left={ 39 | "deadzone": 0.5, 40 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"location":0,"echo":false,"script":null) 41 | , Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194319,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) 42 | ] 43 | } 44 | right={ 45 | "deadzone": 0.5, 46 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null) 47 | , Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194321,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /addons/saveEngine/scripts/SaveAgent.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | class_name SaveAgent 3 | 4 | signal StartAutoSaving 5 | signal FinishAutoSaving 6 | signal StartSaving 7 | signal FinishSaving 8 | signal StartLoading 9 | signal FinishLoading 10 | 11 | @export var AutoSave : bool = true 12 | @export var AutoSavePeriod : int = 60 13 | 14 | var parent : Node 15 | var autoSaveTimer : Timer 16 | 17 | func _enter_tree() -> void: 18 | parent = get_parent() 19 | autoSaveTimer = Timer.new() 20 | autoSaveTimer.autostart = AutoSave 21 | autoSaveTimer.wait_time = AutoSavePeriod 22 | autoSaveTimer.timeout.connect(__AutoSave) 23 | add_child(autoSaveTimer) 24 | LoadSceneData() 25 | 26 | func LoadSceneData(): 27 | StartLoading.emit() 28 | if not SaveService.CurrentLoadedSlot: 29 | FinishLoading.emit() 30 | return 31 | if not SaveService.CurrentLoadedSlot.ScenesData.any(func(item: SaveFileScene) : return item.ScenePath == parent.scene_file_path): 32 | FinishLoading.emit() 33 | return 34 | get_tree().paused = true 35 | var scene : SaveFileScene = SaveService.CurrentLoadedSlot.ScenesData.filter(func(item: SaveFileScene) : return item.ScenePath == parent.scene_file_path)[0] 36 | var persistentNodes = get_tree().get_nodes_in_group(SaveService.PERSISTENCE_GROUP) 37 | 38 | #Remove all persistent nodes: 39 | for persistent in persistentNodes: 40 | persistent.get_parent().queue_free() 41 | 42 | #Re-Add 43 | for persistent in scene.SceneNodes: 44 | var instance = load(persistent.NodeFilePath).instantiate() 45 | parent.get_node(persistent.ParentTreePath).add_child.call_deferred(instance) 46 | instance.request_ready() 47 | await instance.ready 48 | var saveElement = instance.get_children().filter(func(child : Node): return child is SaveElement3D or child is SaveElement2D) 49 | if len(saveElement) > 0: 50 | saveElement[0].Load(persistent) 51 | get_tree().paused = false 52 | FinishLoading.emit() 53 | 54 | 55 | func SaveSceneData(auto = false): 56 | if not SaveService.CurrentLoadedSlot: 57 | SaveService.NewSlot() 58 | 59 | if not auto: 60 | StartSaving.emit() 61 | else: 62 | StartAutoSaving.emit() 63 | 64 | var sceneData = SaveFileScene.new() 65 | sceneData.ScenePath = parent.scene_file_path 66 | 67 | var persistentNodes = get_tree().get_nodes_in_group(SaveService.PERSISTENCE_GROUP) 68 | for persistent in persistentNodes: 69 | sceneData.SceneNodes.append(persistent.Serialize()) 70 | 71 | if SaveService.CurrentLoadedSlot.ScenesData.any(func(item: SaveFileScene) : return item.ScenePath == parent.scene_file_path): 72 | var currentIndex = SaveService.CurrentLoadedSlot.ScenesData.find(SaveService.CurrentLoadedSlot.ScenesData.filter(func(item: SaveFileScene) : return item.ScenePath == parent.scene_file_path)[0]) 73 | SaveService.CurrentLoadedSlot.ScenesData.remove_at(currentIndex) 74 | SaveService.CurrentLoadedSlot.ScenesData.append(sceneData) 75 | SaveService.SaveGame() 76 | 77 | await SaveService.saveCompleted 78 | 79 | if not auto: 80 | FinishSaving.emit() 81 | else: 82 | FinishAutoSaving.emit() 83 | 84 | func __AutoSave(): 85 | SaveSceneData(true) 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SaveEngine 2 | 3 | ### Save and load game mechanics with slots and autosave. 4 | 5 | ## Services: 6 | 7 | ### SaveService 8 | 9 | It's the main singleton service of the plugin, used to save, load, delete and list save slots. 10 | 11 | Save files will be validated by hash and salt. 12 | It will save singleton classes as global data. 13 | Which global classes must be saved can be configured on the auto generated file: Data/Settings/saveSettings.cfg 14 | 15 | **Methods:** 16 | 17 | * **NewSlot(slotId : String = "", name : String = "", currentScene : String = "", metaData : Dictionary = {}) -> bool** 18 | * Create a new save slot 19 | 20 | * **DeleteSlot(slotId : String) -> bool:** 21 | * Delete an saved slot 22 | 23 | * **SaveGame(autoSetCurrentScene = true) -> bool** 24 | * Save a loaded game slot 25 | 26 | * **LoadGame(slotId : String) -> bool** 27 | * Load a game slot 28 | 29 | * **GetSlots() -> Array[SaveFile]** 30 | * Get all saved slots (files) 31 | 32 | ### Configuration File: 33 | 34 | ```json 35 | { 36 | "HashSalt":"HASH salt (used to validate save files, can be any text or hash)", 37 | "SaveFilesPrefix":"prefix to add to every save file, can be empty", 38 | "SaveFolderPath":"default save file folder, default: user://Saves/", 39 | "StaticScriptsToSave":["List of singleton class to be saved as global data, if empty will save all singletons (no recommended)"] 40 | } 41 | ``` 42 | 43 | ### SaveAgent 44 | 45 | Must be placed on a scene that pretend to be saved, handles all saving process for the scene, there's no need to call SaveService methods on the scene. 46 | If there’s an loaded slot and in this slot there’s data related to the scene, will automatically apply the saved data to the nodes that contains the SaveElement child. 47 | 48 | ![image](images/doc1.png) 49 | 50 | **Signals:** 51 | 52 | * **StartAutoSaving** 53 | * **FinishAutoSaving** 54 | * **StartSaving** 55 | * **FinishSaving** 56 | * **StartLoading** 57 | * **FinishLoading** 58 | 59 | **Properties:** 60 | 61 | * **AutoSave : bool = true** 62 | * Enable autosave 63 | * **AutoSavePeriod : int = 60** 64 | * Auto save period in seconds 65 | 66 | **Methods:** 67 | 68 | * **LoadSceneData()** 69 | * Manually trigger the load scene data 70 | 71 | * **SaveSceneData(auto = false)** 72 | * Manually trigger the save scene data 73 | 74 | ### SaveElement2D and SaveElement3D 75 | 76 | Must be placed on a node or scene that pretends to have the properties saved, handles the serialization and load of the object. 77 | Used by the SaveAgent to save and load the scene elements. 78 | Specific properties to be saved can be set on the inspector. 79 | 80 | ![image](images/doc2.png) 81 | ![image](images/doc3.png) 82 | 83 | **Properties:** 84 | 85 | * **SaveTransform : bool = true** 86 | * Save the transform properties of the parent node 87 | * **SaveVisibility : bool = true** 88 | * Save the visibility properties of the parent node 89 | * **SaveProperties : Array[String] = []** 90 | * List of properties to be saved. 91 | * Can save child nodes properties, use following pattern: "nodePath:property" 92 | ex:. "geometry:rotation" 93 | 94 | **Methods (used internally be SaveAgent):** 95 | 96 | * **Load(data : SaveFileNode)** 97 | * Load parent properties 98 | 99 | * **Serialize() -> SaveFileNode** 100 | * Serialize parent properties 101 | 102 | ### About 103 | 104 | By Cianci 105 | 106 | KelvysB. 107 | 108 | Check Cianci Tutorials (Brazilian Portuguese): 109 | 110 | https://www.youtube.com/@CiaNCIStudio 111 | 112 | -------------------------------------------------------------------------------- /example/SaveSlot.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=5 format=3 uid="uid://1nfjxoy27gmu"] 2 | 3 | [ext_resource type="Script" path="res://example/save_slot.gd" id="1_ou5vv"] 4 | [ext_resource type="Texture2D" uid="uid://btq06kdtsv3l4" path="res://images/Save.svg" id="2_adn1g"] 5 | [ext_resource type="Texture2D" uid="uid://crobxyirk3dqt" path="res://images/Load.svg" id="5_6lqsp"] 6 | [ext_resource type="Texture2D" uid="uid://d0x7lwfg088xo" path="res://images/Remove.svg" id="6_45lry"] 7 | 8 | [node name="SaveSlot" type="Control"] 9 | custom_minimum_size = Vector2(100, 80) 10 | layout_mode = 3 11 | anchors_preset = 0 12 | offset_right = 1153.0 13 | offset_bottom = 80.0 14 | size_flags_horizontal = 3 15 | script = ExtResource("1_ou5vv") 16 | 17 | [node name="Background" type="ColorRect" parent="."] 18 | layout_mode = 1 19 | anchors_preset = 15 20 | anchor_right = 1.0 21 | anchor_bottom = 1.0 22 | grow_horizontal = 2 23 | grow_vertical = 2 24 | color = Color(0.223103, 0.223103, 0.223103, 1) 25 | 26 | [node name="HBoxContainer" type="HBoxContainer" parent="."] 27 | layout_mode = 2 28 | offset_left = 20.0 29 | offset_top = 3.0 30 | offset_right = 1132.0 31 | offset_bottom = 75.0 32 | grow_horizontal = 2 33 | grow_vertical = 2 34 | theme_override_constants/separation = 21 35 | 36 | [node name="ImageMargin" type="MarginContainer" parent="HBoxContainer"] 37 | layout_mode = 2 38 | 39 | [node name="SlotImage" type="TextureRect" parent="HBoxContainer/ImageMargin"] 40 | layout_mode = 2 41 | texture = ExtResource("2_adn1g") 42 | expand_mode = 2 43 | stretch_mode = 4 44 | 45 | [node name="VBoxContainer" type="VBoxContainer" parent="HBoxContainer"] 46 | layout_mode = 2 47 | alignment = 1 48 | 49 | [node name="NameGroup" type="HBoxContainer" parent="HBoxContainer/VBoxContainer"] 50 | layout_mode = 2 51 | 52 | [node name="Label" type="Label" parent="HBoxContainer/VBoxContainer/NameGroup"] 53 | layout_mode = 2 54 | text = "Name: " 55 | 56 | [node name="NameLabel" type="Label" parent="HBoxContainer/VBoxContainer/NameGroup"] 57 | layout_mode = 2 58 | text = "Save Name" 59 | 60 | [node name="DateGroup" type="HBoxContainer" parent="HBoxContainer/VBoxContainer"] 61 | layout_mode = 2 62 | 63 | [node name="Label" type="Label" parent="HBoxContainer/VBoxContainer/DateGroup"] 64 | layout_mode = 2 65 | text = "Date: " 66 | 67 | [node name="DateLabel" type="Label" parent="HBoxContainer/VBoxContainer/DateGroup"] 68 | layout_mode = 2 69 | text = "Dat Time" 70 | 71 | [node name="GameTimeGroup" type="HBoxContainer" parent="HBoxContainer/VBoxContainer"] 72 | layout_mode = 2 73 | 74 | [node name="Label" type="Label" parent="HBoxContainer/VBoxContainer/GameTimeGroup"] 75 | layout_mode = 2 76 | text = "Game Time: " 77 | 78 | [node name="GameTimeLabel" type="Label" parent="HBoxContainer/VBoxContainer/GameTimeGroup"] 79 | layout_mode = 2 80 | text = "Game Time" 81 | 82 | [node name="ButtonsMargin" type="MarginContainer" parent="HBoxContainer"] 83 | layout_mode = 2 84 | 85 | [node name="ButtonsGroup" type="HBoxContainer" parent="HBoxContainer/ButtonsMargin"] 86 | layout_mode = 2 87 | theme_override_constants/separation = 17 88 | 89 | [node name="LoadGame" type="Button" parent="HBoxContainer/ButtonsMargin/ButtonsGroup"] 90 | layout_mode = 2 91 | icon = ExtResource("5_6lqsp") 92 | icon_alignment = 1 93 | 94 | [node name="DeleteSlot" type="Button" parent="HBoxContainer/ButtonsMargin/ButtonsGroup"] 95 | layout_mode = 2 96 | icon = ExtResource("6_45lry") 97 | icon_alignment = 1 98 | 99 | [connection signal="pressed" from="HBoxContainer/ButtonsMargin/ButtonsGroup/LoadGame" to="." method="_on_load_game_pressed"] 100 | [connection signal="pressed" from="HBoxContainer/ButtonsMargin/ButtonsGroup/DeleteSlot" to="." method="_on_delete_slot_pressed"] 101 | -------------------------------------------------------------------------------- /addons/saveEngine/scripts/SerializationHelper.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | class_name SaveSerializationHelper 3 | 4 | static func SerializeVariable(variable): 5 | match typeof(variable): 6 | TYPE_NIL: 7 | return null 8 | TYPE_VECTOR2: 9 | return __SerializeVector2(variable) 10 | TYPE_VECTOR3: 11 | return __SerializeVector3(variable) 12 | TYPE_COLOR: 13 | return __SerializeColor(variable) 14 | TYPE_BASIS: 15 | return __SerializeBasis(variable) 16 | TYPE_TRANSFORM3D: 17 | return __SerializeTransform(variable) 18 | TYPE_TRANSFORM2D: 19 | return __SerializeTransform2d(variable) 20 | TYPE_ARRAY: 21 | return __SerializeArray(variable) 22 | TYPE_DICTIONARY: 23 | return __SerializeDictionary(variable) 24 | return variable 25 | 26 | static func DeserializeVariable(input): 27 | if typeof(input) == TYPE_NIL: 28 | return null 29 | 30 | if typeof(input) != TYPE_DICTIONARY: 31 | return input 32 | 33 | match input.type: 34 | "vector2": 35 | return __DeserializeVector2(input) 36 | "vector3": 37 | return __DeserializeVector3(input) 38 | "color": 39 | return __DeserializeColor(input) 40 | "basis": 41 | return __DeserializeBasis(input) 42 | "transform": 43 | return __DeserializeTransform(input) 44 | "transform2d": 45 | return __DeserializeTransform2d(input) 46 | "array": 47 | return __DeserializeArray(input) 48 | "dictionary": 49 | return __DeserializeDictionary(input) 50 | return input 51 | 52 | static func __SerializeVector2(input): 53 | return { 54 | "type": "vector2", 55 | "x" : input.x, 56 | "y" : input.y 57 | } 58 | 59 | static func __SerializeVector3(input): 60 | return { 61 | "type": "vector3", 62 | "x" : input.x, 63 | "y" : input.y, 64 | "z" : input.z 65 | } 66 | 67 | static func __SerializeColor(input): 68 | return { 69 | "type": "color", 70 | "r" : input.r, 71 | "g" : input.g, 72 | "b" : input.b, 73 | "a" : input.a 74 | } 75 | 76 | static func __SerializeBasis(input): 77 | return { 78 | "type": "basis", 79 | "x": SerializeVariable(input.x), 80 | "y": SerializeVariable(input.y), 81 | "z": SerializeVariable(input.z) 82 | } 83 | 84 | static func __SerializeTransform(input): 85 | return { 86 | "type": "transform", 87 | "basis": SerializeVariable(input.basis), 88 | "origin": SerializeVariable(input.origin) 89 | } 90 | 91 | static func __SerializeTransform2d(input): 92 | return { 93 | "type": "transform2d", 94 | "x": SerializeVariable(input.x), 95 | "y": SerializeVariable(input.y), 96 | "origin": SerializeVariable(input.origin) 97 | } 98 | 99 | static func __SerializeArray(input): 100 | var array = [] 101 | for entry in input: 102 | array.push_back(SerializeVariable(entry)) 103 | return { 104 | "type": "array", 105 | "data": array 106 | } 107 | 108 | static func __SerializeDictionary(input): 109 | var dict = {} 110 | for entry in input: 111 | dict[entry] = SerializeVariable(input[entry]) 112 | return { 113 | "type": "dictionary", 114 | "data": dict 115 | } 116 | 117 | static func __DeserializeVector2(input): 118 | return Vector2( 119 | input.x, 120 | input.y 121 | ) 122 | 123 | static func __DeserializeVector3(input): 124 | return Vector3( 125 | input.x, 126 | input.y, 127 | input.z 128 | ) 129 | 130 | static func __DeserializeColor(input): 131 | return Color( 132 | input.r, 133 | input.g, 134 | input.b, 135 | input.a 136 | ) 137 | 138 | static func __DeserializeBasis(input): 139 | return Basis( 140 | __DeserializeVector3(input.x), 141 | __DeserializeVector3(input.y), 142 | __DeserializeVector3(input.z) 143 | ) 144 | 145 | static func __DeserializeTransform(input): 146 | return Transform3D( 147 | __DeserializeBasis(input.basis), 148 | __DeserializeVector3(input.origin) 149 | ) 150 | 151 | static func __DeserializeTransform2d(input): 152 | return Transform2D( 153 | __DeserializeVector2(input.x), 154 | __DeserializeVector2(input.y), 155 | __DeserializeVector2(input.origin) 156 | ) 157 | 158 | static func __DeserializeArray(input): 159 | var array = [] 160 | for entry in input.data: 161 | array.push_back(DeserializeVariable(entry)) 162 | return array 163 | 164 | static func __DeserializeDictionary(input): 165 | var dict = {} 166 | for entry in input.data: 167 | dict[entry] = DeserializeVariable(input.data[entry]) 168 | return dict 169 | -------------------------------------------------------------------------------- /addons/saveEngine/scripts/SaveService.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | signal loadCompleted 4 | signal loadStarted 5 | signal saveCompleted 6 | signal saveStarted 7 | 8 | const PERSISTENCE_GROUP : String = "persist" 9 | const SETTINGS_FOLDER_PATH : String = "res://Data/Settings/" 10 | const SETTINGS_FILE_NAME : String = "saveSettings.cfg" 11 | const IGNORE_GLOBAL_PROPERTIES : Array[String] = ["Auto Translate", 12 | "Editor Description", 13 | "GameState.gd", 14 | "Node", 15 | "Physics Interpolation", 16 | "Process", 17 | "Thread Group", 18 | "_import_path", 19 | "auto_translate_mode", 20 | "editor_description", 21 | "multiplayer", 22 | "name", 23 | "owner", 24 | "physics_interpolation_mode", 25 | "process_mode", 26 | "process_physics_priority", 27 | "process_priority", 28 | "process_thread_group", 29 | "process_thread_group_order", 30 | "process_thread_messages", 31 | "scene_file_path", 32 | "script", 33 | "unique_name_in_owner"] 34 | 35 | @export var CurrentLoadedSlot : SaveFile 36 | @export var Settings : SaveSettigns 37 | 38 | func _ready() -> void: 39 | __EnsureSettingsFolder() 40 | __LoadSettings() 41 | __EnsureSaveFolder() 42 | 43 | func NewSlot(slotId : String = "", name : String = "", currentScene : String = "", metaData : Dictionary = {}) -> bool: 44 | var currentSlots = GetSlots() 45 | if currentSlots.any(func(slot:SaveFile): return slot.SlotId == slotId): 46 | return false 47 | CurrentLoadedSlot = SaveFile.new() 48 | if not slotId.is_empty(): 49 | CurrentLoadedSlot.SlotId = slotId 50 | else: 51 | CurrentLoadedSlot.SlotId = __GetDateTime() 52 | if not name.is_empty(): 53 | CurrentLoadedSlot.Name = name 54 | else: 55 | CurrentLoadedSlot.Name = "SaveSlot" 56 | 57 | if not currentScene.is_empty(): 58 | CurrentLoadedSlot.CurrentSceneId = currentScene 59 | else: 60 | CurrentLoadedSlot.CurrentSceneId = get_tree().current_scene.scene_file_path 61 | 62 | CurrentLoadedSlot.MetaData = metaData 63 | CurrentLoadedSlot.LastTime = Time.get_unix_time_from_system() 64 | CurrentLoadedSlot.GameTime = 0 65 | 66 | return SaveGame(false) 67 | 68 | func DeleteSlot(slotId : String) -> bool: 69 | var filePath = Settings.SaveFilesPrefix + Settings.SaveFolderPath + slotId + ".sav" 70 | var fileMetaPath = Settings.SaveFilesPrefix + Settings.SaveFolderPath + slotId + ".meta.sav" 71 | if FileAccess.file_exists(filePath) and FileAccess.file_exists(fileMetaPath): 72 | DirAccess.remove_absolute(filePath) 73 | DirAccess.remove_absolute(fileMetaPath) 74 | else: 75 | return false 76 | return true 77 | 78 | func SaveGame(autoSetCurrentScene = true) -> bool: 79 | if not CurrentLoadedSlot: 80 | return false 81 | emit_signal("saveStarted") 82 | CurrentLoadedSlot.GlobalData = __GetGlobalData() 83 | CurrentLoadedSlot.DateTime = Time.get_datetime_dict_from_system() 84 | CurrentLoadedSlot.GameTime = CurrentLoadedSlot.GameTime + (Time.get_unix_time_from_system() - CurrentLoadedSlot.LastTime) 85 | CurrentLoadedSlot.LastTime = Time.get_unix_time_from_system() 86 | if autoSetCurrentScene: 87 | CurrentLoadedSlot.CurrentSceneId = get_tree().current_scene.scene_file_path 88 | CurrentLoadedSlot.Hash = "" 89 | var serialized = __SerializeSaveFile(CurrentLoadedSlot) 90 | var filePath = Settings.SaveFilesPrefix + Settings.SaveFolderPath + CurrentLoadedSlot.SlotId + ".sav" 91 | var file = FileAccess.open(filePath, FileAccess.WRITE) 92 | file.store_string(serialized) 93 | file.close() 94 | __SaveMetaFile(CurrentLoadedSlot, filePath) 95 | emit_signal("saveCompleted") 96 | return true 97 | 98 | func LoadGame(slotId : String) -> bool: 99 | emit_signal("loadStarted") 100 | if not __ValidateMetaFile(slotId): 101 | emit_signal("loadCompleted") 102 | return false 103 | 104 | var filePath = Settings.SaveFilesPrefix + Settings.SaveFolderPath + slotId + ".sav" 105 | var file = FileAccess.open(filePath, FileAccess.READ) 106 | var content = file.get_as_text() 107 | file.close() 108 | CurrentLoadedSlot = __DeserializeSaveFile(content) 109 | CurrentLoadedSlot.LastTime = Time.get_unix_time_from_system() 110 | __ApplyGlobalData() 111 | emit_signal("loadCompleted") 112 | return true 113 | 114 | func GetSlots() -> Array[SaveFile]: 115 | var result : Array[SaveFile] = [] 116 | var files : Array[String] = [] 117 | files.append_array(DirAccess.get_files_at(Settings.SaveFolderPath)) 118 | files = files.filter(func(item : String) : return item.ends_with(".meta.sav")) 119 | for filePath in files: 120 | var file = FileAccess.open(Settings.SaveFolderPath + filePath, FileAccess.READ) 121 | var fileContent = file.get_as_text() 122 | file.close() 123 | result.append(__DeserializeSaveFile(fileContent)) 124 | return result 125 | 126 | func __ApplyGlobalData(): 127 | if not CurrentLoadedSlot: 128 | return 129 | for autoload in get_tree().root.get_children(): 130 | if CurrentLoadedSlot.GlobalData.any(func(item : SaveFileGlobal) : return item.GlobalClassName == autoload.name): 131 | var stored : SaveFileGlobal = CurrentLoadedSlot.GlobalData.filter(func(item : SaveFileGlobal) : return item.GlobalClassName == autoload.name)[0] 132 | for property in stored.GlobalData: 133 | autoload.set(property, SaveSerializationHelper.DeserializeVariable(stored.GlobalData[property])) 134 | 135 | 136 | func __GetGlobalData() -> Array[SaveFileGlobal]: 137 | var result : Array[SaveFileGlobal] = [] 138 | 139 | for autoload in get_tree().root.get_children(): 140 | if __ValidateAutoload(autoload): 141 | result.append(__SerializeGlobal(autoload)) 142 | 143 | return result 144 | 145 | func __ValidateAutoload(autoload) -> bool: 146 | var result = autoload != get_tree().get_current_scene() and autoload != self 147 | if len(Settings.StaticScriptsToSave) > 0: 148 | result = result and Settings.StaticScriptsToSave.any(func(item : String): return autoload.script.resource_path.ends_with(item)) 149 | 150 | return result 151 | 152 | func __SerializeGlobal(autoload : Node) -> SaveFileGlobal: 153 | var result = SaveFileGlobal.new() 154 | result.GlobalClassName = autoload.name 155 | result.GlobalId = autoload.get_script().resource_path 156 | result.GlobalData = {} 157 | for property in autoload.get_property_list(): 158 | if __ValidateGlobalProperty(property["name"]): 159 | result.GlobalData.get_or_add(property["name"], SaveSerializationHelper.SerializeVariable(autoload.get(property["name"]))) 160 | return result 161 | 162 | func __ValidateGlobalProperty(propertyName) -> bool: 163 | return not IGNORE_GLOBAL_PROPERTIES.any(func(item): return propertyName == item) 164 | 165 | func __SerializeSaveFile(saveFile : SaveFile) -> String: 166 | var result : Dictionary = { 167 | "Version" : saveFile.Version, 168 | "SlotId" : saveFile.SlotId, 169 | "Name" : saveFile.Name, 170 | "DateTime" : saveFile.DateTime, 171 | "LastTime" : saveFile.LastTime, 172 | "GameTime" : saveFile.GameTime, 173 | "Hash" : saveFile.Hash, 174 | "CurrentSceneId" : saveFile.CurrentSceneId, 175 | "MetaData" : saveFile.MetaData, 176 | "GlobalData" : saveFile.GlobalData.map(func(item: SaveFileGlobal): return {"GlobalId": item.GlobalId, "GlobalClassName": item.GlobalClassName, "GlobalData" : item.GlobalData }), 177 | "ScenesData" : saveFile.ScenesData.map(__GetSceneDictionary) 178 | } 179 | return JSON.stringify(result) 180 | 181 | func __DeserializeSaveFile(saveContent : String) -> SaveFile: 182 | var result = SaveFile.new() 183 | var json = JSON.parse_string(saveContent) 184 | result.Version = json["Version"] 185 | result.SlotId = json["SlotId"] 186 | result.Name = json["Name"] 187 | result.DateTime = json["DateTime"] 188 | result.LastTime = json["LastTime"] 189 | result.GameTime = json["GameTime"] 190 | result.Hash = json["Hash"] 191 | result.CurrentSceneId = json["CurrentSceneId"] 192 | result.MetaData = json["MetaData"] 193 | result.GlobalData.append_array(json["GlobalData"].map( 194 | func(item): 195 | var globalResult = SaveFileGlobal.new() 196 | globalResult.GlobalId = item["GlobalId"] 197 | globalResult.GlobalClassName = item["GlobalClassName"] 198 | globalResult.GlobalData = item["GlobalData"] 199 | return globalResult)) 200 | result.ScenesData.append_array(__GetScenesData(json["ScenesData"])) 201 | return result 202 | 203 | func __ValidateMetaFile(slotId : String) -> bool: 204 | var metaFilePath = Settings.SaveFilesPrefix + Settings.SaveFolderPath + slotId + ".meta.sav" 205 | var file = FileAccess.open(metaFilePath, FileAccess.READ) 206 | var content = file.get_as_text() 207 | file.close() 208 | var metaFile = __DeserializeSaveFile(content) 209 | var filePath = Settings.SaveFilesPrefix + Settings.SaveFolderPath + slotId + ".sav" 210 | var hash = __GetFileHash(filePath) 211 | return hash == metaFile.Hash 212 | 213 | func __SaveMetaFile(saveFile : SaveFile, filePath : String): 214 | var metaFile = SaveFile.new() 215 | metaFile.SlotId = saveFile.SlotId 216 | metaFile.Name = saveFile.Name 217 | metaFile.DateTime = Time.get_datetime_dict_from_system() 218 | metaFile.LastTime = saveFile.LastTime 219 | metaFile.GameTime = saveFile.GameTime 220 | metaFile.Version = saveFile.Version 221 | metaFile.MetaData = saveFile.MetaData 222 | metaFile.CurrentSceneId = saveFile.CurrentSceneId 223 | var hash = __GetFileHash(filePath) 224 | if hash.is_empty(): 225 | return "" 226 | metaFile.Hash = hash 227 | var metaFilePath = filePath.replace(".sav", ".meta.sav") 228 | var file = FileAccess.open(metaFilePath, FileAccess.WRITE) 229 | file.store_string(__SerializeSaveFile(metaFile)) 230 | file.close() 231 | 232 | func __GetFileHash(filePath : String) -> String: 233 | var file = FileAccess.open(filePath, FileAccess.READ) 234 | if file == null or not file.is_open(): 235 | return "" 236 | var content = file.get_as_text() 237 | file.close() 238 | content = content + Settings.HashSalt 239 | return content.md5_text() 240 | 241 | func __GetDateTime() -> String: 242 | var dateTime = Time.get_datetime_dict_from_system() 243 | return "%d%02d%02d_%02d%02d%02d" % [dateTime["year"], dateTime["month"], dateTime["day"], dateTime["hour"], dateTime["minute"], dateTime["second"]] 244 | 245 | func __EnsureSaveFolder(): 246 | if not DirAccess.dir_exists_absolute(Settings.SaveFolderPath): 247 | DirAccess.make_dir_recursive_absolute(Settings.SaveFolderPath) 248 | 249 | func __EnsureSettingsFolder(): 250 | if not DirAccess.dir_exists_absolute(SETTINGS_FOLDER_PATH): 251 | DirAccess.make_dir_recursive_absolute(SETTINGS_FOLDER_PATH) 252 | 253 | func __LoadSettings(): 254 | Settings = SaveSettigns.new() 255 | if FileAccess.file_exists(SETTINGS_FOLDER_PATH + SETTINGS_FILE_NAME): 256 | var file = FileAccess.open(SETTINGS_FOLDER_PATH + SETTINGS_FILE_NAME, FileAccess.READ) 257 | var content = file.get_as_text() 258 | file.close() 259 | var json = JSON.parse_string(content) 260 | Settings.SaveFilesPrefix = json["SaveFilesPrefix"] 261 | Settings.StaticScriptsToSave.append_array(json["StaticScriptsToSave"]) 262 | else: 263 | __CreateSettingsFile() 264 | 265 | func __CreateSettingsFile(): 266 | if not FileAccess.file_exists(SETTINGS_FOLDER_PATH + SETTINGS_FILE_NAME): 267 | var file = FileAccess.open(SETTINGS_FOLDER_PATH + SETTINGS_FILE_NAME, FileAccess.WRITE) 268 | var settings = SaveSettigns.new() 269 | var settingsDict = { 270 | "SaveFilesPrefix": settings.SaveFilesPrefix, 271 | "StaticScriptsToSave": settings.StaticScriptsToSave, 272 | "SaveFolderPath": settings.SaveFolderPath, 273 | "HashSalt": settings.HashSalt 274 | } 275 | file.store_string(JSON.stringify(settingsDict)) 276 | file.close() 277 | 278 | func __GetSceneDictionary(sceneData : SaveFileScene) -> Dictionary: 279 | return { 280 | "ScenePath": sceneData.ScenePath, 281 | "SceneNodes" : sceneData.SceneNodes.map(__GetNodeDictionary) 282 | } 283 | 284 | func __GetNodeDictionary(sceneNode : SaveFileNode) -> Dictionary: 285 | return { 286 | "NodeTreePath": sceneNode.NodeTreePath, 287 | "ParentTreePath": sceneNode.ParentTreePath, 288 | "NodeFilePath" : sceneNode.NodeFilePath, 289 | "NodeScriptPath" : sceneNode.NodeScriptPath, 290 | "NodeProperties" : sceneNode.NodeProperties 291 | } 292 | 293 | func __GetScenesData(data) -> Array[SaveFileScene]: 294 | var result : Array[SaveFileScene] 295 | result.append_array(data.map( 296 | func(item): 297 | var sceneResult = SaveFileScene.new() 298 | sceneResult.ScenePath = item["ScenePath"] 299 | sceneResult.SceneNodes.append_array(__GetSceneNodes(item["SceneNodes"])) 300 | return sceneResult 301 | )) 302 | return result 303 | 304 | func __GetSceneNodes(data) -> Array[SaveFileNode]: 305 | var result : Array[SaveFileNode] 306 | result.append_array(data.map( 307 | func(item): 308 | var nodeResult = SaveFileNode.new() 309 | nodeResult.NodeTreePath = item["NodeTreePath"] 310 | nodeResult.ParentTreePath = item["ParentTreePath"] 311 | nodeResult.NodeFilePath = item["NodeFilePath"] 312 | nodeResult.NodeScriptPath = item["NodeScriptPath"] 313 | nodeResult.NodeProperties = item["NodeProperties"] 314 | return nodeResult 315 | )) 316 | return result 317 | -------------------------------------------------------------------------------- /images/SaveEngine.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 38 | 40 | 45 | 53 | 56 | 61 | 63 | 68 | 73 | 78 | 83 | 88 | 93 | 98 | 99 | 102 | 106 | 111 | 118 | 125 | 133 | 139 | 145 | 151 | 157 | 175 | 193 | 201 | 207 | 223 | 239 | 244 | 245 | 250 | 251 | SAve Engine 262 | 267 | 268 | 269 | --------------------------------------------------------------------------------