This commit is contained in:
JeremyStar™ 2024-04-08 22:47:51 +02:00
parent f5ab359c28
commit ed0670eac9
Signed by: JeremyStarTM
GPG key ID: E366BAEF67E4704D
11 changed files with 491 additions and 290 deletions

View file

@ -15,32 +15,29 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
## Template for CORE modules.[br] ## Template for CORE modules.
## [br] ##
## Provides a basic template and a common foundation for building CORE modules.[br] ## Provides a basic template and a common foundation for building CORE modules.[br]
## It provides common functions and variables used in all CORE modules. ## It provides common functions and variables used in all CORE modules.
extends Node extends Node
class_name CoreBaseModule class_name CoreBaseModule
## Contains a reference to the CORE Object[br] ## Contains a reference to the CORE Object.
## [br]
## Set before loading the module into the SceneTree.
var core: Core var core: Core
## Reference to CORE's logger implementation.[br] ## Set to CORE's logger implementation.
## [br]
## Will be set before [method Node._ready]
@onready var logger: CoreBaseModule = core.logger @onready var logger: CoreBaseModule = core.logger
## Reference to a matching [class CoreLoggerInstance]. ## Set to a [class CoreLoggerInstance] with the path you supplied to [method Core.register_custom_module]. You should use this over [code]logger[/code].
var loggeri: CoreLoggerInstance var loggeri: CoreLoggerInstance
## Marks a module as fully initialized and ready. ## Marks a module as fully initialized and ready. **Don't forget to set this!**
var initialized: bool = false var initialized: bool = false
## CORE's replacement for [method Object._init] and [method Node._ready] ## CORE's replacement for [method Object._init] and [method Node._ready].[br]
## It's [b]strongly[/b] recommended to initialize your module here. ## It's [b]strongly[/b] recommended to initialize your module here or stuff might break.
func _initialize() -> void: initialized = true func _initialize() -> void: initialized = true
## Called when CORE is about to cleanup ## Called when CORE is about to cleanup.[br]
## Use this function to free any children ## Use this function to remove any children from the SceneTree and free any nodes.[br]
## If not done you might cause a memory leak.
func _cleanup() -> void: pass func _cleanup() -> void: pass
## Called by [method Core.apply_configuration]. ## Called after CORE's configuration got updated.[br]
## This should be used to update your module configuration. ## Probably useless to your module, unless you want to modify another module's settings.
func _pull_config() -> void: pass func _pull_config() -> void: pass

View file

@ -2,47 +2,44 @@
# Copyright (c) 2024 The StarOpenSource Project & Contributors # Copyright (c) 2024 The StarOpenSource Project & Contributors
# Licensed in the Public Domain # Licensed in the Public Domain
## The [code]CoreConfiguration[/code] class holds the Framework's settings.[br] ## The framework configuration
## The default configuration is designed to be usable without any modification. ##
## Provides the default configuration for the CORE Framework.
extends Node extends Node
class_name CoreConfiguration class_name CoreConfiguration
@export_category("Global") @export_category("Global")
## Controls CORE's functionality. ## Controls CORE's functionality.[br]
## Renders GUI-related modules useless when set to [code]false[/code], which is the recommended behaviour on servers. For CORE's full functionality, set this to [code]true[/code]. ## Renders GUI-related modules useless when set to [code]false[/code], which is the recommended behaviour on servers. For CORE's full functionality, set this to [code]true[/code].
@export var headless: bool @export var headless: bool
## Allows debugging functionality if set to [code]true[/code], or not if set to [code]false[/code].[br] ## Allows debugging functionality if set to [code]true[/code], or not if set to [code]false[/code].[br]
## [br] ## [br]
## Note: This will not enable the development mode automatically, only if you're developing on CORE itself. ## [b]Note: [i]This will not enable the development mode automatically, only if you're developing on CORE itself.[/i][/b]
@export var debugging: bool @export var debugging: bool
## Allows or disallows custom modules. ## Allows or disallows custom modules.
@export var custom_modules: bool @export var custom_modules: bool
@export_category("Logger") @export_category("Logger")
## I don't have to explain this, do I? ## The minimum log level you want to be displayed.
@export var logger_level: CoreTypes.LoggerLevel @export var logger_level: CoreTypes.LoggerLevel
## Toggles colored output. Set to [code]false[/code] if you don't want that. ## Determines if the logger's output should be colored.
@export var logger_colored: bool @export var logger_colored: bool
## The format string the logger will operate on. Available placeholders are: [code]%time%[/code], [code]%time_ms%[/code], [code]%level%[/code], [code]%color%[/code], [code]%message%[/code], [code]%source%[/code], [code]%source_raw%[/code], [code]%function%[/code] and [code]%line%[/code] ## The template for all log messages.[br]
## Available placeholders are: [code]%time%[/code], [code]%time_ms%[/code], [code]%level%[/code], [code]%color%[/code], [code]%message%[/code], [code]%source%[/code], [code]%source_raw%[/code], [code]%function%[/code] and [code]%line%[/code]
@export var logger_format: String @export var logger_format: String
## This example should make it clear, what this does: ## Determines if identation should be provided if the logger encounters a newline.
## [codeblock]
## logger_newlines_override = true:
## [09:47:00] [INFO Test.gd:69] This is a test message...
## with a newline!
## logger_newlines_override = false:
## [09:47:00] [INFO Test.gd:69] This is a test message...
## with a newline!
## [/codeblock]
@export var logger_newlines_override: bool @export var logger_newlines_override: bool
## The maximum amount of characters than can appear before [code]%message%[/code] before newlines won't be overriden. Setting this variable to [code]-1[/code] disables this behaviour. ## The maximum amount of characters excluding the message before newlines are no longer idented. Set to [code]-1[/code] to disable this behaviour.
@export var logger_newlines_sizelimit: int @export var logger_newlines_sizelimit: int
@export_category("LogUI") @export_category("Log UI")
## Determines if [code]LogUI[/code] should be visible or not. ## Enables or disables Log UI.
@export var logui_enabled: bool @export var logui_enabled: bool
## The color the [code]LogUI[/code] background will have. Set to [code]Color.TRANSPARENT[/code] for a transparent background. ## The Log UI background color. Set to [code]Color.TRANSPARENT[/code] for a transparent background.
@export var logui_background_color: Color @export var logui_background_color: Color
## What size the graphical log should have. ## What font size the graphical log should have.
@export var logui_font_size: int @export var logui_font_size: int
@export_category("Easy Request Maker")
## Determines how unsecure requests should be handled.
@export var erm_unsecure_requests: CoreTypes.BlockadeLevel
# Populates configuration with default values # Populates configuration with default values
func _init() -> void: func _init() -> void:
@ -57,7 +54,9 @@ func _init() -> void:
logger_format = "%color%[%time%] [%level% %origin%] %message%" logger_format = "%color%[%time%] [%level% %origin%] %message%"
logger_newlines_override = true logger_newlines_override = true
logger_newlines_sizelimit = 40 logger_newlines_sizelimit = 40
# LogUI # Log UI
logui_enabled = true logui_enabled = true
logui_background_color = Color.BLACK # To disable the background, use Color.TRANSPARENT logui_background_color = Color.BLACK # To disable the background, use Color.TRANSPARENT
logui_font_size = 14 logui_font_size = 14
# Easy Request Maker
erm_unsecure_requests = CoreTypes.BlockadeLevel.BLOCK

View file

@ -15,32 +15,38 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
## Passes [code]origin[/code] for you.[br] ## Passes [code]origin[/code] for you.
## [br] ##
## Pretty much a wrapper around CORE's logging implementation.[br] ## Pretty much a wrapper around CORE's logging implementation.
## CoreLoggerInstance's only job is to save you some effort aka.[br] ## CoreLoggerInstance's only job is to save you some effort aka.
## you not needing to pass the [code]origin[/code] argument to each[br] ## you not needing to pass the [code]origin[/code] argument to each
## and every log call, which is extremely annoying. Thank us later ;) ## and every log call, which is extremely annoying. Thank us later ;)
extends Node extends Node
class_name CoreLoggerInstance class_name CoreLoggerInstance
## Class name ## Reference to CORE's logger module.
var logger: CoreBaseModule var logger: CoreBaseModule
## The origin argument.
var origin: String var origin: String
## For internal purposes only.
## [b]Note: [i]Don't modify.[/i][/b]
var framework: bool = false
## The instance constructor.
func _init(logger_new: CoreBaseModule, origin_new: String) -> void: func _init(logger_new: CoreBaseModule, origin_new: String) -> void:
logger = logger_new logger = logger_new
origin = origin_new origin = origin_new
## Prints a diagnostic message ## Prints a diagnostic message.
func diag(message: String) -> void: logger.diag(origin, message) func diag(message: String) -> void: logger.diag(origin, message)
## Prints a verbose message ## Prints a verbose message.
func verb(message: String) -> void: logger.verb(origin, message) func verb(message: String) -> void: logger.verb(origin, message)
## Prints a informational message ## Prints an informational message.
func info(message: String) -> void: logger.info(origin, message) func info(message: String) -> void: logger.info(origin, message)
## Prints a warning message ## Prints a warning message.
func warn(message: String) -> void: logger.warn(origin, message) func warn(message: String) -> void: logger.warn(origin, message)
## Prints a error message ## Prints an error message.
func error(message: String) -> void: logger.error(origin, message) func error(message: String) -> void: logger.error(origin, message)
## Handles crashes. Will terminate your game/application immediately. ## Handles crashes. Will terminate your game/application immediately.[br]
func crash(message: String) -> void: await logger.crash(origin, message) ## [b]Note: [i]Using the [code]await[/code] keyword is required for this function.[/i][/b]
func crash(message: String) -> void: await logger.crash(origin, message, framework)

View file

