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.
var core: Core
## Set to CORE's logger implementation.
@onready var logger: CoreBaseModule = core.logger
## Set to a [class CoreLoggerInstance] with the path you supplied to [method Core.register_custom_module]. You should use this over [code]logger[/code].
var loggeri: CoreLoggerInstance
## Set to a [class CoreLoggerInstance] with the path you supplied to [method Core.register_custom_module].
var logger: CoreLoggerInstance
## Marks a module as fully initialized and ready. **Don't forget to set this!**
var initialized: bool = false

View file

@ -19,6 +19,8 @@ class_name CoreConfiguration
@export var custom_modules: bool
## If [method Core.quit_safely] (and by extension [method Core.cleanup]) should be called when pressing the X.
@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")
## 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.
@export var logger_colored: bool
## 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] accordingly yourself if you've disabled this, or diagnostic log messages might appear messed up.[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]
## [b]Warning: [i]Updating this during runtime does nothing.[/i][/b]
@export var logger_detect_verbose_mode: bool
## The template for all log messages.[br]
@ -45,6 +46,20 @@ class_name CoreConfiguration
@export var logui_background_color: Color
## What font size the graphical log should have.
@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")
## Determines how unsecure requests should be handled.
@export var erm_unsecure_requests: CoreTypes.BlockadeLevel
@ -54,18 +69,29 @@ func _init() -> void:
# Global
headless = false
development = false
custom_modules = false
custom_modules = true
automatic_shutdown = true
hide_window_on_shutdown = true
# Logger
logger_level = CoreTypes.LoggerLevel.INFO
logger_colored = true
logger_detect_verbose_mode = true
logger_format = "%color%[%time%] [%level% %origin%] %message%"
logger_newlines_override = true
logger_newlines_sizelimit = 40
# Log UI
logui_enabled = true
logui_background_color = Color.BLACK # To disable the background, use Color.TRANSPARENT
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
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.
enum VersionType { RELEASE, RELEASECANDIDATE, BETA, ALPHA }
## Available log levels, followingthe StarOpenSource Logging Specification (SOSLS) version 1.
enum LoggerLevel { NONE, ERROR, WARN, INFO, VERB, DIAG }
## Available log levels, following the StarOpenSource Logging Specification (SOSLS) version 1.
enum LoggerLevel { NONE, SPECIAL, ERROR, WARN, INFO, VERB, DIAG }
## Available scene types.
enum SceneType { NONE, DEBUG, CUTSCENE, MENU, MAIN, BACKGROUND }
## To what degree [i]something[/i] should be blocked.
enum BlockadeLevel { IGNORE, WARN, BLOCK }
## All validation rules some data can be checked against.
enum ValidationType { MATCHES_TYPE, MATCHES_CLASS, IN_RANGE, HAS_MINIMUM, HAS_MAXIMUM, HAS_VALUES, CONTAINS, MATCHES_REGEX, IS_NOT_EMPTY, IS_NOT_NULL, IS_NORMALIZED, IS_ORTHONORMALIZED }

View file

