From 61eaa41fd941aa72bd0ed137dccc0a5b2eac70e6 Mon Sep 17 00:00:00 2001 From: JeremyStarTM Date: Mon, 18 Mar 2024 03:23:27 +0100 Subject: [PATCH] Add storage module --- docs/docs/reference/core.md | 1 + docs/docs/reference/storage.md | 35 ++++++++ src/core.gd | 8 ++ src/storage.gd | 159 +++++++++++++++++++++++++++++++++ 4 files changed, 203 insertions(+) create mode 100644 docs/docs/reference/storage.md create mode 100644 src/storage.gd 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