@ -15,13 +15,17 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
## Contains enums and types shared across the CORE Framework. ## Contains various enums.
##
## Contains globaly accessible custom enums and types used throughout the CORE Framework's source code.
extends Node extends Node
class_name CoreTypes class_name CoreTypes
## Available version types, following the StarOpenSource Versioning Specification (SOSVS) version 1 ## Available version types, following the StarOpenSource Versioning Specification (SOSVS) version 1.
enum VersionType { RELEASE, RELEASECANDIDATE, BETA, ALPHA } enum VersionType { RELEASE, RELEASECANDIDATE, BETA, ALPHA }
## Available log levels, followingthe StarOpenSource Logging Specification (SOSLS) version 1 ## Available log levels, followingthe StarOpenSource Logging Specification (SOSLS) version 1.
enum LoggerLevel { NONE, ERROR, WARN, INFO, VERB, DIAG } enum LoggerLevel { NONE, ERROR, WARN, INFO, VERB, DIAG }
## Available scene types ## Available scene types.
enum SceneType { NONE, DEBUG, CUTSCENE, MENU, MAIN, BACKGROUND } enum SceneType { NONE, DEBUG, CUTSCENE, MENU, MAIN, BACKGROUND }
## To what degree [i]something[/i] should be blocked.
enum BlockadeLevel { IGNORE, WARN, BLOCK }

View file