@ -0,0 +1,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
## The version number
const version_version: int = 1
const version_version: int = 2
## The version type. See [enum CoreTypes.VersionType] for more information.
const version_type: CoreTypes.VersionType = CoreTypes.VersionType.RELEASE
## The version type number. Resets on every new version and version type.
const version_typerelease: int = 1
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
## Used internally for loading, managing and unloading modules.
const modules: Array[String] = [ "logger", "misc", "sms", "logui", "erm", "storage" ]
const modules: Array[String] = [ "logger", "misc", "sms", "logui", "erm", "storage", "validation" ]
## CORE's configuration object.[br]
## [b]NEVER access this yourself! To change the configuration use [method reload_configuration] instead.[/b]
var config: CoreConfiguration
@ -52,60 +54,88 @@ var logui: CoreBaseModule
var erm: CoreBaseModule
## The 'Storage' module
var storage: CoreBaseModule
## The 'Data Validation' module
var validation: CoreBaseModule
# /etc/
## Stores the path to CORE's installation directory.[br]
## [b]Danger: [i]Don't modify this.[/i][/b]
var basepath: String
## Contains a list of all loaded custom modules.[br]
## [b]Danger: [i]Don't modify this.[/i][/b]
# Contains a list of all registered cleanup hooks.
var cleanup_hooks: Dictionary = {}
## Internal, don't modify.
# Contains a list of all loaded custom modules.
var custom_modules: Dictionary = {}
## Contains the node holding all custom modules as children.[br]
## [b]Danger: [i]Don't modify this.[/i][/b]
## Internal, don't modify.
# Contains the node holding all custom modules as children.
var custom_modules_node: Node
## The CORE Object's logger instance.
## [b]Danger: [i]Don't modify this.[/i][/b]
## Internal, don't modify.
# The CORE Object's logger instance.
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 +++
## 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:
var inittime: int = Time.get_ticks_msec()
name = "CORE"
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()
reload_configuration(new_config)
initialize_modules()
apply_configuration()
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]
## [b]Danger: [i]Don't call this.[/i][/b]
# Initialization
func _ready() -> void:
var inittime: int = Time.get_ticks_msec()
inject_modules()
custom_modules_node.name = "Custom Modules"
add_child(custom_modules_node)
loggeri = logger.get_instance(basepath.replace("res://", "") + "src/core.gd", self)
add_child(scheduler)
get_tree().auto_accept_quit = false
initduration_initialization = Time.get_ticks_msec() - inittime
## Initializes all built-in modules during the preinitialization phase.[br]
## [b]Danger: [i]Don't call this.[/i][/b]
# Initializes all built-in modules during the preinitialization phase.
## Internal, don't call.
func initialize_modules() -> void:
for module in modules:
set(module, CoreBaseModule.new())
get(module).name = module
get(module).set_script(load(basepath + "src/" + module + ".gd"))
get(module).core = self
get(module).loggeri = logger.get_instance(basepath.replace("res://", "") + "src/" + module + ".gd", get(module))
get(module).logger = logger.get_instance(basepath.replace("res://", "") + "src/" + module + ".gd", get(module))
get(module)._initialize()
## Injects CORE's builtin modules into the SceneTree.[br]
## [b]Danger: [i]Don't call this.[/i][/b]
# Injects CORE's builtin modules into the SceneTree.
## Internal, don't call.
func inject_modules() -> void: for module in modules: add_child(get(module))
## Initializes the framework scheduler.
## [b]Danger: [i]Don't call this.[/i][/b]
# Initializes the framework scheduler.
## Internal, don't call.
func initialize_scheduler() -> void:
scheduler = Timer.new()
scheduler.name = "Scheduler"
@ -116,7 +146,9 @@ func initialize_scheduler() -> void:
scheduler.process_mode = Node.PROCESS_MODE_ALWAYS
scheduler.connect("timeout", func() -> void:
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()
)
@ -124,9 +156,10 @@ func initialize_scheduler() -> void:
## [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]
func complete_init(no_success_message: bool = false) -> void:
var modsinit_builtin: Array[String] = ["workaround"]
var modsinit_custom: Array[String] = ["workaround"]
func complete_init() -> void:
var inittime: int = Time.get_ticks_msec()
var modsinit_builtin: Array[String] = [ "workaround" ]
var modsinit_custom: Array[String] = [ "workaround" ]
while modsinit_builtin.size() != 0 and modsinit_custom.size() != 0:
# Clear arrays
@ -149,22 +182,45 @@ func complete_init(no_success_message: bool = false) -> void:
# Initialization complete
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 +++
## Loads a (new) configuration object and applies it to all modules.
func reload_configuration(new_config: CoreConfiguration = CoreConfiguration.new()) -> void:
var initialized = config != null
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()
if is_devmode(): # Override configuration in development mode
config.logger_level = CoreTypes.LoggerLevel.DIAG
if initialized: loggeri.verb("Overrode configuration (development mode)")
if initialized: apply_configuration()
## Applies the a configuration.[br]
## [b]Danger: [i]Don't call this.[/i][/b]
# Applies a new configuration.
## Internal, don't call.
func apply_configuration() -> void:
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!")
@ -177,7 +233,7 @@ func apply_configuration() -> void:
custom_modules[module]._pull_config()
# Workaround
logger.verbose_mode = OS.is_stdout_verbose()
if config.logger_detect_verbose_mode: logger.verbose_mode = OS.is_stdout_verbose()
# +++ custom module support +++
## 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.")
return false
if custom_modules.has(module_name):
loggeri.error("Registering module failed: A custom module with the name \"" + module_name + "\" already exists.")
loggeri.error(stringify_variables("Registering module failed: A custom module with the name %name% already exists.", { "name": module_name }))
return false
loggeri.diag("Updating variables")
module_class.name = module_name
module_class.core = self
module_class.loggeri = logger.get_instance(module_origin, module_class)
module_class.loggeri.framework = true
module_class.logger = logger.get_instance(module_origin, module_class)
module_class.logger.framework = true
loggeri.diag("Adding module to SceneTree")
custom_modules_node.add_child(module_class)
loggeri.diag("Merging module with custom_modules")
@ -208,11 +264,11 @@ func register_custom_module(module_name: String, module_origin: String, module_c
func unregister_custom_module(module_name: String) -> void:
loggeri.verb("Unregistering custom module \"" + module_name + "\"")
if !custom_modules.has(module_name):
loggeri.error("Unregistering module failed: A custom module with the name \"" + module_name + "\" does not exist.")
loggeri.error(stringify_variables("Unregistering module failed: A custom module with the name %name% does not exist.", { "name": module_name }))
return
var module: Node = get_custom_module(module_name)
var module: Node = custom_modules[module_name]
await module._cleanup()
module.loggeri.queue_free()
module.logger.queue_free()
custom_modules_node.remove_child(module)
custom_modules.erase(module_name)
module.queue_free()
@ -222,27 +278,88 @@ func unregister_custom_module(module_name: String) -> void:
func get_custom_module(module_name: String) -> CoreBaseModule:
loggeri.diag("Getting custom module \"" + module_name + "\"")
if !custom_modules.has(module_name):
loggeri.error("Getting module failed: A custom module with the name \"" + module_name + "\" does not exist.")
loggeri.error(stringify_variables("Getting module failed: A custom module with the name %name% does not exist.", { "name": module_name }))
return null
return custom_modules[module_name]
# +++ etc ++
# +++ cleanup ++
## 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:
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)
custom_modules_node.queue_free()
await get_tree().process_frame
loggeri.verb("Unloading built-in modules")
var modules_reverse: Array[String] = modules.duplicate()
modules_reverse.reverse()
for module in modules_reverse:
await get(module)._cleanup()
get(module).loggeri.queue_free()
get(module).queue_free()
config.queue_free()
await get_tree().process_frame
print("Freeing configuration")
config.free()
print("Freeing")
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.
func is_devmode() -> bool:
return config.development
@ -261,6 +378,7 @@ func get_formatted_string(string: String) -> String:
# Version strings
string = string.replace("%version%", str(version_version))
string = string.replace("%version_typerelease%", str(version_typerelease))
string = string.replace("%version_fork%", "" if version_fork == "" else "-" + version_fork)
var semantic_version: Array[int] = get_version_semantic()
string = string.replace("%version_semantic%", str(semantic_version[0]) + "." + str(semantic_version[1]) + "." + str(semantic_version[2]))
match(version_type):
@ -299,8 +417,7 @@ func get_version_semantic() -> Array[int]:
CoreTypes.VersionType.ALPHA: version_type_int = 0
return [version_version, version_type_int, version_typerelease]
## Determines CORE's installation/base path.[br]
## [b]Danger: [i]Don't call this.[/i][/b]
# Determines CORE's installation/base path.
func determine_basepath() -> bool:
if FileAccess.file_exists("res://.corebasepath"):
basepath = "res://"
@ -309,18 +426,17 @@ func determine_basepath() -> bool:
elif FileAccess.file_exists("res://addons/CORE/.corebasepath"):
basepath = "res://addons/CORE/"
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 true
# Checks Godot's version
## Checks compatibility with the running version.
func check_godot_version() -> bool:
var version: Dictionary = Engine.get_version_info()
match(version["major"]):
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
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.")
@ -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.")
return false
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 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]
func quit_safely(exitcode: int = 0) -> void:
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 cleanup()
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:
match(what):
NOTIFICATION_WM_CLOSE_REQUEST:
if config.automatic_shutdown:
loggeri.diag("Got close request, shutting down")
await quit_safely(0)

