2
0
Fork 0

Update to 85fcda93b21460bd22830e1a72f6b416566d9132

I unfortunately forgot to update this repository with the latest commits.
This commit is contained in:
JeremyStar™ 2024-05-07 20:16:35 +02:00
parent 4aa2943d29
commit e73b2d849d
Signed by: JeremyStarTM
GPG key ID: E366BAEF67E4704D
13 changed files with 564 additions and 111 deletions

View file

@ -24,10 +24,8 @@ class_name CoreBaseModule
## Contains a reference to the CORE Object.
var core: Core
## Set to CORE's logger implementation.
@onready var logger: CoreBaseModule = core.logger
## 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
## Set to a [class CoreLoggerInstance] with the path you supplied to [method Core.register_custom_module].
var logger: CoreLoggerInstance
## Marks a module as fully initialized and ready. **Don't forget to set this!**
var initialized: bool = false

View file

@ -67,7 +67,7 @@ func _init() -> void:
# Global
headless = false
development = false
custom_modules = false
custom_modules = true
automatic_shutdown = true
hide_window_on_shutdown = true

View file

@ -23,9 +23,11 @@ class_name CoreTypes
## Available version types, following the StarOpenSource Versioning Specification (SOSVS) version 1.
enum VersionType { RELEASE, RELEASECANDIDATE, BETA, ALPHA }
## Available log levels, followingthe StarOpenSource Logging Specification (SOSLS) version 1.
## Available log levels, following the StarOpenSource Logging Specification (SOSLS) version 1.
enum LoggerLevel { NONE, SPECIAL, ERROR, WARN, INFO, VERB, DIAG }
## Available scene types.
enum SceneType { NONE, DEBUG, CUTSCENE, MENU, MAIN, BACKGROUND }
## To what degree [i]something[/i] should be blocked.
enum BlockadeLevel { IGNORE, WARN, BLOCK }
## All validation rules some data can be checked against.
enum ValidationType { MATCHES_TYPE, MATCHES_CLASS, IN_RANGE, HAS_MINIMUM, HAS_MAXIMUM, HAS_VALUES, CONTAINS, MATCHES_REGEX, IS_NOT_EMPTY, IS_NOT_NULL, IS_NORMALIZED, IS_ORTHONORMALIZED }

View file

@ -0,0 +1,51 @@
# 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 <https://www.gnu.org/licenses/>.
## Validates multiple [CoreValidationSingle]s at once.
extends Node
class_name CoreValidationSchema
## Internal, don't modify.
var core: Core
## Internal, don't modify.
var logger: CoreLoggerInstance
## Internal, don't modify.
var parent: Node
## Contains the schema to validate.[br]
## [b]Note: [i]Don't modify.[/i][/b]
var schema: Dictionary = {}
func _init(core_new: Core, schema_new: Dictionary, parent_new: Node) -> void:
core = core_new
logger = core.logger.get_instance(core.basepath.replace("res://", "") + "src/classes/validationschema.gd", self)
parent = parent_new
schema = schema_new
# Check Dictionary
for key in schema:
if typeof(key) != TYPE_STRING: logger.error(core.stringify_variables("Could not parse schema: Schema key %key% is not of type String", { "key": key }))
elif typeof(schema[key]) != TYPE_OBJECT: logger.error(core.stringify_variables("Could not parse schema: Schema value of %key% is not of type Object", { "key": key }))
elif schema[key].get_class() != "Node": logger.error(core.stringify_variables("Could not parse schema: Schema value of %key% is not of type Node", { "key": key }))
func evaluate() -> Array[String]:
var failed: Array[String] = []
for single in schema:
if !schema[single].evaluate():
logger.error(core.stringify_variables("Single %single% failed", { "single": single }))
failed.append(single)
return failed

View file