@ -18,46 +18,53 @@
## Initializes and manages the framework. ## Initializes and manages the framework.
## ##
## The [b]CORE Object[/b] is responsible for initializing, managing and ## The [b]CORE Object[/b] is responsible for initializing, managing and
## serving the CORE Framework to the developer. ## serving the CORE Framework.
extends Node extends Node
class_name Core class_name Core
# Constants # Versioning
## The version number ## The version number
const version_version: int = 1 const version_version: int = 1
## The version type ## The version type. See [enum CoreTypes.VersionType] for more information.
const version_type: CoreTypes.VersionType = CoreTypes.VersionType.BETA const version_type: CoreTypes.VersionType = CoreTypes.VersionType.BETA
## The version type number. Resets on every new version and version type. ## The version type number. Resets on every new version and version type.
const version_typerelease: int = 4 const version_typerelease: int = 6
# Modules # Modules
## Used internally for loading, managing and unloading modules.
const modules: Array[String] = [ "logger", "misc", "sms", "logui", "erm", "storage" ] const modules: Array[String] = [ "logger", "misc", "sms", "logui", "erm", "storage" ]
## Use this to access CORE's logging implementation. ## CORE's configuration object.[br]
## [b]NEVER access this yourself! To change the configuration use [method reload_configuration] instead.[/b]
var config: CoreConfiguration
## The 'Logger' module
var logger: CoreBaseModule var logger: CoreBaseModule
## Use this to access various useful functions. ## The 'Miscellaneous' module
var misc: CoreBaseModule var misc: CoreBaseModule
## Use this to access the scene management system. ## The 'Scene Management System' module
var sms: CoreBaseModule var sms: CoreBaseModule
## Use this to access the graphical log. Serves no importance to you (probably). ## The 'Log UI' module. Not important for you, it just displays the log graphically :3
var logui: CoreBaseModule var logui: CoreBaseModule
## Use this to access CORE's builtin HTTP request maker. ## The 'Easy Request Maker' module (formerly 'Easy DownLoader')
var erm: CoreBaseModule var erm: CoreBaseModule
## Use this to access configuration and settings files easily. ## The 'Storage' module
var storage: CoreBaseModule var storage: CoreBaseModule
# Variables # /etc/
## Contains CORE's load path ## Stores the path to CORE's installation directory.[br]
## [b]Danger: [i]Don't modify this.[/i][/b]
var basepath: String var basepath: String
## Holds the configuration[br] ## Contains a list of all loaded custom modules.[br]
## [br] ## [b]Danger: [i]Don't modify this.[/i][/b]
## [b]NEVER access this yourself. To change the configuration file, use [method Core.reload_configuration] instead.[/b]
var config: CoreConfiguration
## Contains all loaded custom modules.
var custom_modules: Dictionary = {} var custom_modules: Dictionary = {}
## Contains the custom modules node. ## Contains the node holding all custom modules as children.[br]
## [b]Danger: [i]Don't modify this.[/i][/b]
var custom_modules_node: Node var custom_modules_node: Node
## The CORE Object's logger instance.
## [b]Danger: [i]Don't modify this.[/i][/b]
var loggeri: CoreLoggerInstance
# Preinitialization # +++ initialization +++
## Handles the preinitialization part. Does stuff like checking the engine version, loading the config and loading all modules into memory.
func _init(new_config: CoreConfiguration = CoreConfiguration.new()) -> void: func _init(new_config: CoreConfiguration = CoreConfiguration.new()) -> void:
name = "CORE" name = "CORE"
if !check_godot_version(): return if !check_godot_version(): return
@ -67,32 +74,16 @@ func _init(new_config: CoreConfiguration = CoreConfiguration.new()) -> void:
initialize_modules() initialize_modules()
apply_configuration() apply_configuration()
# Initialization ## Handles the initialization part. Injects the builtin modules into the SceneTree and makes sure custom modules can be loaded properly.[br]
## [b]Danger: [i]Don't call this.[/i][/b]
func _ready() -> void: func _ready() -> void:
inject_modules() inject_modules()
custom_modules_node.name = "Custom Modules" custom_modules_node.name = "Custom Modules"
add_child(custom_modules_node) add_child(custom_modules_node)
loggeri = logger.get_instance(basepath.replace("res://", "") + "src/core.gd")
# Cleanup ## Initializes all built-in modules during the preinitialization phase.[br]
# Particularily useful during testing to cleanup stuff, or if you want to do some stupid stuff with CORE during runtime ## [b]Danger: [i]Don't call this.[/i][/b]
func cleanup() -> void:
logger.infof("core", "Cleaning up")
config.queue_free()
var modules_reverse: Array[String] = modules.duplicate()
modules_reverse.reverse()
for module in modules_reverse:
await get(module)._cleanup()
get(module).loggeri.queue_free()
get(module).queue_free()
for module in custom_modules_node.get_children(): unregister_custom_module(module.name)
remove_child(custom_modules_node)
custom_modules_node.queue_free()
queue_free()
# Initialize modules
## Initializes all modules during the first initialization phase.[br]
## [br]
## [b]NEVER call this yourself! You will break everything and risk a crash![/b]
func initialize_modules() -> void: func initialize_modules() -> void:
for module in modules: for module in modules:
set(module, CoreBaseModule.new()) set(module, CoreBaseModule.new())
@ -102,18 +93,15 @@ func initialize_modules() -> void:
get(module).loggeri = logger.get_instance(basepath.replace("res://", "") + "src/" + module + ".gd") get(module).loggeri = logger.get_instance(basepath.replace("res://", "") + "src/" + module + ".gd")
get(module)._initialize() get(module)._initialize()
# Inject modules into the SceneTree
## Injects CORE's builtin modules into the SceneTree.[br] ## Injects CORE's builtin modules into the SceneTree.[br]
## [br] ## [b]Danger: [i]Don't call this.[/i][/b]
## [b]NEVER call this yourself! You will break everything and risk a crash![/b]
func inject_modules() -> void: for module in modules: add_child(get(module)) func inject_modules() -> void: for module in modules: add_child(get(module))
# Wait for all modules to be fully initialized ## Waits for all modules to fully initialize.[br]
## Wait for all builtin modules to be fully initialized.[br]
## [br] ## [br]
## This ensures that all of CORE's builtin modules are fully initialized and ready. ## This ensures that all modules are fully initialized and ready for usage.[br]
## [b]Not calling this function during startup may lead to runtime issues.[/b] ## [i][b]Not calling this function during startup may lead to runtime issues.[/b][/i]
func complete_init(no_success: bool = false) -> void: func complete_init(no_success_message: bool = false) -> void:
var modsinit_builtin: Array[String] = ["workaround"] var modsinit_builtin: Array[String] = ["workaround"]
var modsinit_custom: Array[String] = ["workaround"] var modsinit_custom: Array[String] = ["workaround"]
@ -138,38 +126,38 @@ func complete_init(no_success: bool = false) -> void:
# Initialization complete # Initialization complete
await get_tree().process_frame await get_tree().process_frame
if !no_success: logger.infof("core", "Initialized CORE successfully") if !no_success_message: loggeri.info("Initialized CORE successfully")
# Registers a custom module # +++ custom module support +++
## Registers a new custom module. ## Registers a new custom module.
func register_custom_module(module_name: String, module_origin: String, module_class: CoreBaseModule) -> bool: func register_custom_module(module_name: String, module_origin: String, module_class: CoreBaseModule) -> bool:
logger.verbf("core", "Registering new custom module \"" + module_name + "\"") loggeri.verb("Registering new custom module \"" + module_name + "\"")
if !config.custom_modules: if !config.custom_modules:
logger.errorf("core", "Registering module failed: Custom module support is disabled.") loggeri.error("Registering module failed: Custom module support is disabled.")
return false return false
if custom_modules.has(module_name): if custom_modules.has(module_name):
logger.errorf("core", "Registering module failed: A custom module with the name \"" + module_name + "\" already exists.") loggeri.error("Registering module failed: A custom module with the name \"" + module_name + "\" already exists.")
return false return false
logger.diagf("core", "Updating variables") loggeri.diag("Updating variables")
module_class.name = module_name module_class.name = module_name
module_class.core = self module_class.core = self
module_class.loggeri = logger.get_instance(module_origin) module_class.loggeri = logger.get_instance(module_origin)
logger.diagf("core", "Adding module to SceneTree") module_class.loggeri.framework = true
loggeri.diag("Adding module to SceneTree")
custom_modules_node.add_child(module_class) custom_modules_node.add_child(module_class)
logger.diagf("core", "Merging module with custom_modules") loggeri.diag("Merging module with custom_modules")
custom_modules.merge({ module_name: module_class }) custom_modules.merge({ module_name: module_class })
logger.diagf("core", "Initializing custom module") loggeri.diag("Initializing custom module")
module_class._initialize() module_class._initialize()
logger.diagf("core", "Updating custom module configuration") loggeri.diag("Updating custom module configuration")
module_class._pull_config() module_class._pull_config()
return true return true
# Unregisters a custom module ## Unregisters a custom module, making it no longer available.
## Unregisters a custom module, making it no longer function.
func unregister_custom_module(module_name: String) -> void: func unregister_custom_module(module_name: String) -> void:
logger.verbf("core", "Unregistering custom module \"" + module_name + "\"") loggeri.verb("Unregistering custom module \"" + module_name + "\"")
if !custom_modules.has(module_name): if !custom_modules.has(module_name):
logger.errorf("core", "Unregistering module failed: A custom module with the name \"" + module_name + "\" does not exist.") loggeri.error("Unregistering module failed: A custom module with the name \"" + module_name + "\" does not exist.")
return return
var module: Node = get_custom_module(module_name) var module: Node = get_custom_module(module_name)
await module._cleanup() await module._cleanup()
@ -178,78 +166,93 @@ func unregister_custom_module(module_name: String) -> void:
custom_modules.erase(module_name) custom_modules.erase(module_name)
module.queue_free() module.queue_free()
# Returns a custom module ## Returns a registered custom module.[br]
## Returns a loaded custom module for access. ## Please note that you can't get CORE's built-in modules with this function.
func get_custom_module(module_name: String) -> CoreBaseModule: func get_custom_module(module_name: String) -> CoreBaseModule:
logger.diagf("core", "Getting custom module \"" + module_name + "\"") loggeri.diag("Getting custom module \"" + module_name + "\"")
if !custom_modules.has(module_name): if !custom_modules.has(module_name):
logger.errorf("core", "Getting module failed: A custom module with the name \"" + module_name + "\" does not exist.") loggeri.error("Getting module failed: A custom module with the name \"" + module_name + "\" does not exist.")
return null return null
return custom_modules[module_name] return custom_modules[module_name]
# (Re-)Load configuration # +++ configuration +++
## Loads a (new) configuration file and applies it to all modules. ## Loads a (new) configuration object and applies it to all modules.
func reload_configuration(new_config: CoreConfiguration = CoreConfiguration.new()) -> void: func reload_configuration(new_config: CoreConfiguration = CoreConfiguration.new()) -> void:
var initialized = config != null var initialized = config != null
if initialized: logger.verbf("core", "Reloading CORE's configuration") if initialized: loggeri.verb("Reloading CORE's configuration")
if config != null: config.queue_free() if config != null: config.queue_free()
config = new_config config = new_config
if is_devmode(): # Override configuration in development mode if is_devmode(): # Override configuration in development mode
config.logger_level = CoreTypes.LoggerLevel.VERB config.logger_level = CoreTypes.LoggerLevel.VERB
if initialized: logger.verbf("core", "Overrode configuration (development mode)") if initialized: loggeri.verb("Overrode configuration (development mode)")
if initialized: apply_configuration() if initialized: apply_configuration()
# Call _pull_config() functions ## Applies the a configuration.[br]
## Applies the newly applied configuration.[br] ## [b]Danger: [i]Don't call this.[/i][/b]
## [br]
## [b]NEVER call this yourself unless you know what you are doing![/b]
func apply_configuration() -> void: func apply_configuration() -> void:
logger.verbf("core", "Applying configuration") if loggeri != null: loggeri.verb("Applying configuration")
if is_devmode(): logger.warnf("core", "The CORE Framework is in development mode. Here be dragons!") if is_devmode(): if loggeri != null: loggeri.warn("The CORE Framework is in development mode. Here be dragons!")
if config.headless: logger.warnf("core", "CORE is in headless mode. Certain modules will not work as expected.") if config.headless: loggeri.warn("CORE is in headless mode. Certain modules will not work as expected.")
if !config.custom_modules: if !config.custom_modules:
logger.verbf("core", "Removing all custom modules (custom modules support is disabled)") if loggeri != null: loggeri.verb("Removing all custom modules (custom modules support is disabled)")
for module in custom_modules: unregister_custom_module(module) for module in custom_modules: unregister_custom_module(module)
for module in modules: get(module)._pull_config() for module in modules: get(module)._pull_config()
if config.custom_modules: if config.custom_modules:
for module in custom_modules: for module in custom_modules:
logger.diagf("core", "Updating configuration for custom module \"" + module.name + "\"") if loggeri != null: loggeri.diag("Updating configuration for custom module \"" + module.name + "\"")
module._pull_config() module._pull_config()
# Return development mode status # +++ etc ++
## Makes sure that CORE does not leak memory on shutdown/unload.[br]
## Unloads all custom modules, built-in modules, frees any of CORE's classes and lastly itself.
func cleanup() -> void:
loggeri.info("Cleaning up")
config.queue_free()
var modules_reverse: Array[String] = modules.duplicate()
modules_reverse.reverse()
for module in modules_reverse:
await get(module)._cleanup()
get(module).loggeri.queue_free()
get(module).queue_free()
for module in custom_modules_node.get_children(): unregister_custom_module(module.name)
remove_child(custom_modules_node)
custom_modules_node.queue_free()
queue_free()
## Returns if the CORE Framework is in development mode. ## Returns if the CORE Framework is in development mode.
func is_devmode() -> bool: func is_devmode() -> bool:
return config.debugging and basepath == "res://" and OS.is_debug_build() return config.debugging and basepath == "res://" and OS.is_debug_build()
# Replaces variables with human-friendly strings ## Replaces placeholders with human-friendly strings.[br]
## Replaces placeholders with human-friendly strings You can use the following placeholders:[br] ## You can use the following placeholders:[br]
## - [code]%release%[/code]: Returns the release number.[br] ## - [code]%version%[/code]: Returns the version number.[br]
## - [code]%release_type%[/code]: Returns the typerelease number[br] ## - [code]%version_type%[/code]: Returns the version type number[br]
## - [code]%release_semantic%[/code]: Returns the result of [method Core.get_version_semantic], example [i]5.2.3[/i][br] ## - [code]%version_semantic%[/code]: Returns the result of [method Core.get_version_semantic], example [i]5.2.3[/i][br]
## - [code]%type%[/code]: Returns the release type as a word, for example [i]Release Candidate[/i][br] ## - [code]%version_type%[/code]: Returns the version type as a word, for example [i]Release Candidate[/i][br]
## - [code]%type_technical%[/code]: Returns the release type as one or two lowercase letters, for example [i]rc[/i][br] ## - [code]%version_type_technical%[/code]: Returns the version type as one or two lowercase letters, for example [i]rc[/i][br]
## - [code]%devmode%[/code]: Returns the development mode status[br] ## - [code]%devmode%[/code]: Returns the development mode status[br]
## - [code]%headless%[/code]: Returns the headless mode status ## - [code]%headless%[/code]: Returns the headless mode status[br]
## - [code]%custommodules%[/code]: Returns if custom module support is enabled
func get_formatted_string(string: String) -> String: func get_formatted_string(string: String) -> String:
# Version strings # Version strings
string = string.replace("%version%", str(version_version)) string = string.replace("%version%", str(version_version))
string = string.replace("%version_type%", str(version_typerelease)) string = string.replace("%version_typerelease%", str(version_typerelease))
var semantic_version: Array[int] = get_version_semantic() var semantic_version: Array[int] = get_version_semantic()
string = string.replace("%version_semantic%", str(semantic_version[0]) + "." + str(semantic_version[1]) + "." + str(semantic_version[2])) string = string.replace("%version_semantic%", str(semantic_version[0]) + "." + str(semantic_version[1]) + "." + str(semantic_version[2]))
match(version_type): match(version_type):
CoreTypes.VersionType.RELEASE: CoreTypes.VersionType.RELEASE:
string = string.replace("%type%", "Release") string = string.replace("%version_type%", "Release")
string = string.replace("%type_technical%", "r") string = string.replace("%version_type_technical%", "r")
CoreTypes.VersionType.RELEASECANDIDATE: CoreTypes.VersionType.RELEASECANDIDATE:
string = string.replace("%type%", "Release Candidate") string = string.replace("%version_type%", "Release Candidate")
string = string.replace("%type_technical%", "rc") string = string.replace("%version_type_technical%", "rc")
CoreTypes.VersionType.BETA: CoreTypes.VersionType.BETA:
string = string.replace("%type%", "Beta") string = string.replace("%version_type%", "Beta")
string = string.replace("%type_technical%", "b") string = string.replace("%version_type_technical%", "b")
CoreTypes.VersionType.ALPHA: CoreTypes.VersionType.ALPHA:
string = string.replace("%type%", "Alpha") string = string.replace("%version_type%", "Alpha")
string = string.replace("%type_technical%", "a") string = string.replace("%version_type_technical%", "a")
_: await logger.crashf("core", "Invalid version type " + str(version_type), true) _: await loggeri.crash("Invalid version type " + str(version_type))
# Development mode # Development mode
if is_devmode(): string = string.replace("%devmode%", "Enabled") if is_devmode(): string = string.replace("%devmode%", "Enabled")
else: string = string.replace("%devmode%", "Disabled") else: string = string.replace("%devmode%", "Disabled")
@ -261,9 +264,8 @@ func get_formatted_string(string: String) -> String:
else: string = string.replace("%custommodules%", "Disabled") else: string = string.replace("%custommodules%", "Disabled")
return string return string
# Return CORE's version in the semantic versioning scheme
## Returns CORE's versioning scheme into the semantic versioning scheme.[br] ## Returns CORE's versioning scheme into the semantic versioning scheme.[br]
## The first integer contains the release number, the second integer contains the release type ([code]0[/code] for alpha, [code]1[/code] for beta, [code]2[/code] for rc and [code]3[/code] for release and the last integer contains the typerelease number. ## The first integer contains the version number, the second integer contains the version type ([code]0[/code] for alpha, [code]1[/code] for beta, [code]2[/code] for rc and [code]3[/code] for release and the last integer contains the version type number.
func get_version_semantic() -> Array[int]: func get_version_semantic() -> Array[int]:
var version_type_int: int var version_type_int: int
match(version_type): match(version_type):
@ -273,10 +275,8 @@ func get_version_semantic() -> Array[int]:
CoreTypes.VersionType.ALPHA: version_type_int = 0 CoreTypes.VersionType.ALPHA: version_type_int = 0
return [version_version, version_type_int, version_typerelease] return [version_version, version_type_int, version_typerelease]
# Determines CORE's installation/base path ## Determines CORE's installation/base path.[br]
## Determines CORE's installation/base path[br] ## [b]Danger: [i]Don't call this.[/i][/b]
## [br]
## [b]Calling this function is likely to be safe, but shouldn't be done nonetheless![/b]
func determine_basepath() -> bool: func determine_basepath() -> bool:
if FileAccess.file_exists("res://.corebasepath"): if FileAccess.file_exists("res://.corebasepath"):
basepath = "res://" basepath = "res://"
@ -309,3 +309,12 @@ func check_godot_version() -> bool:
printerr("The CORE Framework does not support unstable Godot versions. Please switch to Godot stable 4.2.x.") printerr("The CORE Framework does not support unstable Godot versions. Please switch to Godot stable 4.2.x.")
return false return false
return true return true
## Makes sure for all log messages to be flushed and that CORE is correctly cleaned up.[br]
## Using [method SceneTree.quit] directly may cause various issues.[br]
## [b]Note: [i]Using the [code]await[/code] keyword is required for this function.[/i][/b]
func quit_safely(exitcode: int = 0) -> void:
loggeri.info("Shutting down (code " + str(exitcode) + ")")
await get_tree().create_timer(0.25).timeout
await cleanup()
get_tree().quit(exitcode)

