diff --git a/docs/docs/reference/core.md b/docs/docs/reference/core.md
index 5c86dc8..f9804d1 100644
--- a/docs/docs/reference/core.md
+++ b/docs/docs/reference/core.md
@@ -21,6 +21,7 @@ Use these to access CORE's modules.
- `logui` (not important for developers, displays the log graphically)
- [`sms`](/reference/sms)
- [`edl`](/reference/edl)
+- [`storage`](/reference/storage)
## Variables
### *String* basepath
diff --git a/docs/docs/reference/storage.md b/docs/docs/reference/storage.md
new file mode 100644
index 0000000..1971235
--- /dev/null
+++ b/docs/docs/reference/storage.md
@@ -0,0 +1,35 @@
+---
+sidebar_position: 9
+---
+
+# `Storage`
+Allows you to write configuration files with ease, without any headaches.
+
+## Variables
+### *bool* is_open
+:::danger[Do not modify]
+NEVER change the value of this variable. You will break stuff with this!
+:::
+Indicates if a storage file is currently open.
+
+## Functions
+### *bool* open_storage(*String* location, *bool* create_new = *true*, *bool* sanity_check = *true*, *bool* fail_on_sanity_check = *false*)
+Loads a storage file from `location` and creates a new one first if missing (`create_new`). Also performs basic sanity checking (`sanity_check`) and may throw an error on failure (`fail_on_sanity_check`).
+### *bool* close_storage()
+Closes the active storage file.
+### *bool* save_storage()
+Saves the active storage file to disk.
+### *bool* nuke_storage(*bool* autosave = *true*)
+Removes all keys from the active storage file. The nuclear option basically.
+### *Variant* get_key(*String* key, *Variant* default = *null*)
+Returns a storage key. Can also return a default value if unset.
+### *bool* set_key(*String* key, *Variant* value, *bool* overwrite = *true*, *bool* autosave = *true*)
+Updates a storage key with the specified value.
+### *bool* del_key(*String* key, *bool* autosave = *true*)
+Deletes a storage key.
+### *Dictionary* get_dict()
+Returns the `storage` dictionary, useful for more advanced operations. Changes are not reflected onto the `storage` dictionary though. To save changes through this module, pass your modified dictionary to [`save_dict`](#bool-save_dictdictionary-dict-bool-sanity_check--true-bool-fail_on_sanity_check--false-bool-autosave--true).
+### *bool* save_dict(*Dictionary* dict, *bool* sanity_check = *true*, *bool* fail_on_sanity_check = *false*, *bool* autosave = *true*)
+Saves a arbitrary dictionary as a `storage` dictionary with sanity checking (`sanity_check` and `fail_on_sanity_check`).
+### *Array[String]* perform_sanity_check(*Dictionary* storage_check)
+Performs sanity checks on a dictionary aka. checks the type of all keys and their values.
diff --git a/src/core.gd b/src/core.gd
index ddfd76a..4b46ee2 100644
--- a/src/core.gd
+++ b/src/core.gd
@@ -41,6 +41,8 @@ var sms: CoreBaseModule
var logui: CoreBaseModule
## Use this to access CORE's builtin HTTP request maker.
var edl: CoreBaseModule
+## Use this to access configuration and settings files easily.
+var storage: CoreBaseModule
# Variables
## Contains CORE's load path
@@ -82,30 +84,35 @@ func initialize_modules() -> void:
sms = CoreBaseModule.new()
logui = CoreBaseModule.new()
edl = CoreBaseModule.new()
+ storage = CoreBaseModule.new()
# Set names
logger.name = "Logger"
misc.name = "Misc"
sms.name = "SceneManagementSystem"
logui.name = "LogUI"
edl.name = "EasyDownLoader"
+ storage.name = "Storage"
# Set scripts
logger.set_script(ResourceLoader.load(basepath + "src/logger.gd"))
misc.set_script(ResourceLoader.load(basepath + "src/misc.gd"))
sms.set_script(ResourceLoader.load(basepath + "src/sms.gd"))
logui.set_script(ResourceLoader.load(basepath + "src/logui.gd"))
edl.set_script(ResourceLoader.load(basepath + "src/edl.gd"))
+ storage.set_script(ResourceLoader.load(basepath + "src/storage.gd"))
# Set reference to self
logger.core = self
misc.core = self
sms.core = self
logui.core = self
edl.core = self
+ storage.core = self
# Call _initialize() (workaround as modules cannot access "core" during _init())
logger._initialize()
misc._initialize()
sms._initialize()
logui._initialize()
edl._initialize()
+ storage._initialize()
# Inject modules into the SceneTree
## Injects CORE's builtin modules into the SceneTree.[br]
@@ -117,6 +124,7 @@ func inject_modules() -> void:
add_child(sms)
add_child(logui)
add_child(edl)
+ add_child(storage)
# Registers a custom module
## Registers a new custom module.
diff --git a/src/storage.gd b/src/storage.gd
new file mode 100644
index 0000000..48c42cd
--- /dev/null
+++ b/src/storage.gd
@@ -0,0 +1,159 @@
+# CORE FRAMEWORK SOURCE FILE
+# Copyright (c) 2024 The StarOpenSource Project & Contributors
+# Licensed under the GNU Affero General Public License v3
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+extends CoreBaseModule
+
+var is_open: bool = false
+var storage: Dictionary = {}
+var storage_location: String = ""
+
+func open_storage(location: String, create_new: bool = true, sanity_check: bool = true, fail_on_sanity_check: bool = false) -> bool:
+ if is_open:
+ logger.errorf("storage.gd", "Failed to open storage: A storage file is already open")
+ return false
+ logger.verbf("storage.gd", "Opening storage file at \"" + location + "\"")
+ var file: FileAccess
+ if !FileAccess.file_exists(location):
+ if create_new:
+ file = FileAccess.open(location, FileAccess.WRITE)
+ if file == null:
+ await logger.crashf("storage.gd", "Could not open storage file at \"" + location + "\": Failed with code " + str(FileAccess.get_open_error()))
+ return false
+ file.store_string("{}")
+ file.close()
+ else:
+ logger.errorf("storage.gd", "Failed to open storage: create_new is set to false")
+ return false
+ file = FileAccess.open(location, FileAccess.READ)
+ var storage_temp: Variant = file.get_as_text()
+ file.close()
+ storage_temp = JSON.parse_string(storage_temp)
+ if typeof(storage_temp) != TYPE_DICTIONARY:
+ logger.errorf("storage.gd", "Failed to open storage: Parsed storage file is of type " + str(typeof(storage_temp)))
+ return false
+ if sanity_check:
+ var check_result: Array[String] = perform_sanity_check(storage_temp)
+ if check_result.size() != 0:
+ if fail_on_sanity_check:
+ logger.errorf("storage.gd", "Sanity check failed (stopping):")
+ for error in check_result:
+ logger.errorf("storage.gd", "-> " + error)
+ return false
+ else:
+ logger.warnf("storage.gd", "Sanity check failed (continuing anyway):")
+ for error in check_result:
+ logger.warnf("storage.gd", "-> " + error)
+ storage = storage_temp
+ storage_location = location
+ is_open = true
+ return true
+
+func close_storage() -> bool:
+ if !is_open:
+ logger.errorf("storage.gd", "Failed to close storage: No storage file is open")
+ return false
+ logger.verbf("storage.gd", "Closing storage file")
+ storage = {}
+ is_open = false
+ return true
+
+func save_storage() -> bool:
+ if !is_open:
+ logger.errorf("storage.gd", "Failed to save storage: No storage file is open")
+ return false
+ var file: FileAccess = FileAccess.open(storage_location, FileAccess.WRITE)
+ if file == null:
+ await logger.crashf("storage.gd", "Could not open storage file at \"" + storage_location + "\": Failed with code " + str(FileAccess.get_open_error()))
+ return false
+ logger.diagf("storage.gd", "Writing storage file to disk")
+ file.store_string(JSON.stringify(storage))
+ file.close()
+ return true
+
+func nuke_storage(autosave: bool = true) -> bool:
+ if !is_open:
+ logger.errorf("storage.gd", "Failed to nuke storage: No storage file is open")
+ return false
+ logger.warnf("storage.gd", "Nuking storage")
+ storage = {}
+ if autosave: save_storage()
+ return true
+
+func get_key(key: String, default: Variant) -> Variant:
+ if !is_open:
+ logger.errorf("storage.gd", "Failed to get key: No storage file is open")
+ return NAN
+ logger.diagf("storage.gd", "Returning storage key \"" + key + "\" (default='" + default + "')")
+ return storage.get(key, default)
+
+func set_key(key: String, value: Variant, overwrite: bool = true, autosave: bool = true) -> bool:
+ if !is_open:
+ logger.errorf("storage.gd", "Failed to set key: No storage file is open")
+ return false
+ logger.diagf("storage.gd", "Updating storage key \"" + key + "\" with value '" + str(value) + "' (overwrite='" + str(overwrite) + "' autosave='" + str(autosave) + "'")
+ storage.merge({key: value}, overwrite)
+ if autosave: save_storage()
+ return true
+
+func del_key(key: String, autosave: bool = true) -> bool:
+ if !is_open:
+ logger.errof("storage.gd", "Failed to delete key: No storage file is open")
+ return false
+ logger.diagf("storage.gd", "Deleting storage key \"" + key + "\" (autosave='" + str(autosave) + "')")
+ storage.erase(key)
+ if autosave: save_storage()
+ return true
+
+func get_dict() -> Dictionary:
+ if !is_open:
+ logger.errorf("storage.gd", "Failed to get dictionary: No storage file is open")
+ return {}
+ logger.verbf("storage.gd", "Returning storage dictionary")
+ return storage
+
+func save_dict(dict: Dictionary, sanity_check: bool = true, fail_on_sanity_check: bool = false, autosave: bool = true) -> bool:
+ if !is_open:
+ logger.errorf("storage.gd", "Failed to save dictionary: No storage file is open")
+ return false
+ logger.verbf("storage.gd", "Saving custom dictionary as storage")
+ if sanity_check:
+ var check_result: Array[String] = perform_sanity_check(dict)
+ if check_result.size() != 0:
+ if fail_on_sanity_check:
+ logger.errorf("storage.gd", "Sanity check failed (stopping):")
+ for error in check_result:
+ logger.errorf("storage.gd", "-> " + error)
+ return false
+ else:
+ logger.warnf("storage.gd", "Sanity check failed (continuing anyway):")
+ for error in check_result:
+ logger.warnf("storage.gd", "-> " + error)
+ storage = dict
+ if autosave: save_storage()
+ return true
+
+func perform_sanity_check(storage_check: Dictionary) -> Array[String]:
+ logger.verbf("storage.gd", "Performing a sanity check on some storage dictionary")
+ var errors: Array[String] = []
+ for key in storage_check:
+ if typeof(key) != TYPE_STRING:
+ errors.append("Key \"" + str(key) + "\" is not of type String (type '" + type_string(typeof(key)) + "')")
+ continue
+ if typeof(storage_check[key]) != TYPE_NIL and typeof(storage_check[key]) != TYPE_STRING and typeof(storage_check[key]) != TYPE_INT and typeof(storage_check[key]) != TYPE_FLOAT and typeof(storage_check[key]) != TYPE_BOOL and typeof(storage_check[key]) != TYPE_ARRAY and typeof(storage_check[key]) != TYPE_DICTIONARY:
+ errors.append("The value of \"" + key + "\" is not null, a string, an integer, a float, boolean, array or dictionary (type '" + type_string(typeof(key)) + "')")
+
+ logger.verbf("storage.gd", "Completed sanity check with " + str(errors.size()) + " errors")
+ return errors