2
0
Fork 0

Update to v2-release0

This commit is contained in:
JeremyStar™ 2024-05-17 01:17:00 +02:00
parent 9d46a0ec05
commit c885f654f4
Signed by: JeremyStarTM
GPG key ID: E366BAEF67E4704D
13 changed files with 1054 additions and 208 deletions

View file

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

View file

@ -19,6 +19,8 @@ class_name CoreConfiguration
@export var custom_modules: bool @export var custom_modules: bool
## If [method Core.quit_safely] (and by extension [method Core.cleanup]) should be called when pressing the X. ## If [method Core.quit_safely] (and by extension [method Core.cleanup]) should be called when pressing the X.
@export var automatic_shutdown: bool @export var automatic_shutdown: bool
## Hides the window during engine shutdown. Useful when your game and the framework will take longer to cleanup.
@export var hide_window_on_shutdown: bool
@export_category("Logger") @export_category("Logger")
## The minimum log level you want to be displayed. ## The minimum log level you want to be displayed.
@ -26,8 +28,7 @@ class_name CoreConfiguration
## Determines if the logger's output should be colored. ## Determines if the logger's output should be colored.
@export var logger_colored: bool @export var logger_colored: bool
## Determines if the logger should check if running in verbose mode (see [method OS.is_stdout_verbose]).[br] ## Determines if the logger should check if running in verbose mode (see [method OS.is_stdout_verbose]).[br]
## Comes with a huge performance penalty on startup, delaying startup by about one to two seconds.[br] ## Update [code]verbose_mode[/code] yourself accordingly if you've set this to [code]false[/code], or face messed up diagnostic log messages.[br]
## Update [code]verbose_mode[/code] accordingly yourself if you've disabled this, or diagnostic log messages might appear messed up.[br]
## [b]Warning: [i]Updating this during runtime does nothing.[/i][/b] ## [b]Warning: [i]Updating this during runtime does nothing.[/i][/b]
@export var logger_detect_verbose_mode: bool @export var logger_detect_verbose_mode: bool
## The template for all log messages.[br] ## The template for all log messages.[br]
@ -45,6 +46,20 @@ class_name CoreConfiguration
@export var logui_background_color: Color @export var logui_background_color: Color
## What font 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
## How many lines can be visible on the screen at any given time. [b]Never[/b] set this higher than 100 or it will cause tremendous amounts of lag for no real benefit. Instead [i]lowering[/i] that amount helps performance.
@export var logui_max_lines: int
@export_category("Miscellaneous")
## Shows or hides the type when calling [code]stringify_variables[/code].
@export var misc_stringify_show_type: bool
## Determines how [code]stringify_variables[/code] should display [class Color] variables.[br]
## Will display colors from [code]0[/code] to [code]255[/code] if [code]true[/code] or from [code]-1.0[/code] to [code]1.0[/code] if [code]false[/code].
@export var misc_stringify_color_range8: bool
## Determines if [class Array]s should be processed by [code]stringify_variables[/code].
@export var misc_stringify_array: bool
## Determines if [class Dictionary]s should be processed by [code]stringify_variables[/code].
@export var misc_stringify_dictionary: bool
@export_category("Easy Request Maker") @export_category("Easy Request Maker")
## Determines how unsecure requests should be handled. ## Determines how unsecure requests should be handled.
@export var erm_unsecure_requests: CoreTypes.BlockadeLevel @export var erm_unsecure_requests: CoreTypes.BlockadeLevel
@ -54,18 +69,29 @@ func _init() -> void:
# Global # Global
headless = false headless = false
development = false development = false
custom_modules = false custom_modules = true
automatic_shutdown = true automatic_shutdown = true
hide_window_on_shutdown = true
# Logger # Logger
logger_level = CoreTypes.LoggerLevel.INFO logger_level = CoreTypes.LoggerLevel.INFO
logger_colored = true logger_colored = true
logger_detect_verbose_mode = true
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
# Log UI # 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
logui_max_lines = 100
# Misc
misc_stringify_show_type = false
misc_stringify_color_range8 = true
misc_stringify_array = true
misc_stringify_dictionary = true
# Easy Request Maker # Easy Request Maker
erm_unsecure_requests = CoreTypes.BlockadeLevel.BLOCK erm_unsecure_requests = CoreTypes.BlockadeLevel.BLOCK

View file