View file

@ -18,10 +18,20 @@
## Allows for awaited, batched and oneline requests. ## Allows for awaited, batched and oneline requests.
extends CoreBaseModule extends CoreBaseModule
## Contains a list of all queued downloads.[br]
## [b]Danger: [i]Don't modify this[/i][/b].
var list_queue: Dictionary = {} var list_queue: Dictionary = {}
## Contains a list of all active downloads.[br]
## [b]Danger: [i]Don't modify this[/i][/b].
var list_active: Dictionary = {} var list_active: Dictionary = {}
## Contains a liust of all completed downloads.[br]
## [b]Danger: [i]Don't modify this[/i][/b].
var list_complete: Dictionary = {} var list_complete: Dictionary = {}
## Determines how unsecure requests should be handled.
var config_unsecure_requests: CoreTypes.BlockadeLevel
# +++ module +++
func _cleanup() -> void: func _cleanup() -> void:
clean_queue() clean_queue()
clean_completed() clean_completed()
@ -31,59 +41,113 @@ func _cleanup() -> void:
await get_tree().process_frame await get_tree().process_frame
remove_child(child) remove_child(child)
func generate_id() -> int: # +++ methods that do the heavily lifting +++
var id = randi() ## Requests a file from the internet.[br]
if list_queue.has(id) or list_active.has(id): return generate_id() ## [br]
logger.diagf("erm", "Generated new download id " + str(id)) ## The returned [code]Dictionary[/code] has the following structure (example):
return id ## [codeblock]
## { "result": 0, "http_code": 200, "headers": [ "Server": "nginx" ], "body": [], "body_utf8": [] }
## ^ ^ ^ ^ ^
## | | | | |
## | | | | --------- The body from the server, as a UTF-8 string (set to "" if 'parse_utf8' is false)
## | | | ----------------------- The body from the server, as bytes
## | | ------------------------------------------------------ A array of headers
## | ---------------------------------------------------------------------- The HTTP response code
## ------------------------------------------------------------------------------------ Equal to @GlobalScope.Error. If not 0/Error.OK = the request failed
## [/codeblock]
## [b]Note: [i]Using the [code]await[/code] keyword is required for this function.[/i][/b]
func awaited_request(url: String, parse_utf8: bool, method: HTTPClient.Method = HTTPClient.Method.METHOD_GET, headers: PackedStringArray = PackedStringArray([]), data: String = "") -> Dictionary: func awaited_request(url: String, parse_utf8: bool, method: HTTPClient.Method = HTTPClient.Method.METHOD_GET, headers: PackedStringArray = PackedStringArray([]), data: String = "") -> Dictionary:
logger.verbf("erm", "Creating awaited request") loggeri.verb("Creating awaited request")
var id: int = create_download(url, method, headers, data) if !await is_url_allowed(url): return {}
start_download(id, parse_utf8) var id: int = create_request(url, method, headers, data)
logger.diagf("erm", "Waiting for request " + str(id) + " to finish") start_request(id, parse_utf8)
while !is_download_complete(id): await get_tree().create_timer(0.1, true).timeout loggeri.diag("Waiting for request " + str(id) + " to finish")
while !is_request_completed(id): await get_tree().create_timer(0.1, true).timeout
var dldata: Dictionary = list_complete[id] var dldata: Dictionary = list_complete[id]
list_complete.erase(id) list_complete.erase(id)
return dldata return dldata
## Requests a file from the internet without returning the godot code, http code or headers. Useful for oneliners.[br]
## [br]
## Returns [code]null[/code] on error. To ignore HTTP errors (ie. non-200 statuses) set [code]ignore_http_code[/code] to [code]true[/code].[br]
## Returns a UTF-8 string with [code]return_utf8[/code] turned on, returns bytes when turned off.[br]
## [b]Note: [i]Using the [code]await[/code] keyword is required for this function.[/i][/b]
func oneline_awaited_request(url: String, return_utf8: bool = true, ignore_http_code: bool = false, method: HTTPClient.Method = HTTPClient.Method.METHOD_GET, headers: PackedStringArray = PackedStringArray([]), data: String = "") -> Variant: func oneline_awaited_request(url: String, return_utf8: bool = true, ignore_http_code: bool = false, method: HTTPClient.Method = HTTPClient.Method.METHOD_GET, headers: PackedStringArray = PackedStringArray([]), data: String = "") -> Variant:
var dldata: Dictionary = await awaited_request(url, return_utf8, method, headers, data) var dldata: Dictionary = await awaited_request(url, return_utf8, method, headers, data)
if dldata == {}: return null
if dldata["result"] != Error.OK: return null if dldata["result"] != Error.OK: return null
elif !ignore_http_code and dldata["http_code"] != 200: return null elif !ignore_http_code and dldata["http_code"] != 200: return null
else: else:
if return_utf8: return dldata["body_utf8"] if return_utf8: return dldata["body_utf8"]
else: return dldata["body"] else: return dldata["body"]
## Requests multiple files from the internet.[br]
## [br]
## Thee returned [code]Dictionary[/code]s have the following structure (example):
## [codeblock]
## { "result": 0, "http_code": 200, "headers": [ "Server": "nginx" ], "body": [], "body_utf8": [] }
## ^ ^ ^ ^ ^
## | | | | |
## | | | | --------- The body from the server, as a UTF-8 string (set to "" if 'parse_utf8' is false)
## | | | ----------------------- The body from the server, as bytes
## | | ------------------------------------------------------ A array of headers
## | ---------------------------------------------------------------------- The HTTP response code
## ------------------------------------------------------------------------------------ Equal to @GlobalScope.Error. If not 0/Error.OK = the request failed
## [/codeblock]
## [b]Note: [i]Using the [code]await[/code] keyword is required for this function.[/i][/b]
func batch_awaited_request(urls: PackedStringArray, parse_utf8: bool, method: HTTPClient.Method = HTTPClient.Method.METHOD_GET, headers: PackedStringArray = PackedStringArray([]), data: String = "") -> Array[Dictionary]: func batch_awaited_request(urls: PackedStringArray, parse_utf8: bool, method: HTTPClient.Method = HTTPClient.Method.METHOD_GET, headers: PackedStringArray = PackedStringArray([]), data: String = "") -> Array[Dictionary]:
logger.verbf("erm", "Creating " + str(urls.size()) + " awaited request(s)") loggeri.verb("Creating " + str(urls.size()) + " awaited request(s)")
var dldata: Array[Dictionary]= [] var dldata: Array[Dictionary] = []
for url in urls: for url in urls:
var id: int = create_download(url, method, headers, data) if !await is_url_allowed(url): continue
start_download(id, parse_utf8) var thread: Thread = Thread.new()
logger.diagf("erm", "Waiting for request " + str(id) + " to finish") thread.start(Callable(self, "_batch_awaited_request").bind(url, parse_utf8, method, headers, data))
while !is_download_complete(id): await get_tree().create_timer(0.1, true).timeout var id: int = thread.wait_to_finish()
dldata.append(list_complete[id]) dldata.append(list_complete[id])
list_complete.erase(id) list_complete.erase(id)
return dldata return dldata
func create_download(url: String, method: HTTPClient.Method = HTTPClient.Method.METHOD_GET, headers: PackedStringArray = PackedStringArray([]), body: String = "") -> int: ## Internal function, do not call
logger.verbf("erm", "Creating new request\n-> URL: " + url + "\n-> Method: " + str(method) + "\nHeaders: " + str(headers.size()) + "\nBody size: " + str(body.length()) + " Characters") func _batch_awaited_request(url: String, parse_utf8: bool, method: HTTPClient.Method = HTTPClient.Method.METHOD_GET, headers: PackedStringArray = PackedStringArray([]), data: String = "") -> int:
var id: int = create_request(url, method, headers, data)
start_request(id, parse_utf8)
loggeri.diag("Waiting for request " + str(id) + " to finish")
while !is_request_completed(id): await get_tree().create_timer(0.1, true).timeout
return id
# +++ internal +++
## Returns a new download id.[br]
## [b]Danger: [i]Don't call this.[/i][/b]
func generate_id() -> int:
var id = randi()
if list_queue.has(id) or list_active.has(id):
loggeri.warn("New download id '" + str(id) + "' is already taken")
return generate_id()
loggeri.diag("Generated new download id " + str(id))
return id
## Creates a new request and stores it in the queue. Returns the download id.[br]
## [b]Warning: [i]You'll probably not need this. Only use this function when implementing your own downloading method.[/i][/b]
func create_request(url: String, method: HTTPClient.Method = HTTPClient.Method.METHOD_GET, headers: PackedStringArray = PackedStringArray([]), body: String = "") -> int:
loggeri.verb("Creating new request\n-> URL: " + url + "\n-> Method: " + str(method) + "\nHeaders: " + str(headers.size()) + "\nBody size: " + str(body.length()) + " Characters")
var id = generate_id() var id = generate_id()
list_queue.merge({ id: { "url": url, "method": method, "headers": headers, "body": body } }) list_queue.merge({ id: { "url": url, "method": method, "headers": headers, "body": body } })
return id return id
func start_download(id: int, parse_utf8: bool) -> void: ## Configures and starts a queued request.[br]
logger.verbf("erm", "Starting request " + str(id)) ## [b]Note: [i]Using the [code]await[/code] keyword is required for this function.[/i][/b][br]
## [b]Warning: [i]You'll probably not need this. Only use this function when implementing your own downloading method.[/i][/b]
func start_request(id: int, parse_utf8: bool) -> void:
loggeri.verb("Starting request " + str(id))
list_active.merge({ id: list_queue[id] }) list_active.merge({ id: list_queue[id] })
list_queue.erase(id) list_queue.erase(id)
logger.diagf("erm", "Creating new HTTPRequest \"Request #" + str(id) + "\"") loggeri.diag("Creating new HTTPRequest \"Request #" + str(id) + "\"")
var download: HTTPRequest = HTTPRequest.new() var download: HTTPRequest = HTTPRequest.new()
download.name = "Request #" + str(id) download.name = "Request #" + str(id)
download.accept_gzip = true download.accept_gzip = true
download.use_threads = true download.use_threads = true
var lambda: Callable = func(result: int, http_code: int, headers: PackedStringArray, body: PackedByteArray, httprequest: HTTPRequest) -> void: var lambda: Callable = func(result: int, http_code: int, headers: PackedStringArray, body: PackedByteArray, httprequest: HTTPRequest) -> void:
logger.verbf("erm", "Request " + str(id) + " completed\nResult: " + str(result) + "\nHTTP response code: " + str(http_code) + "\nHeaders: " + str(headers.size()) + "\nBody size: " + str(body.size()) + " Bytes // " + str(core.misc.byte2mib(body.size(), true)) + " MiB\nParse body as UTF-8: " + str(parse_utf8)) loggeri.verb("Request " + str(id) + " completed\nResult: " + str(result) + "\nHTTP response code: " + str(http_code) + "\nHeaders: " + str(headers.size()) + "\nBody size: " + str(body.size()) + " Bytes // " + str(core.misc.byte2mib(body.size(), true)) + " MiB\nParse body as UTF-8: " + str(parse_utf8))
list_complete.merge({ id: { "result": result, "http_code": http_code, "headers": headers, "body": body, "body_utf8": body.get_string_from_utf8() if parse_utf8 else "" } }) list_complete.merge({ id: { "result": result, "http_code": http_code, "headers": headers, "body": body, "body_utf8": body.get_string_from_utf8() if parse_utf8 else "" } })
list_active.erase(id) list_active.erase(id)
remove_child(httprequest) remove_child(httprequest)
@ -92,12 +156,31 @@ func start_download(id: int, parse_utf8: bool) -> void:
add_child(download) add_child(download)
download.request(list_active[id]["url"], list_active[id]["headers"], list_active[id]["method"], list_active[id]["body"]) download.request(list_active[id]["url"], list_active[id]["headers"], list_active[id]["method"], list_active[id]["body"])
func is_download_complete(id: int) -> bool: return list_complete.has(id) ## Checks if [code]url[/code] can be used.
func is_url_allowed(url: String) -> bool:
if url.begins_with("http://"):
match(config_unsecure_requests):
CoreTypes.BlockadeLevel.BLOCK:
loggeri.error("Blocked unsecure url '" + url + "'")
return false
CoreTypes.BlockadeLevel.WARN: loggeri.warn("Requesting unsecure url '" + url + "'")
CoreTypes.BlockadeLevel.IGNORE: pass
_: await loggeri.crash("Invalid BlockadeLevel '" + str(config_unsecure_requests) + "'")
elif url.begins_with("https://"): pass
else:
loggeri.error("Invalid url '" + url + "'")
return false
return true
## Returns if a request has completed yet.
func is_request_completed(id: int) -> bool: return list_complete.has(id)
## Cleans the request queue.
func clean_queue() -> void: func clean_queue() -> void:
logger.verbf("erm", "Cleaning request queue") loggeri.verb("Cleaning request queue")
list_queue.clear() list_queue.clear()
## Cleans the completed requests list.
func clean_completed() -> void: func clean_completed() -> void:
logger.verbf("erm", "Cleaning completed requests") loggeri.verb("Cleaning completed requests")
list_complete.clear() list_complete.clear()

