# 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 . ## Initializes and manages the framework. ## ## The [b]CORE Object[/b] is responsible for initializing, managing and ## serving the CORE Framework to the developer. extends Node class_name Core # Constants ## The version number const version_version: int = 1 ## The version type const version_type: CoreTypes.VersionType = CoreTypes.VersionType.BETA ## The version type number. Resets on every new version and version type. const version_typerelease: int = 4 # Modules const modules: Array[String] = [ "logger", "misc", "sms", "logui", "erm", "storage" ] ## Use this to access CORE's logging implementation. var logger: CoreBaseModule ## Use this to access various useful functions. var misc: CoreBaseModule ## Use this to access the scene management system. var sms: CoreBaseModule ## Use this to access the graphical log. Serves no importance to you (probably). var logui: CoreBaseModule ## Use this to access CORE's builtin HTTP request maker. var erm: CoreBaseModule ## Use this to access configuration and settings files easily. var storage: CoreBaseModule # Variables ## Contains CORE's load path var basepath: String ## Holds the configuration[br] ## [br] ## [b]NEVER access this yourself. To change the configuration file, use [method Core.reload_configuration] instead.[/b] var config: CoreConfiguration ## Contains all loaded custom modules. var custom_modules: Dictionary = {} ## Contains the custom modules node. var custom_modules_node: Node # Preinitialization func _init(new_config: CoreConfiguration = CoreConfiguration.new()) -> void: name = "CORE" if !check_godot_version(): return if !determine_basepath(): queue_free() custom_modules_node = Node.new() reload_configuration(new_config) initialize_modules() apply_configuration() # Initialization func _ready() -> void: inject_modules() custom_modules_node.name = "Custom Modules" add_child(custom_modules_node) # Cleanup # Particularily useful during testing to cleanup stuff, or if you want to do some stupid stuff with CORE during runtime func cleanup() -> void: logger.infof("Core", "Cleaning up") config.queue_free() 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() for module in custom_modules_node.get_children(): unregister_custom_module(module.name) remove_child(custom_modules_node) custom_modules_node.queue_free() queue_free() # Initialize modules ## Initializes all modules during the first initialization phase.[br] ## [br] ## [b]NEVER call this yourself! You will break everything and risk a crash![/b] 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)._initialize() # Inject modules into the SceneTree ## Injects CORE's builtin modules into the SceneTree.[br] ## [br] ## [b]NEVER call this yourself! You will break everything and risk a crash![/b] func inject_modules() -> void: for module in modules: add_child(get(module)) # Wait for all modules to be fully initialized ## Wait for all builtin modules to be fully initialized.[br] ## [br] ## This ensures that all of CORE's builtin modules are fully initialized and ready. ## [b]Not calling this function during startup may lead to runtime issues.[/b] func complete_init(no_success: bool = false) -> void: 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) # 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(" Builtin: " + str(modsinit_builtin)) if modsinit_custom.size() != 0: print(" Custom: " + str(modsinit_custom)) await get_tree().create_timer(1).timeout # Initialization complete await get_tree().process_frame if !no_success: logger.infof("Core", "Initialized CORE successfully") # Registers a custom module ## Registers a new custom module. func register_custom_module(module_name: String, module_origin: String, module_class: CoreBaseModule) -> bool: logger.verbf("Core", "Registering new custom module \"" + module_name + "\"") if !config.custom_modules: logger.errorf("Core", "Registering module failed: Custom module support is disabled.") return false if custom_modules.has(module_name): logger.errorf("Core", "Registering module failed: A custom module with the name \"" + module_name + "\" already exists.") return false logger.diagf("Core", "Updating variables") module_class.name = module_name module_class.core = self module_class.loggeri = logger.get_instance(module_origin) logger.diagf("Core", "Adding module to SceneTree") custom_modules_node.add_child(module_class) logger.diagf("Core", "Merging module with custom_modules") custom_modules.merge({ module_name: module_class }) logger.diagf("Core", "Initializing custom module") module_class._initialize() logger.diagf("Core", "Updating custom module configuration") module_class._pull_config() return true # Unregisters a custom module ## Unregisters a custom module, making it no longer function. func unregister_custom_module(module_name: String) -> void: logger.verbf("Core", "Unregistering custom module \"" + module_name + "\"") if !custom_modules.has(module_name): logger.errorf("Core", "Unregistering module failed: A custom module with the name \"" + module_name + "\" does not exist.") return var module: Node = get_custom_module(module_name) await module._cleanup() module.loggeri.queue_free() custom_modules_node.remove_child(module) custom_modules.erase(module_name) module.queue_free() # Returns a custom module ## Returns a loaded custom module for access. func get_custom_module(module_name: String) -> CoreBaseModule: logger.diagf("Core", "Getting custom module \"" + module_name + "\"") if !custom_modules.has(module_name): logger.errorf("Core", "Getting module failed: A custom module with the name \"" + module_name + "\" does not exist.") return null return custom_modules[module_name] # (Re-)Load configuration ## Loads a (new) configuration file and applies it to all modules. func reload_configuration(new_config: CoreConfiguration = CoreConfiguration.new()) -> void: var initialized = config != null if initialized: logger.verbf("Core", "Reloading CORE's configuration") if config != null: config.queue_free() config = new_config if is_devmode(): # Override configuration in development mode config.logger_level = CoreTypes.LoggerLevel.VERB if initialized: logger.verbf("Core", "Overrode configuration (development mode)") if initialized: apply_configuration() # Call _pull_config() functions ## Applies the newly applied configuration.[br] ## [br] ## [b]NEVER call this yourself unless you know what you are doing![/b] func apply_configuration() -> void: logger.verbf("Core", "Applying configuration") if is_devmode(): logger.warnf("Core", "The CORE Framework is in development mode. Here be dragons!") if config.headless: logger.warnf("Core", "CORE is in headless mode. Certain modules will not work as expected.") if !config.custom_modules: logger.verbf("Core", "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: logger.diagf("Core", "Updating configuration for custom module \"" + module.name + "\"") module._pull_config() # Return development mode status ## Returns if the CORE Framework is in development mode. func is_devmode() -> bool: return config.debugging and basepath == "res://" and OS.is_debug_build() # Replaces variables with human-friendly strings ## Replaces placeholders with human-friendly strings You can use the following placeholders:[br] ## - [code]%release%[/code]: Returns the release number.[br] ## - [code]%release_type%[/code]: Returns the typerelease number[br] ## - [code]%release_semantic%[/code]: Returns the result of [method Core.get_version_semantic], example [i]5.2.3[/i][br] ## - [code]%type%[/code]: Returns the release type as a word, for example [i]Release Candidate[/i][br] ## - [code]%type_technical%[/code]: Returns the release type as one or two lowercase letters, for example [i]rc[/i][br] ## - [code]%devmode%[/code]: Returns the development mode status[br] ## - [code]%headless%[/code]: Returns the headless mode status func get_formatted_string(string: String) -> String: # Version strings string = string.replace("%version%", str(version_version)) string = string.replace("%version_type%", str(version_typerelease)) 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): CoreTypes.VersionType.RELEASE: string = string.replace("%type%", "Release") string = string.replace("%type_technical%", "r") CoreTypes.VersionType.RELEASECANDIDATE: string = string.replace("%type%", "Release Candidate") string = string.replace("%type_technical%", "rc") CoreTypes.VersionType.BETA: string = string.replace("%type%", "Beta") string = string.replace("%type_technical%", "b") CoreTypes.VersionType.ALPHA: string = string.replace("%type%", "Alpha") string = string.replace("%type_technical%", "a") _: await logger.crashf("Core", "Invalid version type " + str(version_type), true) # 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") return string # Return CORE's version in the semantic versioning scheme ## Returns CORE's versioning scheme into the semantic versioning scheme.[br] ## The first integer contains the release number, the second integer contains the release 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 typerelease number. 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] # Determines CORE's installation/base path ## Determines CORE's installation/base path[br] ## [br] ## [b]Calling this function is likely to be safe, but shouldn't be done nonetheless![/b] 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 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") 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.") 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.") 2: pass _: 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 return true