@ -0,0 +1,312 @@
# 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 <https://www.gnu.org/licenses/>.
## Validates some [param data] against a set of predefined rules.
extends Node
class_name CoreValidationSingle
## Internal, don't modify.
var core: Core
## Internal, don't modify.
var logger: CoreLoggerInstance
## Is set to the parent owning the validation single.[br]
## [b]Note: [i]Don't modify.[/i][/b]
var parent: Node
## Contains the data to validate.[br]
## [b]Note: [i]Don't modify.[/i][/b]
var data # This is the only instance where we don't want to define what type this variable should have
## All rules to evaluate in [method evaluate].[br]
## [b]Note: [i]Don't modify.[/i][/b]
var rules: Array[Dictionary]
## Contains error messages for failed rules.[br]
## [b]Note: [i]Don't modify.[/i][/b]
var failures: Array[String] = []
# +++ constructor +++
func _init(core_new: Core, data_new, parent_new: Node) -> void:
core = core_new
logger = core.logger.get_instance(core.basepath.replace("res://", "") + "src/classes/validationsingle.gd", self)
parent = parent_new
data = data_new
# +++ evaluation +++
## Evaluates all set rules and returns [code]true[/code] if all rules passed, or [code]false[/code] if at least one failed.
func evaluate() -> bool:
# Reset failures
failures = []
# Iterate through all rules
for rule in rules:
match(rule["type"]):
CoreTypes.ValidationType.MATCHES_TYPE:
var success: bool = false
for type in rule["types"]:
if typeof(data) == type:
success = true
if !success:
var types_string: String = ""
for type in rule["types"]:
if types_string == "": types_string = type_string(type)
else: types_string = types_string + "|" + type_string(type)
failures.append(core.stringify_variables("Data is not of expected type(s) %expected% (is %got%)", { "expected": types_string, "got": type_string(typeof(data)) }))
CoreTypes.ValidationType.MATCHES_CLASS:
# If not an object, skip
if typeof(data) != Variant.Type.TYPE_OBJECT:
logger.warn("Can't match class as data is not of type Object")
continue
if rule["exact"]:
# Only check if class name matches specified class
if data.get_class() != rule["class"]:
failures.append(core.stringify_variables("Expected exact class %expected% but data is of class %got%", { "expected": rule["class"], "got": data.get_class() }))
else:
# Check if class is or inherits specified class
if !data.is_class(rule["class"]):
failures.append(core.stringify_variables("Expected class %expected% but data is of class %got%", { "expected": rule["class"], "got": data.get_class() }))
CoreTypes.ValidationType.IN_RANGE:
# If not an int or float, skip
match(rule["matched_against"]):
"integer":
if typeof(data) != Variant.Type.TYPE_INT:
logger.warn("Can't match class as data is not of type int")
continue
"float":
if typeof(data) != Variant.Type.TYPE_FLOAT:
logger.warn("Can't match class as data is not of type float")
continue
_: logger.crash(core.stringify_variables("Invalid 'matched_against' value %value% for IN_RANGE", { "value": rule["matched_against"] }))
# Check if from is bigger than to
if rule["from"] > rule["to"]:
logger.error(core.stringify_variables("Can't match invalid %type% range %from% to %to%", { "type": rule["matched_against"], "from": rule["from"], "to": rule["to"] }))
continue
# Perform checks
if data < rule["from"]: failures.append(core.stringify_variables("Data is smaller than %type% range start %expected%", { "type": rule["matched_against"], "expected": rule["from"] }))
if data > rule["to"]: failures.append(core.stringify_variables("Data is larger than %type% range end %expected%", { "type": rule["matched_against"], "expected": rule["to"] }))
CoreTypes.ValidationType.HAS_MINIMUM:
# If not an int or float, skip
match(rule["matched_against"]):
"integer":
if typeof(data) != Variant.Type.TYPE_INT:
logger.warn("Can't determine if data has minimum value as data is not of type int")
continue
"float":
if typeof(data) != Variant.Type.TYPE_FLOAT:
logger.warn("Can't determine if data has minimum value as data is not of type float")
continue
_: logger.crash(core.stringify_variables("Invalid 'matched_against' value %value% for HAS_MINIMUM", { "value": rule["matched_against"] }))
# Perform check
if data < rule["minimum"]: failures.append(core.stringify_variables("Data is smaller than minimum %type% value %expected%", { "type": rule["matched_against"], "expected": rule["minimum"] }))
CoreTypes.ValidationType.HAS_MAXIMUM:
# If not an int or float, skip
match(rule["matched_against"]):
"integer":
if typeof(data) != Variant.Type.TYPE_INT:
logger.warn("Can't determine if data has maximum value as data is not of type int")
continue
"float":
if typeof(data) != Variant.Type.TYPE_FLOAT:
logger.warn("Can't determine if data has maximum value as data is not of type float")
continue
_: logger.crash(core.stringify_variables("Invalid 'matched_against' value %value% for HAS_MINIMUM", { "value": rule["matched_against"] }))
# Perform check
if data > rule["maximum"]: failures.append(core.stringify_variables("Data is smaller than minimum %type% value %expected%", { "type": rule["matched_against"], "expected": rule["maximum"] }))
CoreTypes.ValidationType.HAS_VALUES:
var success: bool = false
for value in rule["values"]:
if data == value:
success = true
if !success: failures.append("Data did not match a single provided value")
CoreTypes.ValidationType.CONTAINS:
# If not a String or StringName, skip
if typeof(data) != Variant.Type.TYPE_STRING or typeof(data) != Variant.Type.TYPE_STRING_NAME:
logger.warn("Can't determine if data contains values as data is not of type String or StringName")
continue
var successes: int = 0
for value in rule["values"]:
if data.contains(value):
successes += 1
# Success if equals or bigger than 'minimum_matches'
if successes < rule["minimum_matches"]: failures.append(core.stringify_variables("Data did matched %got% out of %expected% expected strings", { "got": successes, "expected": rule["minimum_matches"] }))
CoreTypes.ValidationType.MATCHES_REGEX:
# If not a String or StringName, skip
if typeof(data) != Variant.Type.TYPE_STRING or typeof(data) != Variant.Type.TYPE_STRING_NAME:
logger.warn("Can't determine if data matches regex as data is not of type String or StringName")
continue
var regex: RegEx = RegEx.new()
# Compile regex
regex.compile(rule["regex_string"])
# Get result
var result: RegExMatch = regex.search(data)
# If result yielded no result, fail
if !result: failures.append(core.stringify_variables("Data doesn't match regex %regex%", { "regex": rule["regex_string"] }))
CoreTypes.ValidationType.IS_NOT_EMPTY:
match(typeof(data)):
Variant.Type.TYPE_STRING: if data == "": failures.append("Data string is empty")
Variant.Type.TYPE_STRING_NAME: if data == "": failures.append("Data string name is empty")
Variant.Type.TYPE_INT: if data == 0: failures.append("Data integer is zero")
Variant.Type.TYPE_FLOAT: if data == 0.0: failures.append("Data float is zero")
# If not a String, StringName, int or float, skip
_: logger.warn("Can't determine if data is null as data is not of type String, StringName, int or float")
CoreTypes.ValidationType.IS_NOT_NULL:
# ⡴⠑⡄⠀⠀⠀⠀⠀⠀⠀ ⣀⣀⣤⣤⣤⣀⡀
# ⠸⡇⠀⠿⡀⠀⠀⠀⣀⡴⢿⣿⣿⣿⣿⣿⣿⣿⣷⣦⡀
# ⠀⠀⠀⠀⠑⢄⣠⠾⠁⣀⣄⡈⠙⣿⣿⣿⣿⣿⣿⣿⣿⣆
# ⠀⠀⠀⠀⢀⡀⠁⠀⠀⠈⠙⠛⠂⠈⣿⣿⣿⣿⣿⠿⡿⢿⣆
# ⠀⠀⠀⢀⡾⣁⣀⠀⠴⠂⠙⣗⡀⠀⢻⣿⣿⠭⢤⣴⣦⣤⣹⠀⠀⠀⢀⢴⣶⣆
# ⠀⠀⢀⣾⣿⣿⣿⣷⣮⣽⣾⣿⣥⣴⣿⣿⡿⢂⠔⢚⡿⢿⣿⣦⣴⣾⠸⣼⡿
# ⠀⢀⡞⠁⠙⠻⠿⠟⠉⠀⠛⢹⣿⣿⣿⣿⣿⣌⢤⣼⣿⣾⣿⡟⠉
# ⠀⣾⣷⣶⠇⠀⠀⣤⣄⣀⡀⠈⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇
# ⠀⠉⠈⠉⠀⠀⢦⡈⢻⣿⣿⣿⣶⣶⣶⣶⣤⣽⡹⣿⣿⣿⣿⡇
# ⠀⠀⠀⠀⠀⠀⠀⠉⠲⣽⡻⢿⣿⣿⣿⣿⣿⣿⣷⣜⣿⣿⣿⡇
# ⠀⠀⠀⠀⠀⢸⣿⣿⣷⣶⣮⣭⣽⣿⣿⣿⣿⣿⣿⣿⠇
# ⠀⠀⠀⠀⠀⠀⣀⣀⣈⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠇
# ⠀⠀⠀⠀⠀⠀⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠃
# https://steamcommunity.com/sharedfiles/filedetails/?id=2902061979
if data == null: failures.append("Data is null")
CoreTypes.ValidationType.IS_NORMALIZED:
# If not a Vector2, Vector3, Vector4, Plane or Quaternion, skip
match(typeof(data)):
Variant.Type.TYPE_VECTOR2: pass
Variant.Type.TYPE_VECTOR3: pass
Variant.Type.TYPE_VECTOR4: pass
Variant.Type.TYPE_PLANE: pass
Variant.Type.TYPE_QUATERNION: pass
_:
logger.warn("Can't determine if data is normalized as data is not of type Vector2, Vector3, Vector4, Plane or Quaternion")
continue
# Perform check
if !data.is_normalized(): failures.append("Data is not normalized")
CoreTypes.ValidationType.IS_ORTHONORMALIZED:
# If not a Transform2D, Transform3D, or Basis, skip
match(typeof(data)):
Variant.Type.TYPE_TRANSFORM2D: pass
Variant.Type.TYPE_TRANSFORM3D: pass
Variant.Type.TYPE_BASIS: pass
_:
logger.warn("Can't determine if data is orthonormalized as data is not of type Transform2D, Transform3D or Basis")
continue
# Perform check
if !data.is_orthonormalized(): failures.append("Data is not orthonormalized")
_: await logger.crash(core.stringify_variables("Invalid validation rule type %type%", { "type": rule["type"] }))
return failures.size() == 0
# +++ types and classes +++
## Validates if [param data] matches some [enum Variant.Type].[br]
## Applies to all data types (obviously).
func matches_type(types: Array[Variant.Type]) -> CoreValidationSingle:
rules.append({ "type": CoreTypes.ValidationType.MATCHES_TYPE, "types": types })
return self
## Validates if [param data] matches some class.[br]
## Applies to [Object].
func matches_class(clazz: StringName, exact: bool) -> CoreValidationSingle:
rules.append({ "type": CoreTypes.ValidationType.MATCHES_CLASS, "class": clazz, "exact": exact })
return self
# +++ ranges +++
## Validates if [param data] contains the specified integer range.[br]
## Applies to [int].
func in_range_int(from: int, to: int) -> CoreValidationSingle:
rules.append({ "type": CoreTypes.ValidationType.IN_RANGE, "matched_against": "integer", "from": from, "to": to })
return self
## Validates if [param data] contains the specified float range.[br]
## Applies to [float].
func in_range_float(from: float, to: float) -> CoreValidationSingle:
rules.append({ "type": CoreTypes.ValidationType.IN_RANGE, "matched_against": "float", "from": from, "to": to })
return self
## Ensures that [param data] is equal to or exceeds the specified integer.[br]
## Applies to [int].
func has_minimum_int(minimum: int) -> CoreValidationSingle:
rules.append({ "type": CoreTypes.ValidationType.HAS_MINIMUM, "matched_against": "integer", "minimum": minimum })
return self
## Ensures that [param data] is under the specified integer.[br]
## Applies to [int].
func has_maximum_int(maximum: int) -> CoreValidationSingle:
rules.append({ "type": CoreTypes.ValidationType.HAS_MAXIMUM, "matched_against": "integer", "maximum": maximum })
return self
## Ensures that [param data] is equal to or exceeds the specified float.[br]
## Applies to [float].
func has_minimum_float(minimum: float) -> CoreValidationSingle:
rules.append({ "type": CoreTypes.ValidationType.HAS_MINIMUM, "matched_against": "float", "minimum": minimum })
return self
## Ensures that [param data] is under the specified float.[br]
## Applies to [float].
func has_maximum_float(maximum: float) -> CoreValidationSingle:
rules.append({ "type": CoreTypes.ValidationType.HAS_MAXIMUM, "matched_against": "float", "maximum": maximum })
return self
# +++ values +++
## Checks whenether at least one value matches [param data].[br]
## Applies to all data types.
func has_values(values: Array) -> CoreValidationSingle:
rules.append({ "type": CoreTypes.ValidationType.HAS_VALUES, "values": values })
return self
## Ensures that [param data] contains at least <[code]minimum_matches[/code]> values.[br]
## Applies to [String] & [StringName].
func contains(values: Array[String], minimum_matches: int = 1) -> CoreValidationSingle:
rules.append({ "type": CoreTypes.ValidationType.HAS_VALUES, "values": values, "minimum_matches": minimum_matches })
return self
## Matches a regular expression against [param data].[br]
## Applies to [String] & [StringName].
func matches_regex(regex_string: String) -> CoreValidationSingle:
rules.append({ "type": CoreTypes.ValidationType.MATCHES_REGEX, "regex_string": regex_string })
return self
# +++ empty/null and booleans +++
## Ensures that [param data] is not empty.[br]
## Applies to [String] & [StringName] ([code]!= ""[/code]), [int] ([code]!= 0[/code]) and [float] ([code]!= 0.0[/code]).
func is_not_empty() -> CoreValidationSingle:
rules.append({ "type": CoreTypes.ValidationType.IS_NOT_EMPTY })
return self
## Ensures that [param data] is not [code]null[/code].
func is_not_null() -> CoreValidationSingle:
rules.append({ "type": CoreTypes.ValidationType.IS_NOT_NULL })
return self
## Ensures that [param data] is normalized.[br]
## Applies to [Vector2], [Vector3], [Vector4], [Plane] and [Quaternion].
func is_normalized() -> CoreValidationSingle:
rules.append({ "type": CoreTypes.ValidationType.IS_NORMALIZED })
return self
## Ensures that [param data] is orthonormalized.[br]
## Applies to [Transform2D], [Transform3D] and [Basis].
func is_orthonormalized() -> CoreValidationSingle:
rules.append({ "type": CoreTypes.ValidationType.IS_ORTHONORMALIZED })
return self