View file

@ -21,17 +21,21 @@
## large variety of placeholders usable in [param config_format]. ## large variety of placeholders usable in [param config_format].
extends CoreBaseModule extends CoreBaseModule
# Signals
signal log_event signal log_event
# Configuration ## The minimum log level you want to be displayed.
var config_level: CoreTypes.LoggerLevel var config_level: CoreTypes.LoggerLevel
## Determines if the logger's output should be colored.
var config_colored: bool var config_colored: bool
## The template for all log messages[br]
## Available placeholders are: [code]%time%[/code], [code]%time_ms%[/code], [code]%level%[/code], [code]%color%[/code], [code]%message%[/code], [code]%source%[/code], [code]%source_raw%[/code], [code]%function%[/code] and [code]%line%[/code]
var config_format: String var config_format: String
## Determines if identation should be provided if the logger encounters a newline.
var config_newlines_override: bool var config_newlines_override: bool
## The maximum amount of characters excluding the message before newlines are no longer idented. Set to [code]-1[/code] to disable this behaviour.
var config_newlines_sizelimit: int var config_newlines_sizelimit: int
# Update configuration # +++ module +++
func _pull_config() -> void: func _pull_config() -> void:
config_level = core.config.logger_level config_level = core.config.logger_level
config_colored = core.config.logger_colored config_colored = core.config.logger_colored
@ -39,7 +43,9 @@ func _pull_config() -> void:
config_newlines_override = core.config.logger_newlines_override config_newlines_override = core.config.logger_newlines_override
config_newlines_sizelimit = core.config.logger_newlines_sizelimit config_newlines_sizelimit = core.config.logger_newlines_sizelimit
# Creates log messages # +++ logging +++
## The main logging function that does the heavy lifting.[br]
## [b]Danger: [i]Don't call this.[/i][/b]
func _log(level: CoreTypes.LoggerLevel, origin: String, message: String) -> void: func _log(level: CoreTypes.LoggerLevel, origin: String, message: String) -> void:
if !is_level_allowed(level): if !is_level_allowed(level):
emit_signal("log_event", false, level, origin, message, "") emit_signal("log_event", false, level, origin, message, "")
@ -82,19 +88,20 @@ func _log(level: CoreTypes.LoggerLevel, origin: String, message: String) -> void
if config_colored: print_rich(format) if config_colored: print_rich(format)
else: print(format) else: print(format)
# Check if level is allowed # +++ self explanitory +++
func is_level_allowed(level: CoreTypes.LoggerLevel) -> bool: ## Prints a diagnostic log message.
if level <= config_level: return true
else: return false
# Self explanitory
func diag(origin: String, message: String) -> void: _log(CoreTypes.LoggerLevel.DIAG, origin, message) func diag(origin: String, message: String) -> void: _log(CoreTypes.LoggerLevel.DIAG, origin, message)
## Prints a verbose log message.
func verb(origin: String, message: String) -> void: _log(CoreTypes.LoggerLevel.VERB, origin, message) func verb(origin: String, message: String) -> void: _log(CoreTypes.LoggerLevel.VERB, origin, message)
## Prints an informational log message.
func info(origin: String, message: String) -> void: _log(CoreTypes.LoggerLevel.INFO, origin, message) func info(origin: String, message: String) -> void: _log(CoreTypes.LoggerLevel.INFO, origin, message)
## Prints a warning log message.
func warn(origin: String, message: String) -> void: _log(CoreTypes.LoggerLevel.WARN, origin, message) func warn(origin: String, message: String) -> void: _log(CoreTypes.LoggerLevel.WARN, origin, message)
## Prints an error log message.
func error(origin: String, message: String) -> void: _log(CoreTypes.LoggerLevel.ERROR, origin, message) func error(origin: String, message: String) -> void: _log(CoreTypes.LoggerLevel.ERROR, origin, message)
## Handles crashes. Will terminate your game/application immediately.[br]
# Built-in crash handler for CORE and applications using it ## [b]Note: [i]Using the [code]await[/code] keyword is required for this function.[/i][/b][br]
## [b]Danger: [i]Don't set [code]framework_crash[/code] to [code]true[/code], thanks![/i][/b]
func crash(origin: String, message: String, framework_crash: bool = false) -> void: func crash(origin: String, message: String, framework_crash: bool = false) -> void:
# Collect information # Collect information
var stack: Array[Dictionary] = get_stack() var stack: Array[Dictionary] = get_stack()
@ -139,6 +146,8 @@ VIDEO
-> Adapter Information %video_adapter% -> Adapter Information %video_adapter%
+++ GODOT ENGINE INFORMATION +++ +++ GODOT ENGINE INFORMATION +++
ENGINE
-> Version %godot_version%
BUILD BUILD
-> Debug build %godot_debug% -> Debug build %godot_debug%
-> Sandboxed %godot_sandboxed% -> Sandboxed %godot_sandboxed%
@ -154,11 +163,11 @@ STACKTRACE
else: crash_message = crash_message.replace("%causer%", "The running application") else: crash_message = crash_message.replace("%causer%", "The running application")
crash_message = crash_message.replace("%origin%", origin) crash_message = crash_message.replace("%origin%", origin)
crash_message = crash_message.replace("%message%", message) crash_message = crash_message.replace("%message%", message)
crash_message = crash_message.replace("%version_release%", str(core.version_release)) crash_message = crash_message.replace("%version_release%", str(core.version_version))
crash_message = crash_message.replace("%version_type%", await core.get_formatted_string("%type%")) crash_message = crash_message.replace("%version_type%", await core.get_formatted_string("%version_type%"))
crash_message = crash_message.replace("%version_typerelease%", str(core.version_typerelease)) crash_message = crash_message.replace("%version_typerelease%", str(core.version_typerelease))
crash_message = crash_message.replace("%version_full%", str(core.version_release) + await core.get_formatted_string("-%type_technical%") + str(core.version_typerelease)) crash_message = crash_message.replace("%version_full%", str(core.version_version) + await core.get_formatted_string("-%version_type_technical%") + str(core.version_typerelease))
crash_message = crash_message.replace("%version_semantic%", await core.get_formatted_string("%release_semantic%")) crash_message = crash_message.replace("%version_semantic%", await core.get_formatted_string("%version_semantic%"))
crash_message = crash_message.replace("%devmode%", str(core.is_devmode())) crash_message = crash_message.replace("%devmode%", str(core.is_devmode()))
crash_message = crash_message.replace("%headless%", str(core.config.headless)) crash_message = crash_message.replace("%headless%", str(core.config.headless))
crash_message = crash_message.replace("%os%", OS.get_name()) crash_message = crash_message.replace("%os%", OS.get_name())
@ -174,6 +183,7 @@ STACKTRACE
crash_message = crash_message.replace("%processor_name%", OS.get_processor_name()) crash_message = crash_message.replace("%processor_name%", OS.get_processor_name())
crash_message = crash_message.replace("%processor_count%", str(OS.get_processor_count())) crash_message = crash_message.replace("%processor_count%", str(OS.get_processor_count()))
crash_message = crash_message.replace("%video_adapter%", str(OS.get_video_adapter_driver_info())) crash_message = crash_message.replace("%video_adapter%", str(OS.get_video_adapter_driver_info()))
crash_message = crash_message.replace("%godot_version%", str(Engine.get_version_info()["string"]))
crash_message = crash_message.replace("%godot_debug%", str(OS.is_debug_build())) crash_message = crash_message.replace("%godot_debug%", str(OS.is_debug_build()))
crash_message = crash_message.replace("%godot_sandboxed%", str(OS.is_sandboxed())) crash_message = crash_message.replace("%godot_sandboxed%", str(OS.is_sandboxed()))
crash_message = crash_message.replace("%godot_persistance%", str(OS.is_userfs_persistent())) crash_message = crash_message.replace("%godot_persistance%", str(OS.is_userfs_persistent()))
@ -184,15 +194,13 @@ STACKTRACE
# Print crash message # Print crash message
_log(CoreTypes.LoggerLevel.NONE, origin, crash_message) _log(CoreTypes.LoggerLevel.NONE, origin, crash_message)
# Shutdown # Shutdown
await core.misc.quit_safely(69) await core.quit_safely(69)
# Makes CORE development easier # +++ etc +++
func diagf(origin: String, message: String) -> void: _log(CoreTypes.LoggerLevel.DIAG, core.basepath.replace("res://", "") + "src/" + origin + ".gd", message) ## Checks if the specified log level is allowed by the current configuration.
func verbf(origin: String, message: String) -> void: _log(CoreTypes.LoggerLevel.VERB, core.basepath.replace("res://", "") + "src/" + origin + ".gd", message) func is_level_allowed(level: CoreTypes.LoggerLevel) -> bool:
func infof(origin: String, message: String) -> void: _log(CoreTypes.LoggerLevel.INFO, core.basepath.replace("res://", "") + "src/" + origin + ".gd", message) if level <= config_level: return true
func warnf(origin: String, message: String) -> void: _log(CoreTypes.LoggerLevel.WARN, core.basepath.replace("res://", "") + "src/" + origin + ".gd", message) else: return false
func errorf(origin: String, message: String) -> void: _log(CoreTypes.LoggerLevel.ERROR, core.basepath.replace("res://", "") + "src/" + origin + ".gd", message)
func crashf(origin: String, message: String) -> void: crash(core.basepath.replace("res://", "") + "src/" + origin + ".gd", message)
# Returns a logger instance ## Returns a [class CoreLoggerInstance], which is a fancy word meaning you don't need to pass [code]origin[/code] every time you want to log something.
func get_instance(origin: String) -> CoreLoggerInstance: return CoreLoggerInstance.new(self, origin) func get_instance(origin: String) -> CoreLoggerInstance: return CoreLoggerInstance.new(self, origin)