View file

@ -18,14 +18,11 @@
## Allows for awaited, batched and oneline requests.
extends CoreBaseModule
## Contains a list of all queued downloads.[br]
## [b]Danger: [i]Don't modify this[/i][/b].
# Contains a list of all queued downloads.
var list_queue: Dictionary = {}
## Contains a list of all active downloads.[br]
## [b]Danger: [i]Don't modify this[/i][/b].
# Contains a list of all active downloads.
var list_active: Dictionary = {}
## Contains a liust of all completed downloads.[br]
## [b]Danger: [i]Don't modify this[/i][/b].
# Contains a liust of all completed downloads.
var list_complete: Dictionary = {}
## Determines how unsecure requests should be handled.
@ -57,11 +54,12 @@ func _cleanup() -> void:
## [/codeblock]
## [b]Note: [i]Using the [code]await[/code] keyword is required for this function.[/i][/b]
func awaited_request(url: String, parse_utf8: bool, method: HTTPClient.Method = HTTPClient.Method.METHOD_GET, headers: PackedStringArray = PackedStringArray([]), data: String = "") -> Dictionary:
loggeri.verb("Creating awaited request")
logger.verb("Creating awaited request")
if !await is_url_allowed(url): return {}
var id: int = create_request(url, method, headers, data)
start_request(id, parse_utf8)
loggeri.diag("Waiting for request " + str(id) + " to finish")
logger.diag("Waiting for request " + str(id) + " to finish")
logger.diag(core.stringify_variables("Waiting for request %id% to finish", { "id": id }))
while !is_request_completed(id): await get_tree().create_timer(0.1, true).timeout
var dldata: Dictionary = list_complete[id]
list_complete.erase(id)
@ -96,7 +94,7 @@ func oneline_awaited_request(url: String, return_utf8: bool = true, ignore_http_
## [/codeblock]
## [b]Note: [i]Using the [code]await[/code] keyword is required for this function.[/i][/b]
func batch_awaited_request(urls: PackedStringArray, parse_utf8: bool, method: HTTPClient.Method = HTTPClient.Method.METHOD_GET, headers: PackedStringArray = PackedStringArray([]), data: String = "") -> Array[Dictionary]:
loggeri.verb("Creating " + str(urls.size()) + " awaited request(s)")
logger.verb("Creating " + str(urls.size()) + " awaited request(s)")
var dldata: Array[Dictionary] = []
for url in urls:
if !await is_url_allowed(url): continue
@ -107,29 +105,28 @@ func batch_awaited_request(urls: PackedStringArray, parse_utf8: bool, method: HT
list_complete.erase(id)
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:
var id: int = create_request(url, method, headers, data)
start_request(id, parse_utf8)
loggeri.diag("Waiting for request " + str(id) + " to finish")
logger.diag(core.stringify_variables("Waiting for request %id% to finish", { "id": id }))
while !is_request_completed(id): await get_tree().create_timer(0.1, true).timeout
return id
# +++ internal +++
## Returns a new download id.[br]
## [b]Danger: [i]Don't call this.[/i][/b]
# Returns a new download id.
func generate_id() -> int:
var id = randi()
if list_queue.has(id) or list_active.has(id):
loggeri.warn("New download id '" + str(id) + "' is already taken")
logger.warn(core.stringify_variables("New download id %id% already taken", { "id": id }))
return generate_id()
loggeri.diag("Generated new download id " + str(id))
logger.diag(core.stringify_variables("Generated new download id %id%", { "id": id }))
return id
## Creates a new request and stores it in the queue. Returns the download id.[br]
## [b]Warning: [i]You'll probably not need this. Only use this function when implementing your own downloading method.[/i][/b]
func create_request(url: String, method: HTTPClient.Method = HTTPClient.Method.METHOD_GET, headers: PackedStringArray = PackedStringArray([]), body: String = "") -> int:
loggeri.verb("Creating new request\n-> URL: " + url + "\n-> Method: " + str(method) + "\nHeaders: " + str(headers.size()) + "\nBody size: " + str(body.length()) + " Characters")
logger.verb("Creating new request\n-> URL: " + url + "\n-> Method: " + str(method) + "\nHeaders: " + str(headers.size()) + "\nBody size: " + str(body.length()) + " Characters")
var id = generate_id()
list_queue.merge({ id: { "url": url, "method": method, "headers": headers, "body": body } })
return id
@ -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]Warning: [i]You'll probably not need this. Only use this function when implementing your own downloading method.[/i][/b]
func start_request(id: int, parse_utf8: bool) -> void:
loggeri.verb("Starting request " + str(id))
logger.verb("Starting request " + str(id))
logger.verb(core.stringify_variables("Starting request %id%", { "id": id }))
list_active.merge({ id: list_queue[id] })
list_queue.erase(id)
loggeri.diag("Creating new HTTPRequest \"Request #" + str(id) + "\"")
logger.diag("Creating new HTTPRequest \"Request #" + str(id) + "\"")
var download: HTTPRequest = HTTPRequest.new()
download.name = "Request #" + str(id)
download.accept_gzip = true
download.use_threads = true
var lambda: Callable = func(result: int, http_code: int, headers: PackedStringArray, body: PackedByteArray, httprequest: HTTPRequest) -> void:
loggeri.verb("Request " + str(id) + " completed\nResult: " + str(result) + "\nHTTP response code: " + str(http_code) + "\nHeaders: " + str(headers.size()) + "\nBody size: " + str(body.size()) + " Bytes // " + str(core.misc.byte2mib(body.size(), true)) + " MiB\nParse body as UTF-8: " + str(parse_utf8))
logger.verb(core.stringify_variables("Request %id% completed\nResult: %result%\nHTTP response code: %http_code%\nHeaders: %headers%\nBody size: %body_size_bytes% Bytes // %body_size_mib% MiB\nParsed body as UTF-8: %parse_utf8%", { "result": result, "http_code": http_code, "headers": headers.size(), "body_size_bytes": body.size(), "body_size_mib": core.misc.byte2mib(body.size(), true), "parse_utf8": parse_utf8 }, true))
list_complete.merge({ id: { "result": result, "http_code": http_code, "headers": headers, "body": body, "body_utf8": body.get_string_from_utf8() if parse_utf8 else "" } })
list_active.erase(id)
remove_child(httprequest)
@ -161,14 +159,14 @@ func is_url_allowed(url: String) -> bool:
if url.begins_with("http://"):
match(config_unsecure_requests):
CoreTypes.BlockadeLevel.BLOCK:
loggeri.error("Blocked unsecure url '" + url + "'")
logger.error(core.stringify_variables("Blocked unsecure url %url%", { "url": url }))
return false
CoreTypes.BlockadeLevel.WARN: loggeri.warn("Requesting unsecure url '" + url + "'")
CoreTypes.BlockadeLevel.WARN: logger.warn(core.stringify_variables("Requesting unsecure url %url%", { "url": url }))
CoreTypes.BlockadeLevel.IGNORE: pass
_: await loggeri.crash("Invalid BlockadeLevel '" + str(config_unsecure_requests) + "'")
_: await logger.crash(core.stringify_variables("Invalid BlockadeLevel %level%", { "level": config_unsecure_requests }))
elif url.begins_with("https://"): pass
else:
loggeri.error("Invalid url '" + url + "'")
logger.error(core.stringify_variables("Invalid url %url%", { "url": url }))
return false
return true
@ -177,10 +175,10 @@ func is_request_completed(id: int) -> bool: return list_complete.has(id)
## Cleans the request queue.
func clean_queue() -> void:
loggeri.verb("Cleaning request queue")
logger.verb("Cleaning request queue")
list_queue.clear()
## Cleans the completed requests list.
func clean_completed() -> void:
loggeri.verb("Cleaning completed requests")
logger.verb("Cleaning completed requests")
list_complete.clear()

View file

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

View file

@ -28,12 +28,21 @@ var logrtl: RichTextLabel
var font_normal: Font
var font_bold: Font
# Configuration
var config_max_lines: int
# +++ module +++
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
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)
config_max_lines = core.config.logui_max_lines
func _cleanup() -> void:
background.remove_child(logrtl)
@ -90,7 +99,9 @@ func _ready() -> void:
vsbar.add_theme_stylebox_override("grabber_pressed", StyleBoxEmpty.new())
# Connect log_event
logger.connect("log_event", func(allowed: bool, _level: CoreTypes.LoggerLevel, _origin: String, _message: String, format: String) -> void: if allowed: logrtl.text = logrtl.text + format + "\n")
core.logger.connect("log_event", func(allowed: bool, _level: CoreTypes.LoggerLevel, _origin: String, _message: String, format: String) -> void:
if allowed and core.config.logui_enabled: logrtl.text = logrtl.text + format + "\n"
)
# +++ process +++
func _process(_delta: float) -> void:
@ -98,3 +109,17 @@ func _process(_delta: float) -> void:
var window_size: Vector2i = DisplayServer.window_get_size()
background.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.
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 +++
## Converts a number of bytes into mebibytes.[br]
## [br]
## If [code]flatten[/code] is set to [code]true[/code], the decimal part will be discarded.
@warning_ignore("integer_division")
func byte2mib(bytes: int, flatten: bool = true) -> float:
if flatten: return bytes/1048576
return bytes/float(1048576)
if flatten: return float(int(float(bytes)/1048576.0))
return float(bytes)/1048576.0
## Converts a number of mebibytes into bytes.[br]
## [br]
## If [code]flatten[/code] is set to [code]true[/code], the decimal part will be discarded.
func mib2byte(mib: float, flatten: bool = true) -> float:
if flatten: return int(mib*1048576)
return mib*1048576
if flatten: return float(int(mib*1048576.0))
return mib*1048576.0
## Converts a number of mebibytes into gibibytes.[br]
## [br]
## If [code]flatten[/code] is set to [code]true[/code], the decimal part will be discarded.
func mib2gib(mib: float, flatten: bool = true) -> float:
if flatten: return int(mib/1024)
return mib/1024
if flatten: return float(int(mib/1024.0))
return mib/1024.0
## Converts a number of gebibytes into mebibytes.[br]
## [br]
## If [code]flatten[/code] is set to [code]true[/code], the decimal part will be discarded.
func gib2mib(gib: float, flatten: bool = true) -> float:
if flatten: return int(gib*1024)
return gib*1024
if flatten: return float(int(gib*1024.0))
return gib*1024.0
# +++ type formatting +++
## Converts a string array into a normal, nicely formatted string.[br]
@ -72,11 +85,11 @@ func format_stringarray(array: Array[String], item_before: String = "", item_aft
var output: String = ""
if array.size() == 0:
loggeri.warn("Unable to format a string with a size of 0")
logger.warn("Unable to format a string with a size of 0")
return ""
elif array.size() == 1:
loggeri.warn("Unable to format a string with a size of 1")
return array[0]
logger.warn("Unable to format a string with a size of 1")
return item_before + array[0] + item_after
for item in array:
if output == "": output = item_before + item + item_after
@ -94,7 +107,7 @@ func array_to_stringarray(array: Array) -> Array[String]:
for item in array:
if typeof(item) != TYPE_STRING:
logger.error("Cannot convert Array to Array[String]: Item '" + str(item) + "' is not of type String")
logger.error(core.stringify_string("Cannot convert Array to Array[String]: Item %item% is not of type String", { "item": item }))
return []
output.append(item)
@ -125,6 +138,166 @@ func stringarray_to_array(array: Array[String]) -> Array:
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)
## 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].
## @deprecated
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.
const scene_nodes: Array[String] = [ "debug", "cutscene", "menu", "main", "background" ]
## The 'debug' scene collection.[br]
## [b]Danger: [i]Don't modify this.[/i][/b]
# Scene collections
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()
## The 'menu' scene collection.[br]
## [b]Danger: [i]Don't modify this.[/i][/b]
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()
## The 'background' scene collection.[br]
## [b]Danger: [i]Don't modify this.[/i][/b]
var scenes_background: Node = Node.new()
## A list of all loaded scenes[br]
## [b]Danger: [i]Don't modify this.[/i][/b]
# A list of all loaded scenes.
var scenes: Dictionary = {}
# +++ module +++
@ -59,114 +49,114 @@ func _cleanup() -> void:
func _pull_config() -> void:
if core.config.headless:
# Remove all scenes
if is_inside_tree(): loggeri.verb("Removing all scenes (triggered by headless mode)")
if is_inside_tree(): logger.verb("Removing all scenes (triggered by headless mode)")
for scene in scenes: remove_scene(scene, true)
# +++ scene management +++
## Adds a scene to some scene collection.
func add_scene(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
loggeri.verb("Adding scene \"" + sname + "\" of type " + str(type))
if exists(sname) != CoreTypes.SceneType.NONE:
loggeri.error("Scene with name \"" + sname + "\" already exists")
logger.verb(core.stringify_variables("Adding scene %name% of type %type%", { "name": scene_name, "type": scene_type }))
if exists(scene_name) != CoreTypes.SceneType.NONE:
logger.error(core.stringify_variables("A scene named %name% already exists", { "name": scene_name }))
return false
if typeof(sclass) != TYPE_OBJECT or !sclass.is_class("Node"):
loggeri.error("Scene class \"" + sname + "\" is not of type Node")
if typeof(scene_class) != TYPE_OBJECT or !scene_class.is_class("Node"):
logger.error(core.stringify_variables("Scene class %name% is not of type Node", { "name": scene_name }))
return false
sclass.name = sname
match(type):
CoreTypes.SceneType.DEBUG: scenes_debug.add_child(sclass)
CoreTypes.SceneType.CUTSCENE: scenes_cutscene.add_child(sclass)
CoreTypes.SceneType.MENU: scenes_menu.add_child(sclass)
CoreTypes.SceneType.MAIN: scenes_main.add_child(sclass)
CoreTypes.SceneType.BACKGROUND: scenes_background.add_child(sclass)
scene_class.name = scene_name
match(scene_type):
CoreTypes.SceneType.DEBUG: scenes_debug.add_child(scene_class)
CoreTypes.SceneType.CUTSCENE: scenes_cutscene.add_child(scene_class)
CoreTypes.SceneType.MENU: scenes_menu.add_child(scene_class)
CoreTypes.SceneType.MAIN: scenes_main.add_child(scene_class)
CoreTypes.SceneType.BACKGROUND: scenes_background.add_child(scene_class)
CoreTypes.SceneType.NONE:
loggeri.error("CoreTypes.SceneType.NONE is not a valid scene type")
logger.error("CoreTypes.SceneType.NONE is not a valid scene type")
return false
_: await loggeri.crash("Invalid SceneType " + str(type))
scenes.merge({ sname: { "type": type, "class": sclass } })
_: await logger.crash(core.stringify_variables("Invalid SceneType %type%", { "type": scene_type }))
scenes.merge({ scene_name: { "type": scene_type, "class": scene_class } })
return true
## Removes a scene from some scene collection.[br]
## [b]Danger: [i]Don't set [code]force_remove[/code] to [code]true[/code], thanks![/i][/b]
func remove_scene(sname: String, force_remove: bool = false) -> bool:
func remove_scene(scene_name: String, force_remove: bool = false) -> bool:
if core.config.headless and !force_remove: return false
if force_remove: await loggeri.crash("force_remove = true is not allowed")
loggeri.verb("Removing scene \"" + sname + "\"")
match(exists(sname)):
if force_remove: await logger.crash("force_remove is not allowed to be true")
logger.verb(core.stringify_variables("Removing scene %name%", { "name": scene_name }))
match(exists(scene_name)):
CoreTypes.SceneType.DEBUG:
scenes_debug.remove_child(scenes[sname]["class"])
scenes[sname]["class"].queue_free()
scenes_debug.remove_child(scenes[scene_name]["class"])
scenes[scene_name]["class"].queue_free()
CoreTypes.SceneType.CUTSCENE:
scenes_cutscene.remove_child(scenes[sname]["class"])
scenes[sname]["class"].queue_free()
scenes_cutscene.remove_child(scenes[scene_name]["class"])
scenes[scene_name]["class"].queue_free()
CoreTypes.SceneType.MENU:
scenes_menu.remove_child(scenes[sname]["class"])
scenes[sname]["class"].queue_free()
scenes_menu.remove_child(scenes[scene_name]["class"])
scenes[scene_name]["class"].queue_free()
CoreTypes.SceneType.MAIN:
scenes_main.remove_child(scenes[sname]["class"])
scenes[sname]["class"].queue_free()
scenes_main.remove_child(scenes[scene_name]["class"])
scenes[scene_name]["class"].queue_free()
CoreTypes.SceneType.BACKGROUND:
scenes_background.remove_child(scenes[sname]["class"])
scenes[sname]["class"].queue_free()
scenes_background.remove_child(scenes[scene_name]["class"])
scenes[scene_name]["class"].queue_free()
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
_: await loggeri.crash("Invalid SceneType " + str(exists(sname)))
scenes.erase(sname)
_: await logger.crash(core.stringify_variables("Invalid SceneType %type%", { "type": exists(scene_name) }))
scenes.erase(scene_name)
return true
# +++ getters +++
## Returns a scene from some scene collection.[br]
## [br]
## Returns [code]null[/code] if no scene with that name was found.
func get_scene(sname: String) -> Node:
func get_scene(scene_name: String) -> Node:
if core.config.headless: return null
match(exists(sname)):
CoreTypes.SceneType.DEBUG: return scenes[sname]["class"]
CoreTypes.SceneType.CUTSCENE: return scenes[sname]["class"]
CoreTypes.SceneType.MENU: return scenes[sname]["class"]
CoreTypes.SceneType.MAIN: return scenes[sname]["class"]
CoreTypes.SceneType.BACKGROUND: return scenes[sname]["class"]
CoreTypes.SceneType.NONE: loggeri.error("Scene \"" + sname + "\" does not exist")
_: await loggeri.crash("Invalid SceneType " + str(exists(sname)))
match(exists(scene_name)):
CoreTypes.SceneType.DEBUG: return scenes[scene_name]["class"]
CoreTypes.SceneType.CUTSCENE: return scenes[scene_name]["class"]
CoreTypes.SceneType.MENU: return scenes[scene_name]["class"]
CoreTypes.SceneType.MAIN: return scenes[scene_name]["class"]
CoreTypes.SceneType.BACKGROUND: return scenes[scene_name]["class"]
CoreTypes.SceneType.NONE: logger.error(core.stringify_variables("Scene %name% does not exist", { "name": scene_name }))
_: await logger.crash(core.stringify_variables("Invalid SceneType %type%", { "type": exists(scene_name) }))
return null
## Returns a scene collection node.[br]
## Useful if you want to change a child's index.[br]
## [b]Danger: [i]Don't change any properties of the scene collection or free it, otherwise you may cause breakage.[/i][/b]
func get_scene_collection(type: CoreTypes.SceneType) -> Node:
func get_scene_collection(scene_type: CoreTypes.SceneType) -> Node:
if core.config.headless: return null
match(type):
match(scene_type):
CoreTypes.SceneType.DEBUG: return scenes_debug
CoreTypes.SceneType.CUTSCENE: return scenes_cutscene
CoreTypes.SceneType.MENU: return scenes_menu
CoreTypes.SceneType.MAIN: return scenes_main
CoreTypes.SceneType.BACKGROUND: return scenes_background
CoreTypes.SceneType.NONE: loggeri.error("No scene collection was found for CoreTypes.SceneType.NONE")
_: await loggeri.crash("Invalid SceneType " + str(type))
CoreTypes.SceneType.NONE: logger.error("No scene collection was found for CoreTypes.SceneType.NONE")
_: await logger.crash(core.stringify_variables("Invalid SceneType %type%", { "type": scene_type }))
return null
## Returns a list of all loaded scenes in some scene collection.
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] = []
for scene in scenes:
if scenes[scene]["type"] == type:
if scenes[scene]["type"] == scene_type:
list.append(scenes[scene]["class"])
return list
## 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
for scene in scenes:
if scenes[scene]["type"] == type:
if scenes[scene]["type"] == scene_type:
amount += 1
return amount
## Returns the scene collection a scene is loaded in.[br]
## [br]
## [enum CoreTypes.SceneType][code].NONE[/code] if no scene with that name was found.
func exists(sname: String) -> CoreTypes.SceneType:
func exists(scene_name: String) -> CoreTypes.SceneType:
for scene in scenes:
if scene == sname: return scenes[scene]["type"]
if scene == scene_name: return scenes[scene]["type"]
return CoreTypes.SceneType.NONE

