# 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. extends Node class_name Core # Versioning ## The version number 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 ## 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", "validation" ] ## 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 ## The 'Logger' module var logger: CoreBaseModule ## The 'Miscellaneous' module var misc: CoreBaseModule ## The 'Scene Management System' module var sms: CoreBaseModule ## The 'Log UI' module. Not important for you, it just displays the log graphically :3 var logui: CoreBaseModule ## The 'Easy Request Maker' module (formerly 'Easy DownLoader') 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 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. var custom_modules_node: Node ## 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 +++ # 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(): 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 # 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. ## 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).logger = logger.get_instance(basepath.replace("res://", "") + "src/" + module + ".gd", get(module)) get(module)._initialize() # 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. ## 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") 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() ) ## Waits for all modules to fully initialize.[br] ## [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() -> 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 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(" Built-in: " + 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 !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 . 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 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 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!") if !config.custom_modules: 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: custom_modules[module]._pull_config() # Workaround if config.logger_detect_verbose_mode: logger.verbose_mode = OS.is_stdout_verbose() # +++ custom module support +++ ## Registers a new custom module. func register_custom_module(module_name: String, module_origin: String, module_class: CoreBaseModule) -> bool: loggeri.verb("Registering new custom module \"" + module_name + "\"") if !config.custom_modules: loggeri.error("Registering module failed: Custom module support is disabled.") return false if custom_modules.has(module_name): loggeri.error(stringify_variables("Registering module failed: A custom module with the name %name% already exists.", { "name": module_name })) return false # Update variables module_class.name = module_name module_class.core = self module_class.logger = logger.get_instance(module_origin, module_class) module_class.logger.framework = true # Register module custom_modules.merge({ module_name: module_class }) # Add to 'custom_modules_node' custom_modules_node.add_child(module_class) # Initialize module @warning_ignore("redundant_await") await module_class._initialize() # Update module configuration @warning_ignore("redundant_await") await module_class._pull_config() return true ## Unregisters a custom module, making it no longer available. func unregister_custom_module(module_name: String) -> void: loggeri.verb("Unregistering custom module \"" + module_name + "\"") if !custom_modules.has(module_name): loggeri.error(stringify_variables("Unregistering module failed: A custom module with the name %name% does not exist.", { "name": module_name })) return var module: Node = custom_modules[module_name] await module._cleanup() module.logger.queue_free() custom_modules_node.remove_child(module) custom_modules.erase(module_name) module.queue_free() ## 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: loggeri.diag("Getting custom module \"" + module_name + "\"") if !custom_modules.has(module_name): 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] # +++ 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.[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") 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).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 ## 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] ## - [code]%headless%[/code]: Returns the headless mode status[br] ## - [code]%custommodules%[/code]: Returns if custom module support is enabled 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): CoreTypes.VersionType.RELEASE: string = string.replace("%version_type%", "Release") string = string.replace("%version_type_technical%", "r") CoreTypes.VersionType.RELEASECANDIDATE: string = string.replace("%version_type%", "Release Candidate") string = string.replace("%version_type_technical%", "rc") CoreTypes.VersionType.BETA: string = string.replace("%version_type%", "Beta") string = string.replace("%version_type_technical%", "b") CoreTypes.VersionType.ALPHA: string = string.replace("%version_type%", "Alpha") string = string.replace("%version_type_technical%", "a") _: await loggeri.crash("Invalid version type " + str(version_type)) # 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 ## Returns CORE's versioning scheme into the semantic versioning scheme.[br] ## 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. 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. 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: printerr("CORE is not located at 'res://CORE/' or 'res://addons/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 _: 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 4.2.x.stable to ensure full compatibility.") return false return true ## 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) + ")") 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)