View file

@ -28,6 +28,20 @@ var logrtl: RichTextLabel
var font_normal: Font var font_normal: Font
var font_bold: Font var font_bold: Font
# +++ module +++
func _pull_config() -> void:
background.visible = !core.config.headless and core.config.logui_enabled
background.color = core.config.logui_background_color
logrtl.add_theme_font_size_override("normal_font_size", core.config.logui_font_size)
logrtl.add_theme_font_size_override("bold_font_size", core.config.logui_font_size)
func _cleanup() -> void:
background.remove_child(logrtl)
core.sms.remove_child(background)
logrtl.queue_free()
background.queue_free()
# +++ initialization +++
func _initialize() -> void: func _initialize() -> void:
# Load fonts into memory # Load fonts into memory
font_normal = load(core.basepath + "dist/FiraCode/Regular.ttf") font_normal = load(core.basepath + "dist/FiraCode/Regular.ttf")
@ -78,18 +92,7 @@ func _ready() -> void:
# Connect log_event # Connect log_event
logger.connect("log_event", func(allowed: bool, _level: CoreTypes.LoggerLevel, _origin: String, _message: String, format: String) -> void: if allowed: logrtl.text = logrtl.text + format + "\n") logger.connect("log_event", func(allowed: bool, _level: CoreTypes.LoggerLevel, _origin: String, _message: String, format: String) -> void: if allowed: logrtl.text = logrtl.text + format + "\n")
func _cleanup() -> void: # +++ process +++
background.remove_child(logrtl)
core.sms.remove_child(background)
logrtl.queue_free()
background.queue_free()
func _pull_config() -> void:
background.visible = !core.config.headless and core.config.logui_enabled
background.color = core.config.logui_background_color
logrtl.add_theme_font_size_override("normal_font_size", core.config.logui_font_size)
logrtl.add_theme_font_size_override("bold_font_size", core.config.logui_font_size)
func _process(_delta: float) -> void: func _process(_delta: float) -> void:
if !core.config.headless: if !core.config.headless:
var window_size: Vector2i = DisplayServer.window_get_size() var window_size: Vector2i = DisplayServer.window_get_size()

View file

