Update to v2-release0
This commit is contained in:
parent
9d46a0ec05
commit
c885f654f4
13 changed files with 1054 additions and 208 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 }
|
||||
|
|
91
src/classes/validationschema.gd
Normal file
91
src/classes/validationschema.gd
Normal 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
|
317
src/classes/validationsingle.gd
Normal file
317
src/classes/validationsingle.gd
Normal 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
|
233
src/core.gd
233
src/core.gd
|
@ -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)
|
||||
|
|
48
src/erm.gd
48
src/erm.gd
|
@ -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()
|
||||
|
|
|
@ -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")
|
||||
|
|
29
src/logui.gd
29
src/logui.gd
|
@ -28,13 +28,22 @@ 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)
|
||||
core.sms.remove_child(background)
|
||||
|
@ -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
|
||||
|
|
197
src/misc.gd
197
src/misc.gd
|
@ -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)
|
||||
|
|
120
src/sms.gd
120
src/sms.gd
|
@ -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
|
||||
|
|
|
@ -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)) + "')")
|
||||
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
|
||||
|
|
81
src/validation.gd
Normal file
81
src/validation.gd
Normal 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
|
Loading…
Reference in a new issue