View file

@ -29,10 +29,12 @@ const version_version: int = 1
const version_type: CoreTypes.VersionType = CoreTypes.VersionType.RELEASE
## The version type number. Resets on every new version and version type.
const version_typerelease: int = 1
## The fork indicator. Update this if you intend on soft or hard forking this framework.
const version_fork: String = ""
# 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", "validation" ]
## CORE's configuration object.[br]
## [b]NEVER access this yourself! To change the configuration use [method reload_configuration] instead.[/b]
var config: CoreConfiguration
@ -52,6 +54,8 @@ var logui: CoreBaseModule
var erm: CoreBaseModule
## The 'Storage' module
var storage: CoreBaseModule
## The 'Data Validation' module
var validation: CoreBaseModule
# /etc/
## Stores the path to CORE's installation directory.[br]
@ -89,6 +93,7 @@ var initduration_initialization: int = 0
var initduration_complete_initialization: int = 0
# +++ initialization +++
# Preinitialization
func _init(new_config: CoreConfiguration = CoreConfiguration.new()) -> void:
var inittime: int = Time.get_ticks_msec()
name = "CORE"
@ -101,6 +106,7 @@ func _init(new_config: CoreConfiguration = CoreConfiguration.new()) -> void:
initialize_scheduler()
initduration_preinitialization = Time.get_ticks_msec() - inittime
# Initialization
func _ready() -> void:
var inittime: int = Time.get_ticks_msec()
inject_modules()
@ -119,7 +125,7 @@ func initialize_modules() -> void:
get(module).name = module
get(module).set_script(load(basepath + "src/" + module + ".gd"))
get(module).core = self
get(module).loggeri = logger.get_instance(basepath.replace("res://", "") + "src/" + module + ".gd", get(module))
get(module).logger = logger.get_instance(basepath.replace("res://", "") + "src/" + module + ".gd", get(module))
get(module)._initialize()
# Injects CORE's builtin modules into the SceneTree.
@ -148,8 +154,8 @@ func initialize_scheduler() -> void:
## [i][b]Not calling this function during startup may lead to runtime issues.[/b][/i]
func complete_init() -> void:
var inittime: int = Time.get_ticks_msec()
var modsinit_builtin: Array[String] = ["workaround"]
var modsinit_custom: Array[String] = ["workaround"]
var modsinit_builtin: Array[String] = [ "workaround" ]
var modsinit_custom: Array[String] = [ "workaround" ]
while modsinit_builtin.size() != 0 and modsinit_custom.size() != 0:
# Clear arrays
@ -170,13 +176,16 @@ func complete_init() -> void:
if modsinit_custom.size() != 0: print(" Custom: " + str(modsinit_custom))
await get_tree().create_timer(1).timeout
initduration_complete_initialization = Time.get_ticks_msec() - inittime
# Initialization complete
await get_tree().process_frame
if !welcomed:
welcomed = true
logger._log(CoreTypes.LoggerLevel.SPECIAL, basepath.replace("res://", "") + "src/core.gd", """_________________________________ __________ ______
var version_welcome: String = await get_formatted_string("v%version%-%version_type_technical%%version_typerelease%%version_fork%")
if version_welcome.length() > 15:
await logger.crash("Invalid version size of 15")
elif version_welcome.length() < 15:
version_welcome = " ".repeat(15-version_welcome.length()) + version_welcome
logger._log(CoreTypes.LoggerLevel.SPECIAL, basepath.replace("res://", "") + "src/core.gd", """_________________________________ __________ %version% ______
__ ____/_ __ \\__ __ \\__ ____/ ___ ____/____________ _______ ___________ _________________ /__
_ / _ / / /_ /_/ /_ __/ __ /_ __ ___/ __ `/_ __ `__ \\ _ \\_ | /| / / __ \\_ ___/_ //_/
/ /___ / /_/ /_ _, _/_ /___ _ __/ _ / / /_/ /_ / / / / / __/_ |/ |/ // /_/ / / _ ,<
@ -187,10 +196,12 @@ You should have recieved a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Consider contributing to the CORE Framework to make it even better... and remember: #TransRightsAreHumanRights
Thank you for using the CORE Framework to develop your application or game!""")
loggeri.info("Framework initialization took " + str(initduration_preinitialization + initduration_initialization + initduration_complete_initialization) + "ms (pre " + str(initduration_preinitialization) + "ms, init " + str(initduration_initialization) + "ms, complete " + str(initduration_complete_initialization) + "ms)")
Thank you for using the CORE Framework to develop your application or game!""".replace("%version%", version_welcome))
if is_devmode(): loggeri.warn("The CORE Framework is running in development mode.\nThis may cause bugs and issues inside the framework. Here be dragons!")
if logger.verbose_mode: loggeri.warn("Godot is running in verbose stdout mode.\nDue to a bug in the engine that prevents displaying truecolor ANSI escape\nsequences CORE changed the color of all diagnostic log messages.\nTo prevent this, set 'logger_detect_verbose_mode' in the configuration to 'false'.")
initduration_complete_initialization = Time.get_ticks_msec() - inittime
loggeri.info("Framework initialization took " + str(initduration_preinitialization + initduration_initialization + initduration_complete_initialization) + "ms (pre " + str(initduration_preinitialization) + "ms, init " + str(initduration_initialization) + "ms, complete " + str(initduration_complete_initialization) + "ms)")
# +++ configuration +++
## Loads a (new) configuration object and applies it to all modules.
@ -228,13 +239,13 @@ func register_custom_module(module_name: String, module_origin: String, module_c
loggeri.error("Registering module failed: Custom module support is disabled.")
return false
if custom_modules.has(module_name):
loggeri.error("Registering module failed: A custom module with the name \"" + module_name + "\" already exists.")
loggeri.error(stringify_variables("Registering module failed: A custom module with the name %name% already exists.", { "name": module_name }))
return false
loggeri.diag("Updating variables")
module_class.name = module_name
module_class.core = self
module_class.loggeri = logger.get_instance(module_origin, module_class)
module_class.loggeri.framework = true
module_class.logger = logger.get_instance(module_origin, module_class)
module_class.logger.framework = true
loggeri.diag("Adding module to SceneTree")
custom_modules_node.add_child(module_class)
loggeri.diag("Merging module with custom_modules")
@ -249,11 +260,11 @@ func register_custom_module(module_name: String, module_origin: String, module_c
func unregister_custom_module(module_name: String) -> void:
loggeri.verb("Unregistering custom module \"" + module_name + "\"")
if !custom_modules.has(module_name):
loggeri.error("Unregistering module failed: A custom module with the name \"" + module_name + "\" does not exist.")
loggeri.error(stringify_variables("Unregistering module failed: A custom module with the name %name% does not exist.", { "name": module_name }))
return
var module: Node = custom_modules[module_name]
await module._cleanup()
module.loggeri.queue_free()
module.logger.queue_free()
custom_modules_node.remove_child(module)
custom_modules.erase(module_name)
module.queue_free()
@ -263,7 +274,7 @@ func unregister_custom_module(module_name: String) -> void:
func get_custom_module(module_name: String) -> CoreBaseModule:
loggeri.diag("Getting custom module \"" + module_name + "\"")
if !custom_modules.has(module_name):
loggeri.error("Getting module failed: A custom module with the name \"" + module_name + "\" does not exist.")
loggeri.error(stringify_variables("Getting module failed: A custom module with the name %name% does not exist.", { "name": module_name }))
return null
return custom_modules[module_name]
@ -276,7 +287,7 @@ func cleanup() -> void:
loggeri.verb("Calling cleanup hooks")
for hook in cleanup_hooks:
if !cleanup_hooks[hook].is_valid():
loggeri.error("Cleanup hook #" + str(hook) + " is invalid")
loggeri.error(stringify_variables("Cleanup hook %id% is invalid", { "id": hook }))
else:
loggeri.diag("Calling cleanup hook #" + str(hook))
await cleanup_hooks[hook].call()
@ -302,10 +313,10 @@ func cleanup() -> void:
func _generate_hook_id() -> int:
var id = randi()
if cleanup_hooks.has(id):
loggeri.warn("New cleanup hook id #" + str(id) + " is already taken")
loggeri.warn(stringify_variables("New cleanup hook id %id% is already taken", { "id": id }))
return _generate_hook_id()
elif id == -1:
loggeri.warn("Invalid cleanup hook id '-1'")
loggeri.warn(stringify_variables("Invalid cleanup hook id %id%", { "id": id }))
return _generate_hook_id()
return id
@ -324,9 +335,9 @@ func register_cleanup_hook(callable: Callable) -> int:
## Unregisters a cleanup hook by it's id.
func unregister_cleanup_hook_by_id(id: int) -> bool:
if !cleanup_hooks.has(id):
loggeri.error("Could not remove cleanup hook (id): Hook #" + str(id) + " does not exist")
loggeri.error(stringify_variables("Could not remove cleanup hook (id): Hook %id% does not exist", { "id": id }))
return false
loggeri.verb("Removed cleanup hook #" + str(id) + " (id)")
loggeri.verb(stringify_variables("Removed cleanup hook %id%", { "id": id }))
cleanup_hooks.erase(id)
return true
@ -336,9 +347,9 @@ func unregister_cleanup_hook_by_ref(callable: Callable) -> bool:
if id == null:
loggeri.error("Could not remove cleanup hook (ref): Could not find a matching hook")
return false
if typeof(id) != TYPE_INT: await loggeri.crash("Could not remove cleanup hook (ref): find_key did not return an integer (returned '" + str(id) + "')")
loggeri.verb("Removed cleanup hook #" + str(id) + " (ref)")
if typeof(id) != TYPE_INT: await loggeri.crash(stringify_variables("Could not remove cleanup hook (ref): find_key did not return an integer (returned %id%)", { "id": id }))
cleanup_hooks.erase(id)
loggeri.verb(stringify_variables("Removed cleanup hook %id% (ref)", { "id": id }))
return true
# +++ etc +++
@ -360,6 +371,7 @@ func get_formatted_string(string: String) -> String:
# Version strings
string = string.replace("%version%", str(version_version))
string = string.replace("%version_typerelease%", str(version_typerelease))
string = string.replace("%version_fork%", "" if version_fork == "" else "-" + version_fork)
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]))
match(version_type):
@ -455,10 +467,16 @@ func quit_safely(exitcode: int = 0) -> void:
await cleanup()
get_tree().quit(exitcode)
# Just ignore this.
## Internal, don't call
# Makes misc.stringify_variables() calls shorter
func stringify_variables(template: String, arguments: Dictionary, no_quotes: bool = false) -> String:
return misc.stringify_variables(template, arguments, no_quotes, true)
## Internal, don't call.
# Just ignore this.
func _notification(what) -> void:
match(what):
NOTIFICATION_WM_CLOSE_REQUEST:
if config.automatic_shutdown:
loggeri.diag("Got close request, shutting down")
await quit_safely(0)