@ -23,9 +23,11 @@ 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, following the StarOpenSource Logging Specification (SOSLS) version 1.
enum LoggerLevel { NONE, ERROR, WARN, INFO, VERB, DIAG } enum LoggerLevel { NONE, SPECIAL, 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. ## To what degree [i]something[/i] should be blocked.
enum BlockadeLevel { IGNORE, WARN, BLOCK } 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,91 @@
# 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 a Dictionary against multiple [CoreValidationSingle]s.
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 = {}
## Contains the data to validate.[br]
## [b]Note: [i]Don't modify.[/i][/b]
var data: Dictionary = {}
func _init(core_new: Core, schema_new: Dictionary, data_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
data = data_new
# Check Dictionary
_check_dictionary_recursive(schema)
func _check_dictionary_recursive(schema_parent: Dictionary, path: String = "") -> bool:
var success: bool = false
for key in schema_parent:
if typeof(key) != TYPE_STRING:
logger.error(core.stringify_variables("Could not parse schema: Schema key %key% is not of type String", { "key": path + "/" + key }))
success = false
continue
match(typeof(schema_parent[key])):
TYPE_OBJECT:
if schema_parent[key].get_class() != "Node":
logger.error(core.stringify_variables("Could not parse schema: Schema value of %key% is not of type Node", { "key": path + "/" + key }))
success = false
continue
TYPE_DICTIONARY:
_check_dictionary_recursive(schema_parent[key], path + "/" + key)
_:
logger.error(core.stringify_variables("Could not parse schema: Schema value of %key% is not of type CoreValidationSingle or Dictionary", { "key": path + "/" + key }))
success = false
continue
return success
func evaluate() -> Array[String]:
return _evaluate_recursive(str(randf()).repeat(50), schema, data)
func _evaluate_recursive(random: String, schema_parent: Dictionary, data_parent: Dictionary, path: String = "") -> Array[String]:
var failed: Array[String] = []
for key in schema_parent:
# Check if key exists in data
if str(data_parent.get(key, random)) == random:
# Does not exist, append error
failed.append(core.stringify_variables("Key %key% is present in schema but missing in data", { "key": path + "/" + key }))
else:
# Exists in data
if typeof(schema_parent[key]) == TYPE_DICTIONARY:
# Key is of type Dictionary, allow for recursion to happen
failed.append_array(_evaluate_recursive(random, schema_parent[key], data_parent[key], path + "/" + key))
else:
# Key is not of type Dictionary, evaluate against data
schema_parent[key].data = data_parent[key]
if !schema_parent[key].evaluate():
logger.error(core.stringify_variables("Validation for key %key% failed", { "key": path + "/" + key }))
for failure in schema_parent[key].failures:
# Append failures from single
failed.append(path + "/" + key + ": " + failure)
return failed

View file

@ -0,0 +1,317 @@
# 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 and 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
# Set 'minimum_matches' to the amount of values if set to '-1'
if rule["minimum_matches"] == -1: rule["minimum_matches"] = rule["values"].size()
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 and 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 (StringName) is empty")
Variant.Type.TYPE_INT: if data == 0: failures.append("Data (int) is zero")
Variant.Type.TYPE_FLOAT: if data == 0.0: failures.append("Data (float) is zero")
Variant.Type.TYPE_ARRAY: if data == []: failures.append("Data (Array) is empty")
Variant.Type.TYPE_DICTIONARY: if data == {}: failures.append("Data (Dictionary) is empty")
# If not supported, skip
_: logger.warn("Can't determine if data is null as data is not of type String, StringName, int, float, Array or Dictionary")
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) -> 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]> (or all if set to [code]-1[/code]) values.[br]
## Applies to [String] & [StringName].
func contains(values: Array, minimum_matches: int = 1) -> CoreValidationSingle:
rules.append({ "type": CoreTypes.ValidationType.CONTAINS, "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]), [float] ([code]!= 0.0[/code]), [Array] ([code]!= [][/code]) and [Dictionary] ([code]!= {}[/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

@ -24,15 +24,17 @@ class_name Core
# Versioning # Versioning
## The version number ## The version number
const version_version: int = 1 const version_version: int = 2
## The version type. See [enum CoreTypes.VersionType] for more information. ## The version type. See [enum CoreTypes.VersionType] for more information.
const version_type: CoreTypes.VersionType = CoreTypes.VersionType.RELEASE const version_type: CoreTypes.VersionType = CoreTypes.VersionType.RELEASE
## 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 = 1 const version_typerelease: int = 0
## The fork indicator. Update this if you intend on soft or hard forking this framework.
const version_fork: String = ""
# Modules # Modules
## Used internally for loading, managing and unloading 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] ## CORE's configuration object.[br]
## [b]NEVER access this yourself! To change the configuration use [method reload_configuration] instead.[/b] ## [b]NEVER access this yourself! To change the configuration use [method reload_configuration] instead.[/b]
var config: CoreConfiguration var config: CoreConfiguration
@ -52,60 +54,88 @@ var logui: CoreBaseModule
var erm: CoreBaseModule var erm: CoreBaseModule
## The 'Storage' module ## The 'Storage' module
var storage: CoreBaseModule var storage: CoreBaseModule
## The 'Data Validation' module
var validation: CoreBaseModule
# /etc/ # /etc/
## Stores the path to CORE's installation directory.[br] ## Stores the path to CORE's installation directory.[br]
## [b]Danger: [i]Don't modify this.[/i][/b] ## [b]Danger: [i]Don't modify this.[/i][/b]
var basepath: String var basepath: String
## Contains a list of all loaded custom modules.[br] # Contains a list of all registered cleanup hooks.
## [b]Danger: [i]Don't modify this.[/i][/b] var cleanup_hooks: Dictionary = {}
## Internal, don't modify.
# Contains a list of all loaded custom modules.
var custom_modules: Dictionary = {} var custom_modules: Dictionary = {}
## Contains the node holding all custom modules as children.[br] ## Internal, don't modify.
## [b]Danger: [i]Don't modify this.[/i][/b] # Contains the node holding all custom modules as children.
var custom_modules_node: Node var custom_modules_node: Node
## The CORE Object's logger instance. ## Internal, don't modify.
## [b]Danger: [i]Don't modify this.[/i][/b] # The CORE Object's logger instance.
var loggeri: CoreLoggerInstance var loggeri: CoreLoggerInstance
## Internal, don't modify.
# Makes CORE inaccessible if true.
var disabled: bool = false
## Internal, don't modify.
# Displays the ✨ special ✨ welcome message if true
var welcomed: bool = false
## Contains the amount of time it took to preinitialize the framework, measured in milliseconds.[br]
## Captured in [method _init].[br]
## [b]Danger: [i]Don't modify this.[/i][/b]
var initduration_preinitialization: int = 0
## Contains the amount of time it took to initialize the framework, measured in milliseconds.[br]
## Captured in [method _ready].[br]
## [b]Danger: [i]Don't modify this.[/i][/b]
var initduration_initialization: int = 0
## Contains the amount of time it took to completely initialize the framework, measured in milliseconds.[br]
## Captured in [method complete_init].[br]
## [b]Danger: [i]Don't modify this.[/i][/b]
var initduration_complete_initialization: int = 0
# +++ initialization +++ # +++ initialization +++
## Handles the preinitialization part. Does stuff like checking the engine version, loading the config and loading all modules into memory. # Preinitialization
func _init(new_config: CoreConfiguration = CoreConfiguration.new()) -> void: func _init(new_config: CoreConfiguration = CoreConfiguration.new()) -> void:
var inittime: int = Time.get_ticks_msec()
name = "CORE" name = "CORE"
if !check_godot_version(): return if !check_godot_version(): return
if !determine_basepath(): queue_free() if !determine_basepath():
get_tree().quit(70)
while true: await get_tree().create_timer(9999).timeout
custom_modules_node = Node.new() custom_modules_node = Node.new()
reload_configuration(new_config) reload_configuration(new_config)
initialize_modules() initialize_modules()
apply_configuration() apply_configuration()
initialize_scheduler() initialize_scheduler()
initduration_preinitialization = Time.get_ticks_msec() - inittime
## Handles the initialization part. Injects the builtin modules into the SceneTree and makes sure custom modules can be loaded properly.[br] # Initialization
## [b]Danger: [i]Don't call this.[/i][/b]
func _ready() -> void: func _ready() -> void:
var inittime: int = Time.get_ticks_msec()
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", self) loggeri = logger.get_instance(basepath.replace("res://", "") + "src/core.gd", self)
add_child(scheduler) add_child(scheduler)
get_tree().auto_accept_quit = false get_tree().auto_accept_quit = false
initduration_initialization = Time.get_ticks_msec() - inittime
## Initializes all built-in modules during the preinitialization phase.[br] # Initializes all built-in modules during the preinitialization phase.
## [b]Danger: [i]Don't call this.[/i][/b] ## Internal, don't call.
func initialize_modules() -> void: func initialize_modules() -> void:
for module in modules: for module in modules:
set(module, CoreBaseModule.new()) set(module, CoreBaseModule.new())
get(module).name = module get(module).name = module
get(module).set_script(load(basepath + "src/" + module + ".gd")) get(module).set_script(load(basepath + "src/" + module + ".gd"))
get(module).core = self 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() get(module)._initialize()
## Injects CORE's builtin modules into the SceneTree.[br] # Injects CORE's builtin modules into the SceneTree.
## [b]Danger: [i]Don't call this.[/i][/b] ## Internal, don't call.
func inject_modules() -> void: for module in modules: add_child(get(module)) func inject_modules() -> void: for module in modules: add_child(get(module))
## Initializes the framework scheduler. # Initializes the framework scheduler.
## [b]Danger: [i]Don't call this.[/i][/b] ## Internal, don't call.
func initialize_scheduler() -> void: func initialize_scheduler() -> void:
scheduler = Timer.new() scheduler = Timer.new()
scheduler.name = "Scheduler" scheduler.name = "Scheduler"
@ -116,7 +146,9 @@ func initialize_scheduler() -> void:
scheduler.process_mode = Node.PROCESS_MODE_ALWAYS scheduler.process_mode = Node.PROCESS_MODE_ALWAYS
scheduler.connect("timeout", func() -> void: scheduler.connect("timeout", func() -> void:
loggeri.verb("Running scheduler tasks") loggeri.verb("Running scheduler tasks")
for module in modules: await get(module)._schedule() var modules_reverse: Array[String] = modules.duplicate()
modules_reverse.reverse()
for module in modules_reverse: await get(module)._schedule()
for module in custom_modules_node.get_children(): await module._schedule() for module in custom_modules_node.get_children(): await module._schedule()
) )
@ -124,9 +156,10 @@ func initialize_scheduler() -> void:
## [br] ## [br]
## This ensures that all modules are fully initialized and ready for usage.[br] ## This ensures that all modules are fully initialized and ready for usage.[br]
## [i][b]Not calling this function during startup may lead to runtime issues.[/b][/i] ## [i][b]Not calling this function during startup may lead to runtime issues.[/b][/i]
func complete_init(no_success_message: bool = false) -> void: func complete_init() -> void:
var modsinit_builtin: Array[String] = ["workaround"] var inittime: int = Time.get_ticks_msec()
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: while modsinit_builtin.size() != 0 and modsinit_custom.size() != 0:
# Clear arrays # Clear arrays
@ -149,22 +182,45 @@ func complete_init(no_success_message: bool = false) -> void:
# Initialization complete # Initialization complete
await get_tree().process_frame await get_tree().process_frame
if !no_success_message: loggeri.info("Initialized CORE successfully") if !welcomed:
welcomed = true
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% ______
__ ____/_ __ \\__ __ \\__ ____/ ___ ____/____________ _______ ___________ _________________ /__
_ / _ / / /_ /_/ /_ __/ __ /_ __ ___/ __ `/_ __ `__ \\ _ \\_ | /| / / __ \\_ ___/_ //_/
/ /___ / /_/ /_ _, _/_ /___ _ __/ _ / / /_/ /_ / / / / / __/_ |/ |/ // /_/ / / _ ,<
\\____/ \\____/ /_/ |_| /_____/ /_/ /_/ \\__,_/ /_/ /_/ /_/\\___/____/|__/ \\____//_/ /_/|_|
Copyright (c) 2023-2024 The StarOpenSource Project & Contributors.
Licensed under the GNU Affero General Public License v3 WITHOUT ANY WARRANTY.
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!""".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 +++ # +++ configuration +++
## Loads a (new) configuration object 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: loggeri.verb("Reloading CORE's configuration") if initialized: loggeri.verb("Reloading CORE's configuration")
if config != null: config.queue_free() if is_instance_valid(config): config.free()
config = new_config.duplicate() config = new_config.duplicate()
if is_devmode(): # Override configuration in development mode if is_devmode(): # Override configuration in development mode
config.logger_level = CoreTypes.LoggerLevel.DIAG config.logger_level = CoreTypes.LoggerLevel.DIAG
if initialized: loggeri.verb("Overrode configuration (development mode)") if initialized: loggeri.verb("Overrode configuration (development mode)")
if initialized: apply_configuration() if initialized: apply_configuration()
## Applies the a configuration.[br] # Applies a new configuration.
## [b]Danger: [i]Don't call this.[/i][/b] ## Internal, don't call.
func apply_configuration() -> void: func apply_configuration() -> void:
if loggeri != null: loggeri.verb("Applying configuration") if loggeri != null: loggeri.verb("Applying configuration")
if is_devmode() and loggeri != null: loggeri.warn("The CORE Framework is in development mode. Here be dragons!") if is_devmode() and loggeri != null: loggeri.warn("The CORE Framework is in development mode. Here be dragons!")
@ -177,7 +233,7 @@ func apply_configuration() -> void:
custom_modules[module]._pull_config() custom_modules[module]._pull_config()
# Workaround # Workaround
logger.verbose_mode = OS.is_stdout_verbose() if config.logger_detect_verbose_mode: logger.verbose_mode = OS.is_stdout_verbose()
# +++ custom module support +++ # +++ custom module support +++
## Registers a new custom module. ## Registers a new custom module.
@ -187,13 +243,13 @@ func register_custom_module(module_name: String, module_origin: String, module_c
loggeri.error("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):
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 return false
loggeri.diag("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) module_class.logger = logger.get_instance(module_origin, module_class)
module_class.loggeri.framework = true module_class.logger.framework = true
loggeri.diag("Adding module to SceneTree") loggeri.diag("Adding module to SceneTree")
custom_modules_node.add_child(module_class) custom_modules_node.add_child(module_class)
loggeri.diag("Merging module with custom_modules") loggeri.diag("Merging module with custom_modules")
@ -208,11 +264,11 @@ func register_custom_module(module_name: String, module_origin: String, module_c
func unregister_custom_module(module_name: String) -> void: func unregister_custom_module(module_name: String) -> void:
loggeri.verb("Unregistering custom module \"" + module_name + "\"") loggeri.verb("Unregistering custom module \"" + module_name + "\"")
if !custom_modules.has(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 return
var module: Node = get_custom_module(module_name) var module: Node = custom_modules[module_name]
await module._cleanup() await module._cleanup()
module.loggeri.queue_free() module.logger.queue_free()
custom_modules_node.remove_child(module) custom_modules_node.remove_child(module)
custom_modules.erase(module_name) custom_modules.erase(module_name)
module.queue_free() module.queue_free()
@ -222,27 +278,88 @@ func unregister_custom_module(module_name: String) -> void:
func get_custom_module(module_name: String) -> CoreBaseModule: func get_custom_module(module_name: String) -> CoreBaseModule:
loggeri.diag("Getting custom module \"" + module_name + "\"") loggeri.diag("Getting custom module \"" + module_name + "\"")
if !custom_modules.has(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 null
return custom_modules[module_name] return custom_modules[module_name]
# +++ etc ++ # +++ cleanup ++
## Makes sure that CORE does not leak memory on shutdown/unload.[br] ## 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. ## Unloads all custom modules, built-in modules, frees any of CORE's classes and lastly itself.[br]
## Only call this function if you're sure that your application or game no longer uses the CORE Framework.
func cleanup() -> void: func cleanup() -> void:
loggeri.info("Cleaning up") loggeri.info("Cleaning up")
for module in custom_modules_node.get_children(): unregister_custom_module(module.name) loggeri.verb("Calling cleanup hooks")
for hook in cleanup_hooks:
if !cleanup_hooks[hook].is_valid():
loggeri.error(stringify_variables("Cleanup hook %id% is invalid", { "id": hook }))
else:
loggeri.diag("Calling cleanup hook #" + str(hook))
await cleanup_hooks[hook].call()
await get_tree().process_frame
loggeri.verb("Unregistering custom modules")
for module in custom_modules_node.get_children(): await unregister_custom_module(module.name)
await get_tree().process_frame
loggeri.verb("Removing custom module support")
remove_child(custom_modules_node) remove_child(custom_modules_node)
custom_modules_node.queue_free() custom_modules_node.queue_free()
await get_tree().process_frame
loggeri.verb("Unloading built-in modules")
var modules_reverse: Array[String] = modules.duplicate() var modules_reverse: Array[String] = modules.duplicate()
modules_reverse.reverse() modules_reverse.reverse()
for module in modules_reverse: for module in modules_reverse:
await get(module)._cleanup() await get(module)._cleanup()
get(module).loggeri.queue_free()
get(module).queue_free() get(module).queue_free()
config.queue_free() await get_tree().process_frame
print("Freeing configuration")
config.free()
print("Freeing")
queue_free() queue_free()
# Generates a new cleanup hook id
## Internal, don't call.
func _generate_hook_id() -> int:
var id = randi()
if cleanup_hooks.has(id):
loggeri.warn(stringify_variables("New cleanup hook id %id% is already taken", { "id": id }))
return _generate_hook_id()
elif id == -1:
loggeri.warn(stringify_variables("Invalid cleanup hook id %id%", { "id": id }))
return _generate_hook_id()
return id
## Registers a new cleanup hook.[br]
## Returns the hook id.
func register_cleanup_hook(callable: Callable) -> int:
if !callable.is_valid():
loggeri.error("Could not add cleanup hook: Callable is not valid")
return -1
var id: int = _generate_hook_id()
loggeri.verb("Adding new cleanup hook #" + str(id))
cleanup_hooks.merge({ id: callable })
return id
## Unregisters a cleanup hook by it's id.
func unregister_cleanup_hook_by_id(id: int) -> bool:
if !cleanup_hooks.has(id):
loggeri.error(stringify_variables("Could not remove cleanup hook (id): Hook %id% does not exist", { "id": id }))
return false
loggeri.verb(stringify_variables("Removed cleanup hook %id%", { "id": id }))
cleanup_hooks.erase(id)
return true
## Unregisters a cleanup hook by it's reference.
func unregister_cleanup_hook_by_ref(callable: Callable) -> bool:
var id: Variant = cleanup_hooks.find_key(callable)
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(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 +++
## Returns if the framework is in development mode. ## Returns if the framework is in development mode.
func is_devmode() -> bool: func is_devmode() -> bool:
return config.development return config.development
@ -261,6 +378,7 @@ 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_typerelease%", str(version_typerelease)) 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() 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):
@ -299,8 +417,7 @@ 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.[br] # Determines CORE's installation/base path.
## [b]Danger: [i]Don't call this.[/i][/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,18 +426,17 @@ func determine_basepath() -> bool:
elif FileAccess.file_exists("res://addons/CORE/.corebasepath"): elif FileAccess.file_exists("res://addons/CORE/.corebasepath"):
basepath = "res://addons/CORE/" basepath = "res://addons/CORE/"
else: else:
assert(false, "CORE is not located at 'res://CORE/', aborting initialization") printerr("CORE is not located at 'res://CORE/' or 'res://addons/CORE', aborting initialization.")
return false return false
return true return true
# Checks Godot's version
## Checks compatibility with the running version. ## Checks compatibility with the running version.
func check_godot_version() -> bool: func check_godot_version() -> bool:
var version: Dictionary = Engine.get_version_info() var version: Dictionary = Engine.get_version_info()
match(version["major"]): match(version["major"]):
4: pass 4: pass
_: _:
printerr("The CORE Framework does not support Godot versions older or newer than 4.x.x") printerr("The CORE Framework does not support Godot versions older or newer than 4.x.x.")
return false return false
match(version["minor"]): match(version["minor"]):
0: printerr("The CORE Framework does not support Godot versions older than 4.2.x. Please update to Godot 4.2.x to ensure full compatibility.") 0: printerr("The CORE Framework does not support Godot versions older than 4.2.x. Please update to Godot 4.2.x to ensure full compatibility.")
@ -330,7 +446,7 @@ func check_godot_version() -> bool:
printerr("The CORE Framework does not support Godot versions newer than 4.2.x. Please downgrade to Godot 4.2.x.") printerr("The CORE Framework does not support Godot versions newer than 4.2.x. Please downgrade to Godot 4.2.x.")
return false return false
if version["status"] != "stable": if version["status"] != "stable":
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 4.2.x.stable to ensure full compatibility.")
return false return false
return true return true
@ -339,12 +455,35 @@ func check_godot_version() -> bool:
## [b]Note: [i]Using the [code]await[/code] keyword is required for this function.[/i][/b] ## [b]Note: [i]Using the [code]await[/code] keyword is required for this function.[/i][/b]
func quit_safely(exitcode: int = 0) -> void: func quit_safely(exitcode: int = 0) -> void:
loggeri.info("Shutting down (code " + str(exitcode) + ")") loggeri.info("Shutting down (code " + str(exitcode) + ")")
if config.hide_window_on_shutdown:
loggeri.verb("Hiding window")
Engine.max_fps = -1 # a higher framerate seems to make the shutdown process muuuuch faster
DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_ENABLED) # we don't want to cook the cpu tho
DisplayServer.window_set_exclusive(0, false)
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_MINIMIZED)
DisplayServer.window_set_min_size(Vector2.ZERO)
DisplayServer.window_set_size(Vector2i.ZERO)
DisplayServer.window_set_max_size(Vector2.ZERO)
DisplayServer.window_set_position(Vector2i(9999999, 9999999))
DisplayServer.window_set_flag(DisplayServer.WINDOW_FLAG_BORDERLESS, true)
DisplayServer.window_set_flag(DisplayServer.WINDOW_FLAG_ALWAYS_ON_TOP, false)
DisplayServer.window_set_flag(DisplayServer.WINDOW_FLAG_MOUSE_PASSTHROUGH, false)
DisplayServer.window_set_flag(DisplayServer.WINDOW_FLAG_NO_FOCUS, true)
DisplayServer.window_set_flag(DisplayServer.WINDOW_FLAG_RESIZE_DISABLED, true)
await get_tree().create_timer(0.25).timeout await get_tree().create_timer(0.25).timeout
await cleanup() await cleanup()
get_tree().quit(exitcode) get_tree().quit(exitcode)
## 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: func _notification(what) -> void:
match(what): match(what):
NOTIFICATION_WM_CLOSE_REQUEST: NOTIFICATION_WM_CLOSE_REQUEST:
if config.automatic_shutdown: if config.automatic_shutdown:
loggeri.diag("Got close request, shutting down")
await quit_safely(0) await quit_safely(0)

View file

@ -18,14 +18,11 @@
## 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] # Contains a list of all queued downloads.
## [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] # Contains a list of all active downloads.
## [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] # Contains a liust of all completed downloads.
## [b]Danger: [i]Don't modify this[/i][/b].
var list_complete: Dictionary = {} var list_complete: Dictionary = {}
## Determines how unsecure requests should be handled. ## Determines how unsecure requests should be handled.
@ -57,11 +54,12 @@ func _cleanup() -> void:
## [/codeblock] ## [/codeblock]
## [b]Note: [i]Using the [code]await[/code] keyword is required for this function.[/i][/b] ## [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:
loggeri.verb("Creating awaited request") logger.verb("Creating awaited request")
if !await is_url_allowed(url): return {} if !await is_url_allowed(url): return {}
var id: int = create_request(url, method, headers, data) var id: int = create_request(url, method, headers, data)
start_request(id, parse_utf8) 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 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)
@ -96,7 +94,7 @@ func oneline_awaited_request(url: String, return_utf8: bool = true, ignore_http_
## [/codeblock] ## [/codeblock]
## [b]Note: [i]Using the [code]await[/code] keyword is required for this function.[/i][/b] ## [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]:
loggeri.verb("Creating " + str(urls.size()) + " awaited request(s)") logger.verb("Creating " + str(urls.size()) + " awaited request(s)")
var dldata: Array[Dictionary] = [] var dldata: Array[Dictionary] = []
for url in urls: for url in urls:
if !await is_url_allowed(url): continue if !await is_url_allowed(url): continue
@ -107,29 +105,28 @@ func batch_awaited_request(urls: PackedStringArray, parse_utf8: bool, method: HT
list_complete.erase(id) list_complete.erase(id)
return dldata return dldata
## Internal function, do not call # Does the work, but in a thread.
func _batch_awaited_request(url: String, parse_utf8: bool, method: HTTPClient.Method = HTTPClient.Method.METHOD_GET, headers: PackedStringArray = PackedStringArray([]), data: String = "") -> int: 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) var id: int = create_request(url, method, headers, data)
start_request(id, parse_utf8) 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 while !is_request_completed(id): await get_tree().create_timer(0.1, true).timeout
return id return id
# +++ internal +++ # +++ internal +++
## Returns a new download id.[br] # Returns a new download id.
## [b]Danger: [i]Don't call this.[/i][/b]
func generate_id() -> int: func generate_id() -> int:
var id = randi() var id = randi()
if list_queue.has(id) or list_active.has(id): 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() 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 return id
## Creates a new request and stores it in the queue. Returns the download id.[br] ## 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] ## [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: 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() 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
@ -138,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]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] ## [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: 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_active.merge({ id: list_queue[id] })
list_queue.erase(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() 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:
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_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)
@ -161,14 +159,14 @@ func is_url_allowed(url: String) -> bool:
if url.begins_with("http://"): if url.begins_with("http://"):
match(config_unsecure_requests): match(config_unsecure_requests):
CoreTypes.BlockadeLevel.BLOCK: CoreTypes.BlockadeLevel.BLOCK:
loggeri.error("Blocked unsecure url '" + url + "'") logger.error(core.stringify_variables("Blocked unsecure url %url%", { "url": url }))
return false 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 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 elif url.begins_with("https://"): pass
else: else:
loggeri.error("Invalid url '" + url + "'") logger.error(core.stringify_variables("Invalid url %url%", { "url": url }))
return false return false
return true return true
@ -177,10 +175,10 @@ func is_request_completed(id: int) -> bool: return list_complete.has(id)
## Cleans the request queue. ## Cleans the request queue.
func clean_queue() -> void: func clean_queue() -> void:
loggeri.verb("Cleaning request queue") logger.verb("Cleaning request queue")
list_queue.clear() list_queue.clear()
## Cleans the completed requests list. ## Cleans the completed requests list.
func clean_completed() -> void: func clean_completed() -> void:
loggeri.verb("Cleaning completed requests") logger.verb("Cleaning completed requests")
list_complete.clear() list_complete.clear()

View file

@ -23,9 +23,7 @@ extends CoreBaseModule
signal log_event signal log_event
## Keeps track of all logger instances. # Keeps track of all logger instances. Unused instances will be cleaned periodically by CORE's scheduler.
## Unused instances will be cleaned periodically.
## [b]Danger: [i]Don't modify this.[/i][/b]
var instances: Array[CoreLoggerInstance] = [] var instances: Array[CoreLoggerInstance] = []
## Used to determine if running in verbose/command line mode.[br] ## Used to determine if running in verbose/command line mode.[br]
@ -46,16 +44,23 @@ var config_newlines_sizelimit: int
# +++ module +++ # +++ module +++
func _cleanup() -> void: func _cleanup() -> void:
_schedule() for instance in instances:
await get_tree().process_frame if is_instance_valid(instance):
logger.diag("Removing instance '" + instance.name + "'")
instance.queue_free()
func _schedule() -> void: func _schedule() -> void:
var instances_remove_enty: Array[CoreLoggerInstance] = []
for instance in instances: for instance in instances:
if !is_instance_valid(instance): continue if !is_instance_valid(instance): continue
if !is_instance_valid(instance.parent): if !is_instance_valid(instance.parent):
loggeri.diag("Removing instance '" + instance.name + "'") logger.diag("Removing instance '" + instance.name + "'")
instance.queue_free() instance.queue_free()
instances.remove_at(instances.find(instance)) instances_remove_enty.append(instance)
for instance in instances_remove_enty:
var index: int = instances.find(instance)
if index == -1: logger.error("Invalid index -1")
else: instances.remove_at(index)
func _pull_config() -> void: func _pull_config() -> void:
config_level = core.config.logger_level config_level = core.config.logger_level
@ -65,8 +70,7 @@ func _pull_config() -> void:
config_newlines_sizelimit = core.config.logger_newlines_sizelimit config_newlines_sizelimit = core.config.logger_newlines_sizelimit
# +++ logging +++ # +++ logging +++
## The main logging function that does the heavy lifting.[br] # The main logging function that does the heavy lifting.
## [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, "")
@ -99,6 +103,10 @@ func _log(level: CoreTypes.LoggerLevel, origin: String, message: String) -> void
format = format.replace("%level%", "ERR!") format = format.replace("%level%", "ERR!")
format_newline = format_newline.replace("%level%", "ERR!") format_newline = format_newline.replace("%level%", "ERR!")
format = format.replace("%color%", "[color=red]") format = format.replace("%color%", "[color=red]")
CoreTypes.LoggerLevel.SPECIAL:
format = format.replace("%level%", "UWU!")
format_newline = format_newline.replace("%level%", "HI:3")
format = format.replace("%color%", "[color=purple]")
CoreTypes.LoggerLevel.NONE: CoreTypes.LoggerLevel.NONE:
format = format.replace("%level%", "CRSH") format = format.replace("%level%", "CRSH")
format_newline = format_newline.replace("%level%", "CRSH") format_newline = format_newline.replace("%level%", "CRSH")

View file

@ -28,13 +28,22 @@ var logrtl: RichTextLabel
var font_normal: Font var font_normal: Font
var font_bold: Font var font_bold: Font
# Configuration
var config_max_lines: int
# +++ module +++ # +++ module +++
func _pull_config() -> void: func _pull_config() -> void:
background.visible = !core.config.headless and core.config.logui_enabled if !core.config.logui_enabled:
background.visible = false
logrtl.text = ""
else:
background.visible = !core.config.headless
background.color = core.config.logui_background_color 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("normal_font_size", core.config.logui_font_size)
logrtl.add_theme_font_size_override("bold_font_size", core.config.logui_font_size) logrtl.add_theme_font_size_override("bold_font_size", core.config.logui_font_size)
config_max_lines = core.config.logui_max_lines
func _cleanup() -> void: func _cleanup() -> void:
background.remove_child(logrtl) background.remove_child(logrtl)
core.sms.remove_child(background) core.sms.remove_child(background)
@ -90,7 +99,9 @@ func _ready() -> void:
vsbar.add_theme_stylebox_override("grabber_pressed", StyleBoxEmpty.new()) vsbar.add_theme_stylebox_override("grabber_pressed", StyleBoxEmpty.new())
# 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") core.logger.connect("log_event", func(allowed: bool, _level: CoreTypes.LoggerLevel, _origin: String, _message: String, format: String) -> void:
if allowed and core.config.logui_enabled: logrtl.text = logrtl.text + format + "\n"
)
# +++ process +++ # +++ process +++
func _process(_delta: float) -> void: func _process(_delta: float) -> void:
@ -98,3 +109,17 @@ func _process(_delta: float) -> void:
var window_size: Vector2i = DisplayServer.window_get_size() var window_size: Vector2i = DisplayServer.window_get_size()
background.size = window_size background.size = window_size
logrtl.size = window_size logrtl.size = window_size
var stripped_text: String = ""
if logrtl.text.count("\n") > config_max_lines:
var lines: PackedStringArray = logrtl.text.split("\n")
var index: int = 0
for line in lines:
if index >= lines.size()-config_max_lines:
if stripped_text == "": stripped_text = line
else: stripped_text += "\n" + line
index += 1
logrtl.text = stripped_text

View file

@ -21,35 +21,48 @@
## and generally make your life as a developer easier. ## and generally make your life as a developer easier.
extends CoreBaseModule extends CoreBaseModule
# Configuration
var config_stringify_show_type: bool
var config_stringify_color_range8: bool
var config_stringify_array: bool
var config_stringify_dictionary: bool
# +++ module +++
func _pull_config() -> void:
config_stringify_show_type = core.config.misc_stringify_show_type
config_stringify_color_range8 = core.config.misc_stringify_color_range8
config_stringify_array = core.config.misc_stringify_array
config_stringify_dictionary = core.config.misc_stringify_dictionary
# +++ data type conversion +++ # +++ data type conversion +++
## Converts a number of bytes into mebibytes.[br] ## Converts a number of bytes into mebibytes.[br]
## [br] ## [br]
## If [code]flatten[/code] is set to [code]true[/code], the decimal part will be discarded. ## If [code]flatten[/code] is set to [code]true[/code], the decimal part will be discarded.
@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 float(int(float(bytes)/1048576.0))
return bytes/float(1048576) return float(bytes)/1048576.0
## Converts a number of mebibytes into bytes.[br] ## Converts a number of mebibytes into bytes.[br]
## [br] ## [br]
## If [code]flatten[/code] is set to [code]true[/code], the decimal part will be discarded. ## 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 float(int(mib*1048576.0))
return mib*1048576 return mib*1048576.0
## Converts a number of mebibytes into gibibytes.[br] ## Converts a number of mebibytes into gibibytes.[br]
## [br] ## [br]
## If [code]flatten[/code] is set to [code]true[/code], the decimal part will be discarded. ## 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 float(int(mib/1024.0))
return mib/1024 return mib/1024.0
## Converts a number of gebibytes into mebibytes.[br] ## Converts a number of gebibytes into mebibytes.[br]
## [br] ## [br]
## If [code]flatten[/code] is set to [code]true[/code], the decimal part will be discarded. ## 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 float(int(gib*1024.0))
return gib*1024 return gib*1024.0
# +++ type formatting +++ # +++ type formatting +++
## Converts a string array into a normal, nicely formatted string.[br] ## Converts a string array into a normal, nicely formatted string.[br]
@ -72,11 +85,11 @@ func format_stringarray(array: Array[String], item_before: String = "", item_aft
var output: String = "" var output: String = ""
if array.size() == 0: 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 "" return ""
elif array.size() == 1: 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] return item_before + array[0] + item_after
for item in array: for item in array:
if output == "": output = item_before + item + item_after if output == "": output = item_before + item + item_after
@ -94,7 +107,7 @@ func array_to_stringarray(array: Array) -> Array[String]:
for item in array: for item in array:
if typeof(item) != TYPE_STRING: 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 [] return []
output.append(item) output.append(item)
@ -125,6 +138,166 @@ func stringarray_to_array(array: Array[String]) -> Array:
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)
## Makes variables as look correct inside strings.[br]
## Short examples:[br]
## [code]true[/code] -> [code]'true'[/code][br]
## [code]Vector2(69.064, PI)[/code] -> [code]'x=69.064 y=3.14159265358979'[/code][br]
## [code]"This is a test string"[/code] -> [code]'"This is a test string"'[/code][br]
## Full example:[br]
## [codeblock]
## Code:
## logger.diag(stringify_variables("Triggered %trigger% (pos=%position%, successful=%success%)", { "trigger": "shoot", "position": Vector2(5156.149, 581.69), "success": true }))
##
## 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, force_no_type: bool = false) -> String:
# To decrease allocations
var value
var type: String = ""
var replacement: String = ""
for placeholder in variables:
# Check key type
if typeof(placeholder) != TYPE_STRING:
logger.error("Invalid placeholder type '\"" + type_string(typeof(placeholder)) + "\", skipping")
continue
# Check for correct type
value = variables[placeholder]
match(typeof(value)):
# Primitives
Variant.Type.TYPE_NIL:
replacement = "null"
type = ""
Variant.Type.TYPE_BOOL:
replacement = str(value)
type = "bool"
Variant.Type.TYPE_INT:
replacement = str(value)
type = "int"
Variant.Type.TYPE_FLOAT:
replacement = str(value)
type = "float"
Variant.Type.TYPE_STRING:
replacement = "\"" + value + "\""
type = "String"
Variant.Type.TYPE_STRING_NAME:
replacement = "\"" + value + "\""
type = "StringName"
# Non-primitives
Variant.Type.TYPE_OBJECT:
replacement = str(value)
type = "Object"
Variant.Type.TYPE_COLOR:
if config_stringify_color_range8: replacement = "r=" + _sa(value.r8) + " g=" + _sa(value.g8) + " b=" + _sa(value.b8) + " a=" + _sa(value.a8)
else: replacement = "r=" + _sa(value.r) + " g=" + _sa(value.g) + " b=" + _sa(value.b) + " a=" + _sa(value.a)
type = "Color"
Variant.Type.TYPE_RID:
replacement = "id=" + _sa(value.get_id()) + " valid=" + _sa(value.is_valid())
type = "RID"
Variant.Type.TYPE_ARRAY:
if config_stringify_array:
if value.size() == 0:
replacement = "[]"
else:
replacement = "[ "
for item in value:
if replacement == "[ ": replacement += _sa(item)
else: replacement += ", " + _sa(item)
replacement += " ]"
else: replacement = str(value)
if value.get_typed_builtin() != TYPE_NIL:
type = "Array[" + type_string(typeof(value.get_typed_builtin())) + "]"
else:
type = "Array"
Variant.Type.TYPE_DICTIONARY:
if config_stringify_dictionary:
if value.size() == 0: replacement = "{}"
else:
replacement = "{ "
for key in value:
if replacement == "{ ": replacement += _sa(key) + ": " + _sa(value[key])
else: replacement += ", " + _sa(key)
replacement += " }"
else: replacement = str(value)
type = "Dictionary"
# TODO: Packed Arrays
# Nodes & scripting
Variant.Type.TYPE_NODE_PATH:
replacement = str(value)
type = "NodePath"
Variant.Type.TYPE_CALLABLE:
replacement = "valid=" + _sa(value.is_valid()) + " standard=" + _sa(value.is_standard()) + " object=" + _sa(value.get_object() ) + " method=" + value.get_method() + " args=" + _sa(value.get_bound_arguments())
type = "Callable"
Variant.Type.TYPE_SIGNAL:
replacement = "name=" + _sa(value.get_name()) + " object=" + _sa(value.get_object())
type = "Signal"
# 2D
Variant.Type.TYPE_VECTOR2:
replacement = "x=" + _sa(value.x) + " y=" + _sa(value.y)
type = "Vector2"
Variant.Type.TYPE_VECTOR2I:
replacement = "x=" + _sa(value.x) + " y=" + _sa(value.y)
type = "Vector2i"
Variant.Type.TYPE_RECT2:
replacement = "size=" + _sa(value.size) + " pos=" + _sa(value.position) + " end=" + _sa(value.end)
type = "Rect2"
Variant.Type.TYPE_RECT2I:
replacement = "size=" + _sa(value.size) + " pos=" + _sa(value.position) + " end=" + _sa(value.end)
type = "Rect2i"
Variant.Type.TYPE_TRANSFORM2D:
replacement = "x=" + _sa(value.x) + " y=" + _sa(value.y) + " origin=" + _sa(value.origin)
type = "Transform2D"
# 3D
Variant.Type.TYPE_VECTOR3:
replacement = "x=" + _sa(value.x) + " y=" + _sa(value.y) + " z=" + _sa(value.z)
type = "Vector3"
Variant.Type.TYPE_VECTOR3I:
replacement = "x=" + _sa(value.x) + " y=" + _sa(value.y) + " z=" + _sa(value.z)
type = "Vector3i"
Variant.Type.TYPE_PLANE:
replacement = "x=" + _sa(value.x) + " y=" + _sa(value.y) + " z=" + _sa(value.z) + " d=" + _sa(value.d) + " normal=" + _sa(value.normal)
type = "Plane"
Variant.Type.TYPE_QUATERNION:
replacement = "x=" + _sa(value.x) + " y=" + _sa(value.y) + " z=" + _sa(value.z) + " w=" + _sa(value.w)
type = "Quaternion"
Variant.Type.TYPE_AABB:
replacement = "size=" + _sa(value.size) + " pos=" + _sa(value.position) + " end=" + _sa(value.end)
type = "AABB"
Variant.Type.TYPE_TRANSFORM3D:
replacement = "basis=" + _sa(value.basis) + " origin=" + _sa(value.origin)
type = "Transform3D"
Variant.Type.TYPE_BASIS:
replacement = "x=" + _sa(value.x) + " y=" + _sa(value.y) + " z=" + _sa(value.z)
type = "Basis"
Variant.Type.TYPE_PROJECTION:
replacement = "x=" + _sa(value.x) + " y=" + _sa(value.y) + " z=" + _sa(value.z) + " w=" + _sa(value.w)
type = "Projection"
# 4D
Variant.Type.TYPE_VECTOR4:
replacement = "x=" + _sa(value.x) + " y=" + _sa(value.y) + " z=" + _sa(value.z) + " w=" + _sa(value.w)
type = "Vector4"
Variant.Type.TYPE_VECTOR4I:
replacement = "x=" + _sa(value.x) + " y=" + _sa(value.y) + " z=" + _sa(value.z) + " w=" + _sa(value.w)
type = "Vector4i"
_:
replacement = str(value)
type = "unknown"
# Replace
if config_stringify_show_type and !force_no_type:
if type != "": type = "(" + type.to_lower() + ") "
else:
type = ""
var quote: String = "'" if !no_quotes else ""
template = template.replace("%" + placeholder + "%", quote + type + replacement + quote)
return template
# Makes internal calls shorter
func _sa(value) -> String:
return stringify_variables("%var%", { "var": value }, true)
## Moved to [method Core.quit_safely]. ## Moved to [method Core.quit_safely].
## @deprecated ## @deprecated
func quit_safely(exitcode: int = 0) -> void: await core.quit_safely(exitcode) func quit_safely(exitcode: int = 0) -> void: await core.quit_safely(exitcode)

View file

@ -22,24 +22,14 @@ extends CoreBaseModule
## Used internally for adding, managing and removing scene collections. ## 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] # Scene collections
## [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()
## A list of all loaded scenes[br] # A list of all loaded scenes.
## [b]Danger: [i]Don't modify this.[/i][/b]
var scenes: Dictionary = {} var scenes: Dictionary = {}
# +++ module +++ # +++ module +++
@ -59,114 +49,114 @@ 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(): 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) for scene in scenes: remove_scene(scene, true)
# +++ scene management +++ # +++ scene management +++
## Adds a scene to some scene collection. ## Adds a scene to some scene collection.
func add_scene(sname: String, sclass: Node, type: CoreTypes.SceneType) -> bool: func add_scene(scene_name: String, scene_class: Node, scene_type: CoreTypes.SceneType) -> bool:
if core.config.headless: return false if core.config.headless: return false
loggeri.verb("Adding scene \"" + sname + "\" of type " + str(type)) logger.verb(core.stringify_variables("Adding scene %name% of type %type%", { "name": scene_name, "type": scene_type }))
if exists(sname) != CoreTypes.SceneType.NONE: if exists(scene_name) != CoreTypes.SceneType.NONE:
loggeri.error("Scene with name \"" + sname + "\" already exists") logger.error(core.stringify_variables("A scene named %name% already exists", { "name": scene_name }))
return false return false
if typeof(sclass) != TYPE_OBJECT or !sclass.is_class("Node"): if typeof(scene_class) != TYPE_OBJECT or !scene_class.is_class("Node"):
loggeri.error("Scene class \"" + sname + "\" is not of type Node") logger.error(core.stringify_variables("Scene class %name% is not of type Node", { "name": scene_name }))
return false return false
sclass.name = sname scene_class.name = scene_name
match(type): match(scene_type):
CoreTypes.SceneType.DEBUG: scenes_debug.add_child(sclass) CoreTypes.SceneType.DEBUG: scenes_debug.add_child(scene_class)
CoreTypes.SceneType.CUTSCENE: scenes_cutscene.add_child(sclass) CoreTypes.SceneType.CUTSCENE: scenes_cutscene.add_child(scene_class)
CoreTypes.SceneType.MENU: scenes_menu.add_child(sclass) CoreTypes.SceneType.MENU: scenes_menu.add_child(scene_class)
CoreTypes.SceneType.MAIN: scenes_main.add_child(sclass) CoreTypes.SceneType.MAIN: scenes_main.add_child(scene_class)
CoreTypes.SceneType.BACKGROUND: scenes_background.add_child(sclass) CoreTypes.SceneType.BACKGROUND: scenes_background.add_child(scene_class)
CoreTypes.SceneType.NONE: 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 return false
_: await loggeri.crash("Invalid SceneType " + str(type)) _: await logger.crash(core.stringify_variables("Invalid SceneType %type%", { "type": scene_type }))
scenes.merge({ sname: { "type": type, "class": sclass } }) scenes.merge({ scene_name: { "type": scene_type, "class": scene_class } })
return true return true
## Removes a scene from some scene collection.[br] ## 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] ## [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(scene_name: 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 loggeri.crash("force_remove = true is not allowed") if force_remove: await logger.crash("force_remove is not allowed to be true")
loggeri.verb("Removing scene \"" + sname + "\"") logger.verb(core.stringify_variables("Removing scene %name%", { "name": scene_name }))
match(exists(sname)): match(exists(scene_name)):
CoreTypes.SceneType.DEBUG: CoreTypes.SceneType.DEBUG:
scenes_debug.remove_child(scenes[sname]["class"]) scenes_debug.remove_child(scenes[scene_name]["class"])
scenes[sname]["class"].queue_free() scenes[scene_name]["class"].queue_free()
CoreTypes.SceneType.CUTSCENE: CoreTypes.SceneType.CUTSCENE:
scenes_cutscene.remove_child(scenes[sname]["class"]) scenes_cutscene.remove_child(scenes[scene_name]["class"])
scenes[sname]["class"].queue_free() scenes[scene_name]["class"].queue_free()
CoreTypes.SceneType.MENU: CoreTypes.SceneType.MENU:
scenes_menu.remove_child(scenes[sname]["class"]) scenes_menu.remove_child(scenes[scene_name]["class"])
scenes[sname]["class"].queue_free() scenes[scene_name]["class"].queue_free()
CoreTypes.SceneType.MAIN: CoreTypes.SceneType.MAIN:
scenes_main.remove_child(scenes[sname]["class"]) scenes_main.remove_child(scenes[scene_name]["class"])
scenes[sname]["class"].queue_free() scenes[scene_name]["class"].queue_free()
CoreTypes.SceneType.BACKGROUND: CoreTypes.SceneType.BACKGROUND:
scenes_background.remove_child(scenes[sname]["class"]) scenes_background.remove_child(scenes[scene_name]["class"])
scenes[sname]["class"].queue_free() scenes[scene_name]["class"].queue_free()
CoreTypes.SceneType.NONE: CoreTypes.SceneType.NONE:
loggeri.error("Scene \"" + sname + "\" does not exist") logger.error(core.stringify_variables("Scene %name% does not exist", { "name": scene_name }))
return false return false
_: await loggeri.crash("Invalid SceneType " + str(exists(sname))) _: await logger.crash(core.stringify_variables("Invalid SceneType %type%", { "type": exists(scene_name) }))
scenes.erase(sname) scenes.erase(scene_name)
return true return true
# +++ getters +++ # +++ getters +++
## Returns a scene from some scene collection.[br] ## Returns a scene from some scene collection.[br]
## [br] ## [br]
## Returns [code]null[/code] if no scene with that name was found. ## Returns [code]null[/code] if no scene with that name was found.
func get_scene(sname: String) -> Node: func get_scene(scene_name: String) -> Node:
if core.config.headless: return null if core.config.headless: return null
match(exists(sname)): match(exists(scene_name)):
CoreTypes.SceneType.DEBUG: return scenes[sname]["class"] CoreTypes.SceneType.DEBUG: return scenes[scene_name]["class"]
CoreTypes.SceneType.CUTSCENE: return scenes[sname]["class"] CoreTypes.SceneType.CUTSCENE: return scenes[scene_name]["class"]
CoreTypes.SceneType.MENU: return scenes[sname]["class"] CoreTypes.SceneType.MENU: return scenes[scene_name]["class"]
CoreTypes.SceneType.MAIN: return scenes[sname]["class"] CoreTypes.SceneType.MAIN: return scenes[scene_name]["class"]
CoreTypes.SceneType.BACKGROUND: return scenes[sname]["class"] CoreTypes.SceneType.BACKGROUND: return scenes[scene_name]["class"]
CoreTypes.SceneType.NONE: loggeri.error("Scene \"" + sname + "\" does not exist") CoreTypes.SceneType.NONE: logger.error(core.stringify_variables("Scene %name% does not exist", { "name": scene_name }))
_: await loggeri.crash("Invalid SceneType " + str(exists(sname))) _: await logger.crash(core.stringify_variables("Invalid SceneType %type%", { "type": exists(scene_name) }))
return null return null
## Returns a scene collection node.[br] ## Returns a scene collection node.[br]
## Useful if you want to change a child's index.[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] ## [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(scene_type: CoreTypes.SceneType) -> Node:
if core.config.headless: return null if core.config.headless: return null
match(type): match(scene_type):
CoreTypes.SceneType.DEBUG: return scenes_debug CoreTypes.SceneType.DEBUG: return scenes_debug
CoreTypes.SceneType.CUTSCENE: return scenes_cutscene CoreTypes.SceneType.CUTSCENE: return scenes_cutscene
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: loggeri.error("No scene collection was found for CoreTypes.SceneType.NONE") CoreTypes.SceneType.NONE: logger.error("No scene collection was found for CoreTypes.SceneType.NONE")
_: await loggeri.crash("Invalid SceneType " + str(type)) _: await logger.crash(core.stringify_variables("Invalid SceneType %type%", { "type": scene_type }))
return null return null
## Returns a list of all loaded 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(scene_type: CoreTypes.SceneType) -> Array[Node]:
var list: Array[Node] = [] var list: Array[Node] = []
for scene in scenes: for scene in scenes:
if scenes[scene]["type"] == type: if scenes[scene]["type"] == scene_type:
list.append(scenes[scene]["class"]) list.append(scenes[scene]["class"])
return list return list
## Returns the number of loaded scenes 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(scene_type: CoreTypes.SceneType) -> int:
var amount: int = 0 var amount: int = 0
for scene in scenes: for scene in scenes:
if scenes[scene]["type"] == type: if scenes[scene]["type"] == scene_type:
amount += 1 amount += 1
return amount return amount
## Returns the scene collection a scene is loaded in.[br] ## Returns the scene collection a scene is loaded in.[br]
## [br] ## [br]
## [enum CoreTypes.SceneType][code].NONE[/code] if no scene with that name was found. ## [enum CoreTypes.SceneType][code].NONE[/code] if no scene with that name was found.
func exists(sname: String) -> CoreTypes.SceneType: func exists(scene_name: String) -> CoreTypes.SceneType:
for scene in scenes: for scene in scenes:
if scene == sname: return scenes[scene]["type"] if scene == scene_name: return scenes[scene]["type"]
return CoreTypes.SceneType.NONE return CoreTypes.SceneType.NONE

View file

@ -23,51 +23,49 @@ extends CoreBaseModule
## Indicates if a storage file is currently open.[br] ## Indicates if a storage file is currently open.[br]
## [b]Danger: [i]Don't modify this.[/i][/b] ## [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] # The parsed data inside the storage file.
## [b]Danger: [i]Don't modify this.[/i][/b]
var storage: Dictionary = {} var storage: Dictionary = {}
## The location of the storage file.[br] # The location of the storage file.
## [b]Danger: [i]Don't modify this.[/i][/b]
var storage_location: String = "" var storage_location: String = ""
# +++ file management +++ # +++ file management +++
## Opens a storage file into memory. ## 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: 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:
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 return false
loggeri.verb("Opening storage file at \"" + location + "\"") logger.verb(core.stringify_variables("Opening storage file at %location%", { "location": 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 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 return false
file.store_string("{}") file.store_string("{}")
file.close() file.close()
else: 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 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:
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 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:
loggeri.error("Sanity check failed (stopping):") logger.error("Sanity check failed (stopping):")
for error in check_result: for error in check_result:
loggeri.error("-> " + error) logger.error("-> " + error)
return false return false
else: else:
loggeri.warn("Sanity check failed (continuing anyway):") logger.warn("Sanity check failed (continuing anyway):")
for error in check_result: for error in check_result:
loggeri.warn("-> " + error) logger.warn("-> " + error)
storage = storage_temp storage = storage_temp
storage_location = location storage_location = location
is_open = true is_open = true
@ -76,9 +74,9 @@ func open_storage(location: String, create_new: bool = true, sanity_check: bool
## Closes the active storage file. ## Closes the active storage file.
func close_storage() -> bool: func close_storage() -> bool:
if !is_open: 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 return false
loggeri.verb("Closing storage file") logger.verb("Closing storage file")
storage = {} storage = {}
is_open = false is_open = false
return true return true
@ -86,13 +84,13 @@ func close_storage() -> bool:
## Saves the active storage file to disk. ## Saves the active storage file to disk.
func save_storage() -> bool: func save_storage() -> bool:
if !is_open: 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 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 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 return false
loggeri.diag("Writing storage file to disk") logger.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
@ -101,9 +99,9 @@ func save_storage() -> bool:
## Removes all keys from the active storage file. The nuclear option basically. ## 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:
loggeri.error("Failed to nuke storage: No storage file is open") logger.error("Failed to nuke storage: No storage file is open")
return false return false
loggeri.warn("Nuking storage") logger.warn("Nuking storage")
storage = {} storage = {}
if autosave: save_storage() if autosave: save_storage()
return true return true
@ -111,17 +109,17 @@ func nuke_storage(autosave: bool = true) -> bool:
## Returns a storage key. Can also return a default value if unset. ## 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:
loggeri.error("Failed to get key: No storage file is open") logger.error("Failed to get key: No storage file is open")
return NAN 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) return storage.get(key, default)
## Updates a storage key with the specified value. ## 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:
loggeri.error("Failed to set key: No storage file is open") logger.error("Failed to set key: No storage file is open")
return false 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) storage.merge({key: value}, overwrite)
if autosave: save_storage() if autosave: save_storage()
return true return true
@ -131,7 +129,7 @@ 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
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) storage.erase(key)
if autosave: save_storage() if autosave: save_storage()
return true return true
@ -141,30 +139,30 @@ func del_key(key: String, autosave: bool = true) -> bool:
## pass your modified [class Dictionary to [method safe_dict]. ## pass your modified [class Dictionary to [method safe_dict].
func get_dict() -> Dictionary: func get_dict() -> Dictionary:
if !is_open: 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 {} return {}
loggeri.verb("Returning storage dictionary") logger.verb("Returning storage dictionary")
return storage return storage
# +++ raw manipulation +++ # +++ 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]). ## 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:
loggeri.error("Failed to save dictionary: No storage file is open") logger.error("Failed to save dictionary: No storage file is open")
return false return false
loggeri.verb("Saving custom dictionary as storage") logger.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:
loggeri.error("Sanity check failed (stopping):") logger.error("Sanity check failed (stopping):")
for error in check_result: for error in check_result:
loggeri.error("-> " + error) logger.error("-> " + error)
return false return false
else: else:
loggeri.warn("Sanity check failed (continuing anyway):") logger.warn("Sanity check failed (continuing anyway):")
for error in check_result: for error in check_result:
loggeri.warn("-> " + error) logger.warn("-> " + error)
storage = dict storage = dict
if autosave: save_storage() if autosave: save_storage()
return true return true
@ -172,14 +170,14 @@ func save_dict(dict: Dictionary, sanity_check: bool = true, fail_on_sanity_check
# +++ etc +++ # +++ etc +++
## Performs sanity checks on a [class Dictionary] to determine if it can be saved and loaded using this module. ## 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]:
loggeri.verb("Performing a sanity check on some storage dictionary") logger.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:
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 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: 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(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)) }))
loggeri.verb("Completed sanity check with " + str(errors.size()) + " errors") logger.verb(core.stringify_variables("Completed sanity check with %errors% errors", { "errors": errors.size() }))
return errors return errors

81
src/validation.gd Normal file
View file

@ -0,0 +1,81 @@
# 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:
# Schemas
var schemas_remove_enty: Array[CoreValidationSchema] = []
for schema in schemas:
schemas_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))
# Singles
var singles_remove_enty: Array[CoreValidationSingle] = []
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))
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:
var single: CoreValidationSingle = CoreValidationSingle.new(core, data, parent)
single.name = core.stringify_variables("CoreValidationSingle -> %parent% (type=%type% bytes=%bytes%)", { "parent": parent, "type": type_string(typeof(data)), "bytes": var_to_bytes_with_objects(data).size() })
singles.append(single)
return single
## Returns a new [CoreValidationSingle] for use in [CoreValidationSchema]s.
func get_single_schema(parent: Node) -> CoreValidationSingle:
var single: CoreValidationSingle = CoreValidationSingle.new(core, null, parent)
single.name = core.stringify_variables("CoreValidationSingle -> %parent% (no data, for schema)", { "parent": parent })
singles.append(single)
return single
## Returns a new [CoreValidationSchema].
func get_schema(schema_new: Dictionary, data_new: Dictionary, parent: Node) -> CoreValidationSchema:
var schema: CoreValidationSchema = CoreValidationSchema.new(core, schema_new, data_new, parent)
schema.name = core.stringify_variables("CoreValidationSchema -> %parent% (keys_schema=%keys_schema% keys_data=%keys_data% bytes_schema=%bytes_schema% bytes_data=%bytes_data%)", { "parent": parent, "keys_schema": schema_new.size(), "keys_data": data_new.size(), "bytes_schema": var_to_bytes_with_objects(schema_new).size(), "bytes_data": var_to_bytes_with_objects(data_new).size() })
schemas.append(schema)
return schema