View file

@ -23,51 +23,49 @@ extends CoreBaseModule
## Indicates if a storage file is currently open.[br]
## [b]Danger: [i]Don't modify this.[/i][/b]
var is_open: bool = false
## The parsed data inside the storage file.[br]
## [b]Danger: [i]Don't modify this.[/i][/b]
# The parsed data inside the storage file.
var storage: Dictionary = {}
## The location of the storage file.[br]
## [b]Danger: [i]Don't modify this.[/i][/b]
# The location of the storage file.
var storage_location: String = ""
# +++ 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:
if is_open:
loggeri.error("Failed to open storage: A storage file is already open")
logger.error("Failed to open storage: A storage file is already open")
return false
loggeri.verb("Opening storage file at \"" + location + "\"")
logger.verb(core.stringify_variables("Opening storage file at %location%", { "location": location }))
var file: FileAccess
if !FileAccess.file_exists(location):
if create_new:
file = FileAccess.open(location, FileAccess.WRITE)
if file == null:
await loggeri.crash("Could not open storage file at \"" + location + "\": Failed with code " + str(FileAccess.get_open_error()))
await logger.crash(core.stringify_variables("Could not open storage file at %location%: Failed with error %error%", { "location": location, "error": error_string(FileAccess.get_open_error()) }))
return false
file.store_string("{}")
file.close()
else:
loggeri.error("Failed to open storage: create_new is set to false")
logger.error("Failed to open storage: create_new is set to false")
return false
file = FileAccess.open(location, FileAccess.READ)
var storage_temp: Variant = file.get_as_text()
file.close()
storage_temp = JSON.parse_string(storage_temp)
if typeof(storage_temp) != TYPE_DICTIONARY:
loggeri.error("Failed to open storage: Parsed storage file is of type " + str(typeof(storage_temp)))
logger.error(core.stringify_variables("Failed to open storage: Parsed storage file is of type %type%", { "type": type_string(typeof(storage_temp)) }))
return false
if sanity_check:
var check_result: Array[String] = perform_sanity_check(storage_temp)
if check_result.size() != 0:
if fail_on_sanity_check:
loggeri.error("Sanity check failed (stopping):")
logger.error("Sanity check failed (stopping):")
for error in check_result:
loggeri.error("-> " + error)
logger.error("-> " + error)
return false
else:
loggeri.warn("Sanity check failed (continuing anyway):")
logger.warn("Sanity check failed (continuing anyway):")
for error in check_result:
loggeri.warn("-> " + error)
logger.warn("-> " + error)
storage = storage_temp
storage_location = location
is_open = true
@ -76,9 +74,9 @@ func open_storage(location: String, create_new: bool = true, sanity_check: bool
## Closes the active storage file.
func close_storage() -> bool:
if !is_open:
loggeri.error("Failed to close storage: No storage file is open")
logger.error("Failed to close storage: No storage file is open")
return false
loggeri.verb("Closing storage file")
logger.verb("Closing storage file")
storage = {}
is_open = false
return true
@ -86,13 +84,13 @@ func close_storage() -> bool:
## Saves the active storage file to disk.
func save_storage() -> bool:
if !is_open:
loggeri.error("Failed to save storage: No storage file is open")
logger.error("Failed to save storage: No storage file is open")
return false
var file: FileAccess = FileAccess.open(storage_location, FileAccess.WRITE)
if file == null:
await loggeri.crash("Could not open storage file at \"" + storage_location + "\": Failed with code " + str(FileAccess.get_open_error()))
await logger.crash(core.stringify_variables("Could not open storage file at %location%: Failed with error %error%", { "location": storage_location, "error": error_string(FileAccess.get_open_error()) }))
return false
loggeri.diag("Writing storage file to disk")
logger.diag("Writing storage file to disk")
file.store_string(JSON.stringify(storage))
file.close()
return true
@ -101,9 +99,9 @@ func save_storage() -> bool:
## Removes all keys from the active storage file. The nuclear option basically.
func nuke_storage(autosave: bool = true) -> bool:
if !is_open:
loggeri.error("Failed to nuke storage: No storage file is open")
logger.error("Failed to nuke storage: No storage file is open")
return false
loggeri.warn("Nuking storage")
logger.warn("Nuking storage")
storage = {}
if autosave: save_storage()
return true
@ -111,17 +109,17 @@ func nuke_storage(autosave: bool = true) -> bool:
## Returns a storage key. Can also return a default value if unset.
func get_key(key: String, default: Variant = null) -> Variant:
if !is_open:
loggeri.error("Failed to get key: No storage file is open")
logger.error("Failed to get key: No storage file is open")
return NAN
loggeri.diag("Returning storage key \"" + key + "\" (default='" + str(default) + "')")
logger.diag(core.stringify_variables("Returning storage key %key% (default=%default%)", { "key": key, "default": default }))
return storage.get(key, default)
## Updates a storage key with the specified value.
func set_key(key: String, value: Variant, overwrite: bool = true, autosave: bool = true) -> bool:
if !is_open:
loggeri.error("Failed to set key: No storage file is open")
logger.error("Failed to set key: No storage file is open")
return false
loggeri.diag("Updating storage key \"" + key + "\" with value '" + str(value) + "' (overwrite='" + str(overwrite) + "' autosave='" + str(autosave) + "'")
logger.diag(core.stringify_variables("Updating storage key %key% with value %value% (overwrite=%overwrite% autosave=%autosave%)", { "key": key, "value": value, "overwrite": overwrite, "autosave": autosave }))
storage.merge({key: value}, overwrite)
if autosave: save_storage()
return true
@ -131,7 +129,7 @@ func del_key(key: String, autosave: bool = true) -> bool:
if !is_open:
logger.errof("storage", "Failed to delete key: No storage file is open")
return false
loggeri.diag("Deleting storage key \"" + key + "\" (autosave='" + str(autosave) + "')")
logger.diag(core.stringify_variables("Deleting storage key %key% (autosave=%autosave%)", { "key": key, "autosave": autosave }))
storage.erase(key)
if autosave: save_storage()
return true
@ -141,30 +139,30 @@ func del_key(key: String, autosave: bool = true) -> bool:
## pass your modified [class Dictionary to [method safe_dict].
func get_dict() -> Dictionary:
if !is_open:
loggeri.error("Failed to get dictionary: No storage file is open")
logger.error("Failed to get dictionary: No storage file is open")
return {}
loggeri.verb("Returning storage dictionary")
logger.verb("Returning storage dictionary")
return storage
# +++ raw manipulation +++
## Saves a arbitrary dictionary as a [param storage] [class Dictionary] with sanity checking ([code]sanity_check[/code] and [code]fail_on_sanity_check[/code]).
func save_dict(dict: Dictionary, sanity_check: bool = true, fail_on_sanity_check: bool = false, autosave: bool = true) -> bool:
if !is_open:
loggeri.error("Failed to save dictionary: No storage file is open")
logger.error("Failed to save dictionary: No storage file is open")
return false
loggeri.verb("Saving custom dictionary as storage")
logger.verb("Saving custom dictionary as storage")
if sanity_check:
var check_result: Array[String] = perform_sanity_check(dict)
if check_result.size() != 0:
if fail_on_sanity_check:
loggeri.error("Sanity check failed (stopping):")
logger.error("Sanity check failed (stopping):")
for error in check_result:
loggeri.error("-> " + error)
logger.error("-> " + error)
return false
else:
loggeri.warn("Sanity check failed (continuing anyway):")
logger.warn("Sanity check failed (continuing anyway):")
for error in check_result:
loggeri.warn("-> " + error)
logger.warn("-> " + error)
storage = dict
if autosave: save_storage()
return true
@ -172,14 +170,14 @@ func save_dict(dict: Dictionary, sanity_check: bool = true, fail_on_sanity_check
# +++ etc +++
## Performs sanity checks on a [class Dictionary] to determine if it can be saved and loaded using this module.
func perform_sanity_check(storage_check: Dictionary) -> Array[String]:
loggeri.verb("Performing a sanity check on some storage dictionary")
logger.verb("Performing a sanity check on some storage dictionary")
var errors: Array[String] = []
for key in storage_check:
if typeof(key) != TYPE_STRING:
errors.append("Key \"" + str(key) + "\" is not of type String (type '" + type_string(typeof(key)) + "')")
errors.append(core.stringify_variables("Key %key% is not of type String (is of type %type%)", { "key": key, "type": type_string(typeof(key)) }))
continue
if typeof(storage_check[key]) != TYPE_NIL and typeof(storage_check[key]) != TYPE_STRING and typeof(storage_check[key]) != TYPE_INT and typeof(storage_check[key]) != TYPE_FLOAT and typeof(storage_check[key]) != TYPE_BOOL and typeof(storage_check[key]) != TYPE_ARRAY and typeof(storage_check[key]) != TYPE_DICTIONARY:
errors.append("The value of \"" + key + "\" is not null, a string, an integer, a float, boolean, array or dictionary (type '" + type_string(typeof(key)) + "')")
loggeri.verb("Completed sanity check with " + str(errors.size()) + " errors")
errors.append(core.stringify_variables("The value of %key% is not null, a String, an int, a float, boolean, an Array or a Dictionary (is of type %type%)", { "key": key, "type": type_string(typeof(key)) }))
logger.verb(core.stringify_variables("Completed sanity check with %errors% errors", { "errors": errors.size() }))
return errors

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