View file

@ -54,11 +54,12 @@ func _cleanup() -> void:
## [/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:
loggeri.verb("Creating awaited request")
logger.verb("Creating awaited request")
if !await is_url_allowed(url): return {}
var id: int = create_request(url, method, headers, data)
start_request(id, parse_utf8)
loggeri.diag("Waiting for request " + str(id) + " to finish")
logger.diag("Waiting for request " + str(id) + " to finish")
logger.diag(core.stringify_variables("Waiting for request %id% to finish", { "id": id }))
while !is_request_completed(id): await get_tree().create_timer(0.1, true).timeout
var dldata: Dictionary = list_complete[id]
list_complete.erase(id)
@ -93,7 +94,7 @@ func oneline_awaited_request(url: String, return_utf8: bool = true, ignore_http_
## [/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]:
loggeri.verb("Creating " + str(urls.size()) + " awaited request(s)")
logger.verb("Creating " + str(urls.size()) + " awaited request(s)")
var dldata: Array[Dictionary] = []
for url in urls:
if !await is_url_allowed(url): continue
@ -108,7 +109,7 @@ func batch_awaited_request(urls: PackedStringArray, parse_utf8: bool, method: HT
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")
logger.diag(core.stringify_variables("Waiting for request %id% to finish", { "id": id }))
while !is_request_completed(id): await get_tree().create_timer(0.1, true).timeout
return id
@ -117,15 +118,15 @@ func _batch_awaited_request(url: String, parse_utf8: bool, method: HTTPClient.Me
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")
logger.warn(core.stringify_variables("New download id %id% already taken", { "id": id }))
return generate_id()
loggeri.diag("Generated new download id " + str(id))
logger.diag(core.stringify_variables("Generated new download id %id%", { "id": 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")
logger.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()
list_queue.merge({ id: { "url": url, "method": method, "headers": headers, "body": body } })
return id
@ -134,16 +135,17 @@ func create_request(url: String, method: HTTPClient.Method = HTTPClient.Method.M
## [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))
logger.verb("Starting request " + str(id))
logger.verb(core.stringify_variables("Starting request %id%", { "id": id }))
list_active.merge({ id: list_queue[id] })
list_queue.erase(id)
loggeri.diag("Creating new HTTPRequest \"Request #" + str(id) + "\"")
logger.diag("Creating new HTTPRequest \"Request #" + str(id) + "\"")
var download: HTTPRequest = HTTPRequest.new()
download.name = "Request #" + str(id)
download.accept_gzip = true
download.use_threads = true
var lambda: Callable = func(result: int, http_code: int, headers: PackedStringArray, body: PackedByteArray, httprequest: HTTPRequest) -> void:
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))
logger.verb(core.stringify_variables("Request %id% completed\nResult: %result%\nHTTP response code: %http_code%\nHeaders: %headers%\nBody size: %body_size_bytes% Bytes // %body_size_mib% MiB\nParsed body as UTF-8: %parse_utf8%", { "result": result, "http_code": http_code, "headers": headers.size(), "body_size_bytes": body.size(), "body_size_mib": core.misc.byte2mib(body.size(), true), "parse_utf8": parse_utf8 }, true))
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)
remove_child(httprequest)
@ -157,14 +159,14 @@ 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 + "'")
logger.error(core.stringify_variables("Blocked unsecure url %url%", { "url": url }))
return false
CoreTypes.BlockadeLevel.WARN: loggeri.warn("Requesting unsecure url '" + url + "'")
CoreTypes.BlockadeLevel.WARN: logger.warn(core.stringify_variables("Requesting unsecure url %url%", { "url": url }))
CoreTypes.BlockadeLevel.IGNORE: pass
_: await loggeri.crash("Invalid BlockadeLevel '" + str(config_unsecure_requests) + "'")
_: await logger.crash(core.stringify_variables("Invalid BlockadeLevel %level%", { "level": config_unsecure_requests }))
elif url.begins_with("https://"): pass
else:
loggeri.error("Invalid url '" + url + "'")
logger.error(core.stringify_variables("Invalid url %url%", { "url": url }))
return false
return true
@ -173,10 +175,10 @@ func is_request_completed(id: int) -> bool: return list_complete.has(id)
## Cleans the request queue.
func clean_queue() -> void:
loggeri.verb("Cleaning request queue")
logger.verb("Cleaning request queue")
list_queue.clear()
## Cleans the completed requests list.
func clean_completed() -> void:
loggeri.verb("Cleaning completed requests")
logger.verb("Cleaning completed requests")
list_complete.clear()

View file

@ -46,17 +46,17 @@ var config_newlines_sizelimit: int
func _cleanup() -> void:
for instance in instances:
if is_instance_valid(instance):
loggeri.diag("Removing instance '" + instance.name + "'")
logger.diag("Removing instance '" + instance.name + "'")
instance.queue_free()
func _schedule() -> void:
var instances_remove_enty: Array[CoreLoggerInstance] = []
for instance in instances:
instances_remove_enty.append(instance)
if !is_instance_valid(instance): continue
if !is_instance_valid(instance.parent):
loggeri.diag("Removing instance '" + instance.name + "'")
logger.diag("Removing instance '" + instance.name + "'")
instance.queue_free()
instances_remove_enty.append(instance)
for instance in instances_remove_enty:
instances.remove_at(instances.find(instance))

View file

@ -90,7 +90,7 @@ func _ready() -> void:
vsbar.add_theme_stylebox_override("grabber_pressed", StyleBoxEmpty.new())
# 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")
core.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")
# +++ process +++
func _process(_delta: float) -> void:

View file

@ -40,29 +40,29 @@ func _pull_config() -> void:
## If [code]flatten[/code] is set to [code]true[/code], the decimal part will be discarded.
@warning_ignore("integer_division")
func byte2mib(bytes: int, flatten: bool = true) -> float:
if flatten: return bytes/1048576
return bytes/float(1048576)
if flatten: return float(int(float(bytes)/1048576.0))
return float(bytes)/1048576.0
## 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:
if flatten: return int(mib*1048576)
return mib*1048576
if flatten: return float(int(mib*1048576.0))
return mib*1048576.0
## 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:
if flatten: return int(mib/1024)
return mib/1024
if flatten: return float(int(mib/1024.0))
return mib/1024.0
## 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:
if flatten: return int(gib*1024)
return gib*1024
if flatten: return float(int(gib*1024.0))
return gib*1024.0
# +++ type formatting +++
## Converts a string array into a normal, nicely formatted string.[br]
@ -85,10 +85,10 @@ func format_stringarray(array: Array[String], item_before: String = "", item_aft
var output: String = ""
if array.size() == 0:
loggeri.warn("Unable to format a string with a size of 0")
logger.warn("Unable to format a string with a size of 0")
return ""
elif array.size() == 1:
loggeri.warn("Unable to format a string with a size of 1")
logger.warn("Unable to format a string with a size of 1")
return array[0]
for item in array:
@ -107,7 +107,7 @@ func array_to_stringarray(array: Array) -> Array[String]:
for item in array:
if typeof(item) != TYPE_STRING:
logger.error("Cannot convert Array to Array[String]: Item '" + str(item) + "' is not of type String")
logger.error(core.stringify_string("Cannot convert Array to Array[String]: Item %item% is not of type String", { "item": item }))
return []
output.append(item)
@ -151,7 +151,7 @@ func get_center(parent_size: Vector2, child_size: Vector2) -> Vector2:
## Output:
## [16:44:35] [DIAG Test.gd] Triggered '"shoot"' (pos='x=5156.149 y=581.69', successful='true')
## [/codeblock]
func stringify_variables(template: String, variables: Dictionary, no_quotes: bool = false) -> String:
func stringify_variables(template: String, variables: Dictionary, no_quotes: bool = false, force_no_type: bool = false) -> String:
# To decrease allocations
var value
var type: String = ""
@ -286,7 +286,7 @@ func stringify_variables(template: String, variables: Dictionary, no_quotes: boo
type = "unknown"
# Replace
if config_stringify_show_type:
if config_stringify_show_type and !force_no_type:
if type != "": type = "(" + type.to_lower() + ") "
else:
type = ""
@ -294,7 +294,7 @@ func stringify_variables(template: String, variables: Dictionary, no_quotes: boo
template = template.replace("%" + placeholder + "%", quote + type + replacement + quote)
return template
# Makes calls shorter
# Makes internal calls shorter
func _sa(value) -> String:
return stringify_variables("%var%", { "var": value }, true)

View file

@ -49,19 +49,19 @@ func _cleanup() -> void:
func _pull_config() -> void:
if core.config.headless:
# Remove all scenes
if is_inside_tree(): loggeri.verb("Removing all scenes (triggered by headless mode)")
if is_inside_tree(): logger.verb("Removing all scenes (triggered by headless mode)")
for scene in scenes: remove_scene(scene, true)
# +++ scene management +++
## Adds a scene to some scene collection.
func add_scene(scene_name: String, scene_class: Node, scene_type: CoreTypes.SceneType) -> bool:
if core.config.headless: return false
loggeri.verb("Adding scene \"" + scene_name + "\" of type " + str(scene_type))
logger.verb(core.stringify_variables("Adding scene %name% of type %type%", { "name": scene_name, "type": scene_type }))
if exists(scene_name) != CoreTypes.SceneType.NONE:
loggeri.error("Scene with name \"" + scene_name + "\" already exists")
logger.error(core.stringify_variables("A scene named %name% already exists", { "name": scene_name }))
return false
if typeof(scene_class) != TYPE_OBJECT or !scene_class.is_class("Node"):
loggeri.error("Scene class \"" + scene_name + "\" is not of type Node")
logger.error(core.stringify_variables("Scene class %name% is not of type Node", { "name": scene_name }))
return false
scene_class.name = scene_name
match(scene_type):
@ -71,9 +71,9 @@ func add_scene(scene_name: String, scene_class: Node, scene_type: CoreTypes.Scen
CoreTypes.SceneType.MAIN: scenes_main.add_child(scene_class)
CoreTypes.SceneType.BACKGROUND: scenes_background.add_child(scene_class)
CoreTypes.SceneType.NONE:
loggeri.error("CoreTypes.SceneType.NONE is not a valid scene type")
logger.error("CoreTypes.SceneType.NONE is not a valid scene type")
return false
_: await loggeri.crash("Invalid SceneType " + str(scene_type))
_: await logger.crash(core.stringify_variables("Invalid SceneType %type%", { "type": scene_type }))
scenes.merge({ scene_name: { "type": scene_type, "class": scene_class } })
return true
@ -81,8 +81,8 @@ func add_scene(scene_name: String, scene_class: Node, scene_type: CoreTypes.Scen
## [b]Danger: [i]Don't set [code]force_remove[/code] to [code]true[/code], thanks![/i][/b]
func remove_scene(scene_name: String, force_remove: bool = false) -> bool:
if core.config.headless and !force_remove: return false
if force_remove: await loggeri.crash("force_remove = true is not allowed")
loggeri.verb("Removing scene \"" + scene_name + "\"")
if force_remove: await logger.crash("force_remove is not allowed to be true")
logger.verb(core.stringify_variables("Removing scene %name%", { "name": scene_name }))
match(exists(scene_name)):
CoreTypes.SceneType.DEBUG:
scenes_debug.remove_child(scenes[scene_name]["class"])
@ -100,9 +100,9 @@ func remove_scene(scene_name: String, force_remove: bool = false) -> bool:
scenes_background.remove_child(scenes[scene_name]["class"])
scenes[scene_name]["class"].queue_free()
CoreTypes.SceneType.NONE:
loggeri.error("Scene \"" + scene_name + "\" does not exist")
logger.error(core.stringify_variables("Scene %name% does not exist", { "name": scene_name }))
return false
_: await loggeri.crash("Invalid SceneType " + str(exists(scene_name)))
_: await logger.crash(core.stringify_variables("Invalid SceneType %type%", { "type": exists(scene_name) }))
scenes.erase(scene_name)
return true
@ -118,8 +118,8 @@ func get_scene(scene_name: String) -> Node:
CoreTypes.SceneType.MENU: return scenes[scene_name]["class"]
CoreTypes.SceneType.MAIN: return scenes[scene_name]["class"]
CoreTypes.SceneType.BACKGROUND: return scenes[scene_name]["class"]
CoreTypes.SceneType.NONE: loggeri.error("Scene \"" + scene_name + "\" does not exist")
_: await loggeri.crash("Invalid SceneType " + str(exists(scene_name)))
CoreTypes.SceneType.NONE: logger.error(core.stringify_variables("Scene %name% does not exist", { "name": scene_name }))
_: await logger.crash(core.stringify_variables("Invalid SceneType %type%", { "type": exists(scene_name) }))
return null
## Returns a scene collection node.[br]
@ -133,8 +133,8 @@ func get_scene_collection(scene_type: CoreTypes.SceneType) -> Node:
CoreTypes.SceneType.MENU: return scenes_menu
CoreTypes.SceneType.MAIN: return scenes_main
CoreTypes.SceneType.BACKGROUND: return scenes_background
CoreTypes.SceneType.NONE: loggeri.error("No scene collection was found for CoreTypes.SceneType.NONE")
_: await loggeri.crash("Invalid SceneType " + str(scene_type))
CoreTypes.SceneType.NONE: logger.error("No scene collection was found for CoreTypes.SceneType.NONE")
_: await logger.crash(core.stringify_variables("Invalid SceneType %type%", { "type": scene_type }))
return null
## Returns a list of all loaded scenes in some scene collection.

View file

@ -32,40 +32,40 @@ var storage_location: String = ""
## Opens a storage file and loads it into memory.
func open_storage(location: String, create_new: bool = true, sanity_check: bool = true, fail_on_sanity_check: bool = false) -> bool:
if is_open:
loggeri.error("Failed to open storage: A storage file is already open")
logger.error("Failed to open storage: A storage file is already open")
return false
loggeri.verb("Opening storage file at \"" + location + "\"")
logger.verb(core.stringify_variables("Opening storage file at %location%", { "location": location }))
var file: FileAccess
if !FileAccess.file_exists(location):
if create_new:
file = FileAccess.open(location, FileAccess.WRITE)
if file == null:
await loggeri.crash("Could not open storage file at \"" + location + "\": Failed with code " + str(FileAccess.get_open_error()))
await logger.crash(core.stringify_variables("Could not open storage file at %location%: Failed with error %error%", { "location": location, "error": error_string(FileAccess.get_open_error()) }))
return false
file.store_string("{}")
file.close()
else:
loggeri.error("Failed to open storage: create_new is set to false")
logger.error("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:
loggeri.error("Failed to open storage: Parsed storage file is of type " + str(typeof(storage_temp)))
logger.error(core.stringify_variables("Failed to open storage: Parsed storage file is of type %type%", { "type": type_string(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:
loggeri.error("Sanity check failed (stopping):")
logger.error("Sanity check failed (stopping):")
for error in check_result:
loggeri.error("-> " + error)
logger.error("-> " + error)
return false
else:
loggeri.warn("Sanity check failed (continuing anyway):")
logger.warn("Sanity check failed (continuing anyway):")
for error in check_result:
loggeri.warn("-> " + error)
logger.warn("-> " + error)
storage = storage_temp
storage_location = location
is_open = true
@ -74,9 +74,9 @@ func open_storage(location: String, create_new: bool = true, sanity_check: bool
## Closes the active storage file.
func close_storage() -> bool:
if !is_open:
loggeri.error("Failed to close storage: No storage file is open")
logger.error("Failed to close storage: No storage file is open")
return false
loggeri.verb("Closing storage file")
logger.verb("Closing storage file")
storage = {}
is_open = false
return true
@ -84,13 +84,13 @@ func close_storage() -> bool:
## Saves the active storage file to disk.
func save_storage() -> bool:
if !is_open:
loggeri.error("Failed to save storage: No storage file is open")
logger.error("Failed to save storage: No storage file is open")
return false
var file: FileAccess = FileAccess.open(storage_location, FileAccess.WRITE)
if file == null:
await loggeri.crash("Could not open storage file at \"" + storage_location + "\": Failed with code " + str(FileAccess.get_open_error()))
await logger.crash(core.stringify_variables("Could not open storage file at %location%: Failed with error %error%", { "location": storage_location, "error": error_string(FileAccess.get_open_error()) }))
return false
loggeri.diag("Writing storage file to disk")
logger.diag("Writing storage file to disk")
file.store_string(JSON.stringify(storage))
file.close()
return true
@ -99,9 +99,9 @@ func save_storage() -> bool:
## Removes all keys from the active storage file. The nuclear option basically.
func nuke_storage(autosave: bool = true) -> bool:
if !is_open:
loggeri.error("Failed to nuke storage: No storage file is open")
logger.error("Failed to nuke storage: No storage file is open")
return false
loggeri.warn("Nuking storage")
logger.warn("Nuking storage")
storage = {}
if autosave: save_storage()
return true
@ -109,17 +109,17 @@ func nuke_storage(autosave: bool = true) -> bool:
## Returns a storage key. Can also return a default value if unset.
func get_key(key: String, default: Variant = null) -> Variant:
if !is_open:
loggeri.error("Failed to get key: No storage file is open")
logger.error("Failed to get key: No storage file is open")
return NAN
loggeri.diag("Returning storage key \"" + key + "\" (default='" + str(default) + "')")
logger.diag(core.stringify_variables("Returning storage key %key% (default=%default%)", { "key": key, "default": 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:
if !is_open:
loggeri.error("Failed to set key: No storage file is open")
logger.error("Failed to set key: No storage file is open")
return false
loggeri.diag("Updating storage key \"" + key + "\" with value '" + str(value) + "' (overwrite='" + str(overwrite) + "' autosave='" + str(autosave) + "'")
logger.diag(core.stringify_variables("Updating storage key %key% with value %value% (overwrite=%overwrite% autosave=%autosave%)", { "key": key, "value": value, "overwrite": overwrite, "autosave": autosave }))
storage.merge({key: value}, overwrite)
if autosave: save_storage()
return true
@ -129,7 +129,7 @@ func del_key(key: String, autosave: bool = true) -> bool:
if !is_open:
logger.errof("storage", "Failed to delete key: No storage file is open")
return false
loggeri.diag("Deleting storage key \"" + key + "\" (autosave='" + str(autosave) + "')")
logger.diag(core.stringify_variables("Deleting storage key %key% (autosave=%autosave%)", { "key": key, "autosave": autosave }))
storage.erase(key)
if autosave: save_storage()
return true
@ -139,30 +139,30 @@ func del_key(key: String, autosave: bool = true) -> bool:
## pass your modified [class Dictionary to [method safe_dict].
func get_dict() -> Dictionary:
if !is_open:
loggeri.error("Failed to get dictionary: No storage file is open")
logger.error("Failed to get dictionary: No storage file is open")
return {}
loggeri.verb("Returning storage dictionary")
logger.verb("Returning storage dictionary")
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:
if !is_open:
loggeri.error("Failed to save dictionary: No storage file is open")
logger.error("Failed to save dictionary: No storage file is open")
return false
loggeri.verb("Saving custom dictionary as storage")
logger.verb("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:
loggeri.error("Sanity check failed (stopping):")
logger.error("Sanity check failed (stopping):")
for error in check_result:
loggeri.error("-> " + error)
logger.error("-> " + error)
return false
else:
loggeri.warn("Sanity check failed (continuing anyway):")
logger.warn("Sanity check failed (continuing anyway):")
for error in check_result:
loggeri.warn("-> " + error)
logger.warn("-> " + error)
storage = dict
if autosave: save_storage()
return true
@ -170,14 +170,14 @@ func save_dict(dict: Dictionary, sanity_check: bool = true, fail_on_sanity_check
# +++ 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]:
loggeri.verb("Performing a sanity check on some storage dictionary")
logger.verb("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)) + "')")
errors.append(core.stringify_variables("Key %key% is not of type String (is of type %type%)", { "key": key, "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)) + "')")
loggeri.verb("Completed sanity check with " + str(errors.size()) + " errors")
errors.append(core.stringify_variables("The value of %key% is not null, a String, an int, a float, boolean, an Array or a Dictionary (is of type %type%)", { "key": key, "type": type_string(typeof(key)) }))
logger.verb(core.stringify_variables("Completed sanity check with %errors% errors", { "errors": errors.size() }))
return errors

70
src/validation.gd Normal file
View file

@ -0,0 +1,70 @@
# 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 <https://www.gnu.org/licenses/>.
## Allows for data validation.
extends CoreBaseModule
var schemas: Array[CoreValidationSchema]
var singles: Array[CoreValidationSingle]
# +++ module +++
func _cleanup() -> void:
# Singles
var singles_remove_enty: Array[CoreLoggerInstance] = []
for single in singles:
singles_remove_enty.append(single)
if !is_instance_valid(single): continue
if !is_instance_valid(single.parent):
logger.diag("Removing single '" + single.name + "'")
single.queue_free()
for single in singles_remove_enty:
singles.remove_at(singles.find(single))
# Schemas
var schemas_remove_enty: Array[CoreLoggerInstance] = []
for schema in schemas:
singles_remove_enty.append(schema)
if !is_instance_valid(schema): continue
if !is_instance_valid(schema.parent):
logger.diag("Removing schema '" + schema.name + "'")
schema.queue_free()
for schema in schemas_remove_enty:
schemas.remove_at(schemas.find(schema))
func _schedule() -> void:
# Singles
for single in singles:
if is_instance_valid(single):
logger.diag("Removing single '" + single.name + "'")
single.queue_free()
# Schemas
for schema in schemas:
if is_instance_valid(schema):
logger.diag("Removing schema '" + schema.name + "'")
schema.queue_free()
# +++ data validation +++
## Returns a new [CoreValidationSingle]
func get_single(data, parent: Node) -> CoreValidationSingle:
singles.append(parent)
return CoreValidationSingle.new(core, data, parent)
## Returns a new [CoreValidationSchema]
func get_schema(schema: Dictionary, parent: Node) -> CoreValidationSchema:
schemas.append(parent)
return CoreValidationSchema.new(core, schema, parent)