CORE/src/core.gd

411 lines
17 KiB
GDScript3
Raw Normal View History

# CORE FRAMEWORK SOURCE FILE
# Copyright (c) 2024 The StarOpenSource Project & Contributors
2024-03-03 18:53:09 +01:00
# Licensed under the GNU Affero General Public License v3
#
# This program is free software: you can redistribute it and/or modify
2024-03-03 18:53:09 +01:00
# 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
2024-03-03 18:53:09 +01:00
# GNU Affero General Public License for more details.
#
2024-03-03 18:53:09 +01:00
# 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/>.
## Initializes and manages the framework.
##
## The [b]CORE Object[/b] is responsible for initializing, managing and
2024-04-08 02:43:31 +02:00
## serving the CORE Framework.
2024-02-04 21:36:30 +01:00
extends Node
class_name Core
2024-04-08 02:43:31 +02:00
# Versioning
## The version number
const version_version: int = 1
2024-04-08 02:43:31 +02:00
## 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.
2024-04-14 15:18:21 +02:00
const version_typerelease: int = 1
2024-02-04 21:36:30 +01:00
# Modules
2024-04-08 02:43:31 +02:00
## Used internally for loading, managing and unloading modules.
2024-03-31 17:35:09 +02:00
const modules: Array[String] = [ "logger", "misc", "sms", "logui", "erm", "storage" ]
2024-04-08 02:43:31 +02:00
## CORE's configuration object.[br]
## [b]NEVER access this yourself! To change the configuration use [method reload_configuration] instead.[/b]
var config: CoreConfiguration
## The framework scheduler.[br]
## Performs various maintenance tasks every minute.
## [b]Danger: [i]Don't modify this.[/i][/b]
var scheduler: Timer
2024-04-08 02:43:31 +02:00
## The 'Logger' module
2024-02-04 21:36:30 +01:00
var logger: CoreBaseModule
2024-04-08 02:43:31 +02:00
## The 'Miscellaneous' module
2024-02-04 21:36:30 +01:00
var misc: CoreBaseModule
2024-04-08 02:43:31 +02:00
## The 'Scene Management System' module
2024-02-09 21:18:14 +01:00
var sms: CoreBaseModule
2024-04-08 02:43:31 +02:00
## The 'Log UI' module. Not important for you, it just displays the log graphically :3
2024-02-04 21:36:30 +01:00
var logui: CoreBaseModule
2024-04-08 02:43:31 +02:00
## The 'Easy Request Maker' module (formerly 'Easy DownLoader')
2024-03-31 17:35:09 +02:00
var erm: CoreBaseModule
2024-04-08 02:43:31 +02:00
## The 'Storage' module
2024-03-18 03:23:27 +01:00
var storage: CoreBaseModule
2024-02-04 21:36:30 +01:00
2024-04-08 02:43:31 +02:00
# /etc/
## Stores the path to CORE's installation directory.[br]
## [b]Danger: [i]Don't modify this.[/i][/b]
2024-02-04 21:36:30 +01:00
var basepath: String
# 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 = {}
## Internal, don't modify.
# Contains the node holding all custom modules as children.
2024-02-09 15:30:20 +01:00
var custom_modules_node: Node
## Internal, don't modify.
# The CORE Object's logger instance.
2024-04-08 21:30:24 +02:00
var loggeri: CoreLoggerInstance
## Internal, don't modify.
# Makes CORE inaccessible if true.
var disabled: bool = false
2024-02-04 21:36:30 +01:00
2024-04-08 02:43:31 +02:00
# +++ initialization +++
2024-02-04 21:36:30 +01:00
func _init(new_config: CoreConfiguration = CoreConfiguration.new()) -> void:
name = "CORE"
2024-03-03 19:08:20 +01:00
if !check_godot_version(): return
2024-02-04 21:36:30 +01:00
if !determine_basepath(): queue_free()
2024-02-09 15:30:20 +01:00
custom_modules_node = Node.new()
2024-02-04 21:36:30 +01:00
reload_configuration(new_config)
initialize_modules()
apply_configuration()
initialize_scheduler()
2024-02-04 21:36:30 +01:00
func _ready() -> void:
inject_modules()
2024-02-09 22:59:43 +01:00
custom_modules_node.name = "Custom Modules"
2024-02-09 15:30:20 +01:00
add_child(custom_modules_node)
loggeri = logger.get_instance(basepath.replace("res://", "") + "src/core.gd", self)
add_child(scheduler)
2024-04-14 14:23:05 +02:00
get_tree().auto_accept_quit = false
2024-02-04 21:36:30 +01:00
# Initializes all built-in modules during the preinitialization phase.
## Internal, don't call.
2024-02-04 21:36:30 +01:00
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)._initialize()
2024-02-04 21:36:30 +01:00
# Injects CORE's builtin modules into the SceneTree.
## Internal, don't call.
func inject_modules() -> void: for module in modules: add_child(get(module))
2024-02-04 21:36:30 +01:00
# Initializes the framework scheduler.
## Internal, don't call.
func initialize_scheduler() -> void:
scheduler = Timer.new()
scheduler.name = "Scheduler"
scheduler.autostart = true
scheduler.one_shot = false
scheduler.paused = false
scheduler.wait_time = 60
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()
for module in custom_modules_node.get_children(): await module._schedule()
)
2024-04-08 02:43:31 +02:00
## Waits for all modules to fully initialize.[br]
2024-03-22 21:12:11 +01:00
## [br]
2024-04-08 02:43:31 +02:00
## 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:
2024-03-22 21:12:11 +01:00
var modsinit_builtin: Array[String] = ["workaround"]
var modsinit_custom: Array[String] = ["workaround"]
while modsinit_builtin.size() != 0 and modsinit_custom.size() != 0:
# Clear arrays
modsinit_builtin = []
modsinit_custom = []
# Check builtin modules
for module in modules: if !get(module).initialized: modsinit_builtin.append(module)
2024-03-22 21:12:11 +01:00
# Check custom modules
for module_name in custom_modules:
if !custom_modules[module_name].initialized: modsinit_custom.append(module_name)
# Print and sleep
if modsinit_builtin.size() != 0 or modsinit_custom.size() != 0:
print("Waiting for modules to finish initialization:")
if modsinit_builtin.size() != 0: print(" Built-in: " + str(modsinit_builtin))
if modsinit_custom.size() != 0: print(" Custom: " + str(modsinit_custom))
2024-03-22 21:12:11 +01:00
await get_tree().create_timer(1).timeout
# Initialization complete
await get_tree().process_frame
2024-04-08 21:30:24 +02:00
if !no_success_message: loggeri.info("Initialized CORE successfully")
2024-03-22 21:12:11 +01:00
# +++ 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()
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 a new configuration.
## Internal, don't call.
func apply_configuration() -> void:
if loggeri != null: loggeri.verb("Applying configuration")
2024-04-10 21:34:12 +02:00
if is_devmode() and loggeri != null: loggeri.warn("The CORE Framework is in development mode. Here be dragons!")
if !config.custom_modules:
2024-04-12 15:48:17 +02:00
if loggeri != null: loggeri.diag("Removing all custom modules (custom modules support is disabled)")
for module in custom_modules: unregister_custom_module(module)
for module in modules: get(module)._pull_config()
if config.custom_modules:
for module in custom_modules:
2024-04-12 15:48:17 +02:00
custom_modules[module]._pull_config()
2024-04-14 15:17:55 +02:00
# Workaround
logger.verbose_mode = OS.is_stdout_verbose()
2024-04-08 02:43:31 +02:00
# +++ custom module support +++
## Registers a new custom module.
func register_custom_module(module_name: String, module_origin: String, module_class: CoreBaseModule) -> bool:
2024-04-08 21:30:24 +02:00
loggeri.verb("Registering new custom module \"" + module_name + "\"")
if !config.custom_modules:
2024-04-08 21:30:24 +02:00
loggeri.error("Registering module failed: Custom module support is disabled.")
return false
if custom_modules.has(module_name):
2024-04-08 21:30:24 +02:00
loggeri.error("Registering module failed: A custom module with the name \"" + module_name + "\" already exists.")
return false
2024-04-08 21:30:24 +02:00
loggeri.diag("Updating variables")
module_class.name = module_name
2024-03-22 21:41:27 +01:00
module_class.core = self
module_class.loggeri = logger.get_instance(module_origin, module_class)
module_class.loggeri.framework = true
2024-04-08 21:30:24 +02:00
loggeri.diag("Adding module to SceneTree")
2024-02-09 15:30:20 +01:00
custom_modules_node.add_child(module_class)
2024-04-08 21:30:24 +02:00
loggeri.diag("Merging module with custom_modules")
custom_modules.merge({ module_name: module_class })
2024-04-08 21:30:24 +02:00
loggeri.diag("Initializing custom module")
module_class._initialize()
2024-04-08 21:30:24 +02:00
loggeri.diag("Updating custom module configuration")
module_class._pull_config()
return true
2024-04-08 02:43:31 +02:00
## Unregisters a custom module, making it no longer available.
func unregister_custom_module(module_name: String) -> void:
2024-04-08 21:30:24 +02:00
loggeri.verb("Unregistering custom module \"" + module_name + "\"")
if !custom_modules.has(module_name):
2024-04-08 21:30:24 +02:00
loggeri.error("Unregistering module failed: A custom module with the name \"" + module_name + "\" does not exist.")
return
2024-04-14 19:58:48 +02:00
var module: Node = custom_modules[module_name]
await module._cleanup()
module.loggeri.queue_free()
custom_modules_node.remove_child(module)
custom_modules.erase(module_name)
module.queue_free()
2024-04-08 02:43:31 +02:00
## Returns a registered custom module.[br]
## Please note that you can't get CORE's built-in modules with this function.
func get_custom_module(module_name: String) -> CoreBaseModule:
2024-04-08 21:30:24 +02:00
loggeri.diag("Getting custom module \"" + module_name + "\"")
if !custom_modules.has(module_name):
2024-04-08 21:30:24 +02:00
loggeri.error("Getting module failed: A custom module with the name \"" + module_name + "\" does not exist.")
2024-03-22 21:41:27 +01:00
return null
return custom_modules[module_name]
# +++ cleanup ++
2024-04-08 02:43:31 +02:00
## 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.[br]
## Only call this function if you're sure that your application or game no longer uses the CORE Framework.
2024-04-08 02:43:31 +02:00
func cleanup() -> void:
2024-04-08 21:30:24 +02:00
loggeri.info("Cleaning up")
loggeri.verb("Calling cleanup hooks")
for hook in cleanup_hooks:
if !cleanup_hooks[hook].is_valid():
loggeri.error("Cleanup hook #" + str(hook) + " is invalid")
else:
loggeri.diag("Calling cleanup hook #" + str(hook))
await cleanup_hooks[hook].call()
loggeri.verb("Unregistering custom modules")
2024-04-14 19:58:48 +02:00
for module in custom_modules_node.get_children(): await unregister_custom_module(module.name)
loggeri.verb("Removing custom module support")
2024-04-12 17:03:37 +02:00
remove_child(custom_modules_node)
custom_modules_node.queue_free()
loggeri.verb("Unloading built-in modules")
2024-04-08 02:43:31 +02:00
var modules_reverse: Array[String] = modules.duplicate()
modules_reverse.reverse()
for module in modules_reverse:
await get(module)._cleanup()
get(module).queue_free()
print("Freeing configuration")
2024-04-12 17:03:37 +02:00
config.queue_free()
2024-04-14 19:58:48 +02:00
await get_tree().process_frame
print("Freeing")
2024-04-08 02:43:31 +02:00
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("New cleanup hook id #" + str(id) + " is already taken")
return _generate_hook_id()
elif id == -1:
loggeri.warn("Invalid cleanup hook id '-1'")
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("Could not remove cleanup hook (id): Hook #" + str(id) + " does not exist")
return false
loggeri.verb("Removed cleanup hook #" + str(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("Could not remove cleanup hook (ref): find_key did not return an integer (returned '" + str(id) + "')")
loggeri.verb("Removed cleanup hook #" + str(id) + " (ref)")
cleanup_hooks.erase(id)
return true
# +++ etc +++
2024-04-09 00:53:43 +02:00
## Returns if the framework is in development mode.
2024-02-04 21:36:30 +01:00
func is_devmode() -> bool:
2024-04-09 00:53:43 +02:00
return config.development
2024-02-04 21:36:30 +01:00
2024-04-08 02:43:31 +02:00
## Replaces placeholders with human-friendly strings.[br]
## You can use the following placeholders:[br]
## - [code]%version%[/code]: Returns the version number.[br]
## - [code]%version_type%[/code]: Returns the version type number[br]
## - [code]%version_semantic%[/code]: Returns the result of [method Core.get_version_semantic], example [i]5.2.3[/i][br]
## - [code]%version_type%[/code]: Returns the version type as a word, for example [i]Release Candidate[/i][br]
## - [code]%version_type_technical%[/code]: Returns the version type as one or two lowercase letters, for example [i]rc[/i][br]
## - [code]%devmode%[/code]: Returns the development mode status[br]
2024-04-08 02:43:31 +02:00
## - [code]%headless%[/code]: Returns the headless mode status[br]
## - [code]%custommodules%[/code]: Returns if custom module support is enabled
2024-02-04 21:36:30 +01:00
func get_formatted_string(string: String) -> String:
# Version strings
string = string.replace("%version%", str(version_version))
2024-04-08 20:18:49 +02:00
string = string.replace("%version_typerelease%", str(version_typerelease))
2024-02-04 21:36:30 +01:00
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]))
2024-02-04 21:36:30 +01:00
match(version_type):
CoreTypes.VersionType.RELEASE:
2024-04-08 02:43:31 +02:00
string = string.replace("%version_type%", "Release")
string = string.replace("%version_type_technical%", "r")
2024-02-04 21:36:30 +01:00
CoreTypes.VersionType.RELEASECANDIDATE:
2024-04-08 02:43:31 +02:00
string = string.replace("%version_type%", "Release Candidate")
string = string.replace("%version_type_technical%", "rc")
2024-02-04 21:36:30 +01:00
CoreTypes.VersionType.BETA:
2024-04-08 02:43:31 +02:00
string = string.replace("%version_type%", "Beta")
string = string.replace("%version_type_technical%", "b")
2024-02-04 21:36:30 +01:00
CoreTypes.VersionType.ALPHA:
2024-04-08 02:43:31 +02:00
string = string.replace("%version_type%", "Alpha")
string = string.replace("%version_type_technical%", "a")
2024-04-08 21:30:24 +02:00
_: await loggeri.crash("Invalid version type " + str(version_type))
2024-02-04 21:36:30 +01:00
# Development mode
if is_devmode(): string = string.replace("%devmode%", "Enabled")
else: string = string.replace("%devmode%", "Disabled")
# Headless mode
if config.headless: string = string.replace("%headless%", "Enabled")
else: string = string.replace("%headless%", "Disabled")
# Custom module support
if config.custom_modules: string = string.replace("%custommodules%", "Enabled")
else: string = string.replace("%custommodules%", "Disabled")
2024-02-04 21:36:30 +01:00
return string
## Returns CORE's versioning scheme into the semantic versioning scheme.[br]
2024-04-08 02:43:31 +02:00
## The first integer contains the version number, the second integer contains the version type ([code]0[/code] for alpha, [code]1[/code] for beta, [code]2[/code] for rc and [code]3[/code] for release and the last integer contains the version type number.
2024-02-04 21:36:30 +01:00
func get_version_semantic() -> Array[int]:
var version_type_int: int
match(version_type):
CoreTypes.VersionType.RELEASE: version_type_int = 3
CoreTypes.VersionType.RELEASECANDIDATE: version_type_int = 2
CoreTypes.VersionType.BETA: version_type_int = 1
CoreTypes.VersionType.ALPHA: version_type_int = 0
return [version_version, version_type_int, version_typerelease]
2024-02-12 19:56:17 +01:00
# Determines CORE's installation/base path.
2024-02-12 19:56:17 +01:00
func determine_basepath() -> bool:
if FileAccess.file_exists("res://.corebasepath"):
basepath = "res://"
elif FileAccess.file_exists("res://CORE/.corebasepath"):
basepath = "res://CORE/"
elif FileAccess.file_exists("res://addons/CORE/.corebasepath"):
basepath = "res://addons/CORE/"
else:
assert(false, "CORE is not located at 'res://CORE/', aborting initialization")
return false
return true
## Checks compatibility with the running version.
func check_godot_version() -> bool:
var version: Dictionary = Engine.get_version_info()
match(version["major"]):
4: pass
_:
2024-03-03 19:08:20 +01:00
printerr("The CORE Framework does not support Godot versions older or newer than 4.x.x")
return false
2024-02-12 19:56:17 +01:00
match(version["minor"]):
2024-03-03 19:08:20 +01:00
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.")
1: 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.")
2024-02-12 19:56:17 +01:00
2: pass
2024-03-03 19:08:20 +01:00
_:
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.")
return false
2024-02-12 19:56:17 +01:00
return true
2024-04-08 21:42:03 +02:00
## Makes sure for all log messages to be flushed and that CORE is correctly cleaned up.[br]
## Using [method SceneTree.quit] directly may cause various issues.[br]
## [b]Note: [i]Using the [code]await[/code] keyword is required for this function.[/i][/b]
func quit_safely(exitcode: int = 0) -> void:
loggeri.info("Shutting down (code " + str(exitcode) + ")")
await get_tree().create_timer(0.25).timeout
await cleanup()
get_tree().quit(exitcode)
2024-04-14 14:23:05 +02:00
# Just ignore this.
## Internal, don't call.
2024-04-14 14:23:05 +02:00
func _notification(what) -> void:
match(what):
NOTIFICATION_WM_CLOSE_REQUEST:
if config.automatic_shutdown:
await quit_safely(0)