@ -15,44 +15,67 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
## Contains various useful functions that do not fit into other modules. ## Contains various utility functions.
## ##
## Contains various useful functions that you can use to save yourself some time ## This module contains many methods that don't fit into any other module
## programming or searching. ## and generally make your life as a developer easier.
extends CoreBaseModule extends CoreBaseModule
func quit_safely(exitcode: int = 0) -> void: # +++ data type conversion +++
logger.infof("misc", "Shutting down (code " + str(exitcode) + ")") ## Converts a number of bytes into mebibytes.[br]
logger.diagf("misc", "Waiting for log messages to be flushed") ## [br]
await get_tree().create_timer(0.25).timeout ## If [code]flatten[/code] is set to [code]true[/code], the decimal part will be discarded.
await core.cleanup()
get_tree().quit(exitcode)
@warning_ignore("integer_division") @warning_ignore("integer_division")
func byte2mib(bytes: int, flatten: bool = true) -> float: func byte2mib(bytes: int, flatten: bool = true) -> float:
if flatten: return bytes/1048576 if flatten: return bytes/1048576
return bytes/float(1048576) return bytes/float(1048576)
## Converts a number of mebibytes into bytes.[br]
## [br]
## If [code]flatten[/code] is set to [code]true[/code], the decimal part will be discarded.
func mib2byte(mib: float, flatten: bool = true) -> float: func mib2byte(mib: float, flatten: bool = true) -> float:
if flatten: return int(mib*1048576) if flatten: return int(mib*1048576)
return mib*1048576 return mib*1048576
## Converts a number of mebibytes into gibibytes.[br]
## [br]
## If [code]flatten[/code] is set to [code]true[/code], the decimal part will be discarded.
func mib2gib(mib: float, flatten: bool = true) -> float: func mib2gib(mib: float, flatten: bool = true) -> float:
if flatten: return int(mib/1024) if flatten: return int(mib/1024)
return mib/1024 return mib/1024
## Converts a number of gebibytes into mebibytes.[br]
## [br]
## If [code]flatten[/code] is set to [code]true[/code], the decimal part will be discarded.
func gib2mib(gib: float, flatten: bool = true) -> float: func gib2mib(gib: float, flatten: bool = true) -> float:
if flatten: return int(gib*1024) if flatten: return int(gib*1024)
return gib*1024 return gib*1024
# +++ type formatting +++
## Converts a string array into a normal, nicely formatted string.[br]
## [br]
## With [code]item_before[/code] and [code]item_after[/code] you can customize the lists apperance. Here's an example on how to format every item bold:
## [codeblock]
## extends Node
##
## var core: Core = get_node("/root/CORE")
## var misc: CoreBaseModule = core.misc
## var logger: CoreLoggerInstance = core.logger.get_instance("some/script.gd")
##
## func _ready() -> void:
## var array: Array[String] = ["Apples", "Bananas", "Oranges"]
##
## logger.info(misc.format_stringarray(array))
## logger.info(misc.format_stringarray(array, "[b]", "[/b]"))
## [/codeblock]
func format_stringarray(array: Array[String], item_before: String = "", item_after: String = "", separator_list: String = ", ", separator_final: String = " & ") -> String: func format_stringarray(array: Array[String], item_before: String = "", item_after: String = "", separator_list: String = ", ", separator_final: String = " & ") -> String:
var output: String = "" var output: String = ""
if array.size() == 0: if array.size() == 0:
logger.warnf("misc", "Unable to format a string with a size of 0") loggeri.warn("Unable to format a string with a size of 0")
return "" return ""
elif array.size() == 1: elif array.size() == 1:
logger.warnf("misc", "Unable to format a string with a size of 1") loggeri.warn("Unable to format a string with a size of 1")
return array[0] return array[0]
for item in array: for item in array:
@ -62,6 +85,10 @@ func format_stringarray(array: Array[String], item_before: String = "", item_aft
return output return output
# +++ array conversion +++
## Converts an array into a string array.[br]
## [br]
## If an item is found that is not of type [code]String[/code], an empty array is returned.
func array_to_stringarray(array: Array) -> Array[String]: func array_to_stringarray(array: Array) -> Array[String]:
var output: Array[String] = [] var output: Array[String] = []
@ -73,6 +100,7 @@ func array_to_stringarray(array: Array) -> Array[String]:
return output return output
## Converts a string array into an array.
func stringarray_to_array(array: Array[String]) -> Array: func stringarray_to_array(array: Array[String]) -> Array:
var output: Array = [] var output: Array = []
@ -81,5 +109,22 @@ func stringarray_to_array(array: Array[String]) -> Array:
return output return output
# +++ etc +++
## Calculates the center of the child in the area of the parent.[br]
## [br]
## Example:
## [codeblock]
## extends Control
##
## var core: Core = get_node("/root/CORE")
## var misc: CoreBaseModule = core.misc
##
## func _ready() -> void:
## position = misc.center_object(get_parent().size, size)
## [/codeblock]
func get_center(parent_size: Vector2, child_size: Vector2) -> Vector2: func get_center(parent_size: Vector2, child_size: Vector2) -> Vector2:
return Vector2(parent_size.x/2-child_size.x/2, parent_size.y/2-child_size.y/2) return Vector2(parent_size.x/2-child_size.x/2, parent_size.y/2-child_size.y/2)
## Moved to [method Core.quit_safely].
## @deprecated
func quit_safely(exitcode: int = 0) -> void: await core.quit_safely(exitcode)

View file

@ -15,22 +15,34 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
## Manages scenes more efficiently. ## Manages scenes for you efficiently.
## ##
## Allows for organized scene management, making development much faster. ## Allows for organized scene management, making development much faster.
extends CoreBaseModule extends CoreBaseModule
# Objects ## Used internally for adding, managing and removing scene collections.
const scene_nodes: Array[String] = ["debug", "cutscene", "menu", "main", "background"] const scene_nodes: Array[String] = [ "debug", "cutscene", "menu", "main", "background" ]
## The 'debug' scene collection.[br]
## [b]Danger: [i]Don't modify this.[/i][/b]
var scenes_debug: Node = Node.new() var scenes_debug: Node = Node.new()
## The 'cutscene' scene collection.[br]
## [b]Danger: [i]Don't modify this.[/i][/b]
var scenes_cutscene: Node = Node.new() var scenes_cutscene: Node = Node.new()
## The 'menu' scene collection.[br]
## [b]Danger: [i]Don't modify this.[/i][/b]
var scenes_menu: Node = Node.new() var scenes_menu: Node = Node.new()
## The 'main' scene collection.[br]
## [b]Danger: [i]Don't modify this.[/i][/b]
var scenes_main: Node = Node.new() var scenes_main: Node = Node.new()
## The 'background' scene collection.[br]
## [b]Danger: [i]Don't modify this.[/i][/b]
var scenes_background: Node = Node.new() var scenes_background: Node = Node.new()
# Variables ## A list of all loaded scenes[br]
## [b]Danger: [i]Don't modify this.[/i][/b]
var scenes: Dictionary = {} var scenes: Dictionary = {}
# +++ module +++
func _initialize() -> void: func _initialize() -> void:
for scene in scene_nodes: for scene in scene_nodes:
get("scenes_" + scene).name = scene.to_upper() get("scenes_" + scene).name = scene.to_upper()
@ -47,18 +59,19 @@ func _cleanup() -> void:
func _pull_config() -> void: func _pull_config() -> void:
if core.config.headless: if core.config.headless:
# Remove all scenes # Remove all scenes
if is_inside_tree(): logger.verbf("sms", "Removing all scenes (triggered by headless mode)") if is_inside_tree(): loggeri.verb("Removing all scenes (triggered by headless mode)")
for scene in scenes: remove_scene(scene, true) for scene in scenes: remove_scene(scene, true)
# Add a scene to some scene collection # +++ scene management +++
## Adds a scene to some scene collection.
func add_scene(sname: String, sclass: Node, type: CoreTypes.SceneType) -> bool: func add_scene(sname: String, sclass: Node, type: CoreTypes.SceneType) -> bool:
if core.config.headless: return false if core.config.headless: return false
logger.verbf("sms", "Adding scene \"" + sname + "\" of type " + str(type)) loggeri.verb("Adding scene \"" + sname + "\" of type " + str(type))
if exists(sname) != CoreTypes.SceneType.NONE: if exists(sname) != CoreTypes.SceneType.NONE:
logger.errorf("sms", "Scene with name \"" + sname + "\" already exists") loggeri.error("Scene with name \"" + sname + "\" already exists")
return false return false
if typeof(sclass) != TYPE_OBJECT or !sclass.is_class("Node"): if typeof(sclass) != TYPE_OBJECT or !sclass.is_class("Node"):
logger.errorf("sms", "Scene class \"" + sname + "\" is not of type Node") loggeri.error("Scene class \"" + sname + "\" is not of type Node")
return false return false
sclass.name = sname sclass.name = sname
match(type): match(type):
@ -68,17 +81,18 @@ func add_scene(sname: String, sclass: Node, type: CoreTypes.SceneType) -> bool:
CoreTypes.SceneType.MAIN: scenes_main.add_child(sclass) CoreTypes.SceneType.MAIN: scenes_main.add_child(sclass)
CoreTypes.SceneType.BACKGROUND: scenes_background.add_child(sclass) CoreTypes.SceneType.BACKGROUND: scenes_background.add_child(sclass)
CoreTypes.SceneType.NONE: CoreTypes.SceneType.NONE:
logger.errorf("sms", "CoreTypes.SceneType.NONE is not a valid scene type") loggeri.error("CoreTypes.SceneType.NONE is not a valid scene type")
return false return false
_: await logger.crashf("sms", "Invalid SceneType " + str(type), true) _: await loggeri.crash("Invalid SceneType " + str(type))
scenes.merge({ sname: { "type": type, "class": sclass } }) scenes.merge({ sname: { "type": type, "class": sclass } })
return true return true
# Remove a scene from some scene collection ## Removes a scene from some scene collection.[br]
## [b]Danger: [i]Don't set [code]force_remove[/code] to [code]true[/code], thanks![/i][/b]
func remove_scene(sname: String, force_remove: bool = false) -> bool: func remove_scene(sname: String, force_remove: bool = false) -> bool:
if core.config.headless and !force_remove: return false if core.config.headless and !force_remove: return false
if force_remove: await logger.crashf("sms", "force_remove = true is not allowed", true) if force_remove: await loggeri.crash("force_remove = true is not allowed")
logger.verbf("sms", "Removing scene \"" + sname + "\"") loggeri.verb("Removing scene \"" + sname + "\"")
match(exists(sname)): match(exists(sname)):
CoreTypes.SceneType.DEBUG: CoreTypes.SceneType.DEBUG:
scenes_debug.remove_child(scenes[sname]["class"]) scenes_debug.remove_child(scenes[sname]["class"])
@ -96,13 +110,16 @@ func remove_scene(sname: String, force_remove: bool = false) -> bool:
scenes_background.remove_child(scenes[sname]["class"]) scenes_background.remove_child(scenes[sname]["class"])
scenes[sname]["class"].queue_free() scenes[sname]["class"].queue_free()
CoreTypes.SceneType.NONE: CoreTypes.SceneType.NONE:
logger.errorf("sms", "Scene \"" + sname + "\" does not exist") loggeri.error("Scene \"" + sname + "\" does not exist")
return false return false
_: await logger.crashf("sms", "Invalid SceneType " + str(exists(sname)), true) _: await loggeri.crash("Invalid SceneType " + str(exists(sname)))
scenes.erase(sname) scenes.erase(sname)
return true return true
# Return a loaded scene # +++ getters +++
## Returns a scene from some scene collection.[br]
## [br]
## Returns [code]null[/code] if no scene with that name was found.
func get_scene(sname: String) -> Node: func get_scene(sname: String) -> Node:
if core.config.headless: return null if core.config.headless: return null
match(exists(sname)): match(exists(sname)):
@ -111,11 +128,13 @@ func get_scene(sname: String) -> Node:
CoreTypes.SceneType.MENU: return scenes[sname]["class"] CoreTypes.SceneType.MENU: return scenes[sname]["class"]
CoreTypes.SceneType.MAIN: return scenes[sname]["class"] CoreTypes.SceneType.MAIN: return scenes[sname]["class"]
CoreTypes.SceneType.BACKGROUND: return scenes[sname]["class"] CoreTypes.SceneType.BACKGROUND: return scenes[sname]["class"]
CoreTypes.SceneType.NONE: logger.errorf("sms", "Scene \"" + sname + "\" does not exist") CoreTypes.SceneType.NONE: loggeri.error("Scene \"" + sname + "\" does not exist")
_: await logger.crashf("sms", "Invalid SceneType " + str(exists(sname)), true) _: await loggeri.crash("Invalid SceneType " + str(exists(sname)))
return null return null
# Return a scene collection for scene manipulation ## Returns a scene collection node.[br]
## Useful if you want to change a child's index.[br]
## [b]Danger: [i]Don't change any properties of the scene collection or free it, otherwise you may cause breakage.[/i][/b]
func get_scene_collection(type: CoreTypes.SceneType) -> Node: func get_scene_collection(type: CoreTypes.SceneType) -> Node:
if core.config.headless: return null if core.config.headless: return null
match(type): match(type):
@ -124,11 +143,11 @@ func get_scene_collection(type: CoreTypes.SceneType) -> Node:
CoreTypes.SceneType.MENU: return scenes_menu CoreTypes.SceneType.MENU: return scenes_menu
CoreTypes.SceneType.MAIN: return scenes_main CoreTypes.SceneType.MAIN: return scenes_main
CoreTypes.SceneType.BACKGROUND: return scenes_background CoreTypes.SceneType.BACKGROUND: return scenes_background
CoreTypes.SceneType.NONE: logger.errorf("sms", "No scene collection was found for CoreTypes.SceneType.NONE") CoreTypes.SceneType.NONE: loggeri.error("No scene collection was found for CoreTypes.SceneType.NONE")
_: await logger.crashf("sms", "Invalid SceneType " + str(type), true) _: await loggeri.crash("Invalid SceneType " + str(type))
return null return null
# Return scenes in some scene collection ## Returns a list of all loaded scenes in some scene collection.
func get_scene_collection_list(type: CoreTypes.SceneType) -> Array[Node]: func get_scene_collection_list(type: CoreTypes.SceneType) -> Array[Node]:
var list: Array[Node] = [] var list: Array[Node] = []
for scene in scenes: for scene in scenes:
@ -136,7 +155,7 @@ func get_scene_collection_list(type: CoreTypes.SceneType) -> Array[Node]:
list.append(scenes[scene]["class"]) list.append(scenes[scene]["class"])
return list return list
# Return scene count in some scene collection ## Returns the number of loaded scenes in some scene collection.
func get_scene_collection_count(type: CoreTypes.SceneType) -> int: func get_scene_collection_count(type: CoreTypes.SceneType) -> int:
var amount: int = 0 var amount: int = 0
for scene in scenes: for scene in scenes:
@ -144,7 +163,9 @@ func get_scene_collection_count(type: CoreTypes.SceneType) -> int:
amount += 1 amount += 1
return amount return amount
# Return scene existance & scene collection ## Returns the scene collection a scene is loaded in.[br]
## [br]
## [enum CoreTypes.SceneType][code].NONE[/code] if no scene with that name was found.
func exists(sname: String) -> CoreTypes.SceneType: func exists(sname: String) -> CoreTypes.SceneType:
for scene in scenes: for scene in scenes:
if scene == sname: return scenes[scene]["type"] if scene == sname: return scenes[scene]["type"]

View file

@ -16,137 +16,163 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
extends CoreBaseModule extends CoreBaseModule
## Easy config management.
##
## Allows you to read and write configuration files with ease, without any headaches.
## Indicates if a storage file is currently open.[br]
## [b]Danger: [i]Don't modify this.[/i][/b]
var is_open: bool = false var is_open: bool = false
## The parsed data inside the storage file.[br]
## [b]Danger: [i]Don't modify this.[/i][/b]
var storage: Dictionary = {} var storage: Dictionary = {}
## The location of the storage file.[br]
## [b]Danger: [i]Don't modify this.[/i][/b]
var storage_location: String = "" var storage_location: String = ""
# +++ file management +++
## Opens a storage file into memory.
func open_storage(location: String, create_new: bool = true, sanity_check: bool = true, fail_on_sanity_check: bool = false) -> bool: func open_storage(location: String, create_new: bool = true, sanity_check: bool = true, fail_on_sanity_check: bool = false) -> bool:
if is_open: if is_open:
logger.errorf("storage", "Failed to open storage: A storage file is already open") loggeri.error("Failed to open storage: A storage file is already open")
return false return false
logger.verbf("storage", "Opening storage file at \"" + location + "\"") loggeri.verb("Opening storage file at \"" + location + "\"")
var file: FileAccess var file: FileAccess
if !FileAccess.file_exists(location): if !FileAccess.file_exists(location):
if create_new: if create_new:
file = FileAccess.open(location, FileAccess.WRITE) file = FileAccess.open(location, FileAccess.WRITE)
if file == null: if file == null:
await logger.crashf("storage", "Could not open storage file at \"" + location + "\": Failed with code " + str(FileAccess.get_open_error())) await loggeri.crash("Could not open storage file at \"" + location + "\": Failed with code " + str(FileAccess.get_open_error()))
return false return false
file.store_string("{}") file.store_string("{}")
file.close() file.close()
else: else:
logger.errorf("storage", "Failed to open storage: create_new is set to false") loggeri.error("Failed to open storage: create_new is set to false")
return false return false
file = FileAccess.open(location, FileAccess.READ) file = FileAccess.open(location, FileAccess.READ)
var storage_temp: Variant = file.get_as_text() var storage_temp: Variant = file.get_as_text()
file.close() file.close()
storage_temp = JSON.parse_string(storage_temp) storage_temp = JSON.parse_string(storage_temp)
if typeof(storage_temp) != TYPE_DICTIONARY: if typeof(storage_temp) != TYPE_DICTIONARY:
logger.errorf("storage", "Failed to open storage: Parsed storage file is of type " + str(typeof(storage_temp))) loggeri.error("Failed to open storage: Parsed storage file is of type " + str(typeof(storage_temp)))
return false return false
if sanity_check: if sanity_check:
var check_result: Array[String] = perform_sanity_check(storage_temp) var check_result: Array[String] = perform_sanity_check(storage_temp)
if check_result.size() != 0: if check_result.size() != 0:
if fail_on_sanity_check: if fail_on_sanity_check:
logger.errorf("storage", "Sanity check failed (stopping):") loggeri.error("Sanity check failed (stopping):")
for error in check_result: for error in check_result:
logger.errorf("storage", "-> " + error) loggeri.error("-> " + error)
return false return false
else: else:
logger.warnf("storage", "Sanity check failed (continuing anyway):") loggeri.warn("Sanity check failed (continuing anyway):")
for error in check_result: for error in check_result:
logger.warnf("storage", "-> " + error) loggeri.warn("-> " + error)
storage = storage_temp storage = storage_temp
storage_location = location storage_location = location
is_open = true is_open = true
return true return true
## Closes the active storage file.
func close_storage() -> bool: func close_storage() -> bool:
if !is_open: if !is_open:
logger.errorf("storage", "Failed to close storage: No storage file is open") loggeri.error("Failed to close storage: No storage file is open")
return false return false
logger.verbf("storage", "Closing storage file") loggeri.verb("Closing storage file")
storage = {} storage = {}
is_open = false is_open = false
return true return true
## Saves the active storage file to disk.
func save_storage() -> bool: func save_storage() -> bool:
if !is_open: if !is_open:
logger.errorf("storage", "Failed to save storage: No storage file is open") loggeri.error("Failed to save storage: No storage file is open")
return false return false
var file: FileAccess = FileAccess.open(storage_location, FileAccess.WRITE) var file: FileAccess = FileAccess.open(storage_location, FileAccess.WRITE)
if file == null: if file == null:
await logger.crashf("storage", "Could not open storage file at \"" + storage_location + "\": Failed with code " + str(FileAccess.get_open_error())) await loggeri.crash("Could not open storage file at \"" + storage_location + "\": Failed with code " + str(FileAccess.get_open_error()))
return false return false
logger.diagf("storage", "Writing storage file to disk") loggeri.diag("Writing storage file to disk")
file.store_string(JSON.stringify(storage)) file.store_string(JSON.stringify(storage))
file.close() file.close()
return true return true
# +++ config manipulation +++
## Removes all keys from the active storage file. The nuclear option basically.
func nuke_storage(autosave: bool = true) -> bool: func nuke_storage(autosave: bool = true) -> bool:
if !is_open: if !is_open:
logger.errorf("storage", "Failed to nuke storage: No storage file is open") loggeri.error("Failed to nuke storage: No storage file is open")
return false return false
logger.warnf("storage", "Nuking storage") loggeri.warn("Nuking storage")
storage = {} storage = {}
if autosave: save_storage() if autosave: save_storage()
return true return true
## Returns a storage key. Can also return a default value if unset.
func get_key(key: String, default: Variant = null) -> Variant: func get_key(key: String, default: Variant = null) -> Variant:
if !is_open: if !is_open:
logger.errorf("storage", "Failed to get key: No storage file is open") loggeri.error("Failed to get key: No storage file is open")
return NAN return NAN
logger.diagf("storage", "Returning storage key \"" + key + "\" (default='" + str(default) + "')") loggeri.diag("Returning storage key \"" + key + "\" (default='" + str(default) + "')")
return storage.get(key, default) return storage.get(key, default)
## Updates a storage key with the specified value.
func set_key(key: String, value: Variant, overwrite: bool = true, autosave: bool = true) -> bool: func set_key(key: String, value: Variant, overwrite: bool = true, autosave: bool = true) -> bool:
if !is_open: if !is_open:
logger.errorf("storage", "Failed to set key: No storage file is open") loggeri.error("Failed to set key: No storage file is open")
return false return false
logger.diagf("storage", "Updating storage key \"" + key + "\" with value '" + str(value) + "' (overwrite='" + str(overwrite) + "' autosave='" + str(autosave) + "'") loggeri.diag("Updating storage key \"" + key + "\" with value '" + str(value) + "' (overwrite='" + str(overwrite) + "' autosave='" + str(autosave) + "'")
storage.merge({key: value}, overwrite) storage.merge({key: value}, overwrite)
if autosave: save_storage() if autosave: save_storage()
return true return true
## Deletes a storage key.
func del_key(key: String, autosave: bool = true) -> bool: func del_key(key: String, autosave: bool = true) -> bool:
if !is_open: if !is_open:
logger.errof("storage", "Failed to delete key: No storage file is open") logger.errof("storage", "Failed to delete key: No storage file is open")
return false return false
logger.diagf("storage", "Deleting storage key \"" + key + "\" (autosave='" + str(autosave) + "')") loggeri.diag("Deleting storage key \"" + key + "\" (autosave='" + str(autosave) + "')")
storage.erase(key) storage.erase(key)
if autosave: save_storage() if autosave: save_storage()
return true return true
## Returns the [param storage] [class Dictionary], useful for more advanced operations.[br]
## Changes are not reflected onto the [param storage] though. To save changes through this module,
## pass your modified [class Dictionary to [method safe_dict].
func get_dict() -> Dictionary: func get_dict() -> Dictionary:
if !is_open: if !is_open:
logger.errorf("storage", "Failed to get dictionary: No storage file is open") loggeri.error("Failed to get dictionary: No storage file is open")
return {} return {}
logger.verbf("storage", "Returning storage dictionary") loggeri.verb("Returning storage dictionary")
return storage return storage
# +++ raw manipulation +++
## Saves a arbitrary dictionary as a [param storage] [class Dictionary] with sanity checking ([code]sanity_check[/code] and [code]fail_on_sanity_check[/code]).
func save_dict(dict: Dictionary, sanity_check: bool = true, fail_on_sanity_check: bool = false, autosave: bool = true) -> bool: func save_dict(dict: Dictionary, sanity_check: bool = true, fail_on_sanity_check: bool = false, autosave: bool = true) -> bool:
if !is_open: if !is_open:
logger.errorf("storage", "Failed to save dictionary: No storage file is open") loggeri.error("Failed to save dictionary: No storage file is open")
return false return false
logger.verbf("storage", "Saving custom dictionary as storage") loggeri.verb("Saving custom dictionary as storage")
if sanity_check: if sanity_check:
var check_result: Array[String] = perform_sanity_check(dict) var check_result: Array[String] = perform_sanity_check(dict)
if check_result.size() != 0: if check_result.size() != 0:
if fail_on_sanity_check: if fail_on_sanity_check:
logger.errorf("storage", "Sanity check failed (stopping):") loggeri.error("Sanity check failed (stopping):")
for error in check_result: for error in check_result:
logger.errorf("storage", "-> " + error) loggeri.error("-> " + error)
return false return false
else: else:
logger.warnf("storage", "Sanity check failed (continuing anyway):") loggeri.warn("Sanity check failed (continuing anyway):")
for error in check_result: for error in check_result:
logger.warnf("storage", "-> " + error) loggeri.warn("-> " + error)
storage = dict storage = dict
if autosave: save_storage() if autosave: save_storage()
return true return true
# +++ etc +++
## Performs sanity checks on a [class Dictionary] to determine if it can be saved and loaded using this module.
func perform_sanity_check(storage_check: Dictionary) -> Array[String]: func perform_sanity_check(storage_check: Dictionary) -> Array[String]:
logger.verbf("storage", "Performing a sanity check on some storage dictionary") loggeri.verb("Performing a sanity check on some storage dictionary")
var errors: Array[String] = [] var errors: Array[String] = []
for key in storage_check: for key in storage_check:
if typeof(key) != TYPE_STRING: if typeof(key) != TYPE_STRING:
@ -155,5 +181,5 @@ func perform_sanity_check(storage_check: Dictionary) -> Array[String]:
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: 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)) + "')") 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", "Completed sanity check with " + str(errors.size()) + " errors") loggeri.verb("Completed sanity check with " + str(errors.size()) + " errors")
return errors return errors