From 3c1935c9e9373b6c4ae5860ee1fc3ed38858abe9 Mon Sep 17 00:00:00 2001 From: JeremyStarTM Date: Thu, 25 Apr 2024 17:25:41 +0200 Subject: [PATCH] Add custom module guide to documentation --- docs/docs/guides/_category_.json | 8 +++ docs/docs/guides/custom-modules.md | 98 +++++++++++++++++++++++++++++ docs/docs/reference/_category_.json | 2 +- 3 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 docs/docs/guides/_category_.json create mode 100644 docs/docs/guides/custom-modules.md diff --git a/docs/docs/guides/_category_.json b/docs/docs/guides/_category_.json new file mode 100644 index 0000000..f68ed3e --- /dev/null +++ b/docs/docs/guides/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Guides", + "position": 3, + "link": { + "type": "generated-index", + "description": "Guides and tutorials for specific areas." + } +} diff --git a/docs/docs/guides/custom-modules.md b/docs/docs/guides/custom-modules.md new file mode 100644 index 0000000..7c27ef6 --- /dev/null +++ b/docs/docs/guides/custom-modules.md @@ -0,0 +1,98 @@ +--- +sidebar_position: 0 +description: Shows you how to create and use a custom module. +--- + +# Creating custom modules +Custom modules in CORE are special scripts that run inside and interact with the framework. They can be accessed globally and come with many enhancements over using autoload singletons. + +## What should be packaged as a custom module? +Before you write any code, you should decide wheneter to write your code as a module or as a simple script. Generally you **not** write everything used between two classes as a module. Use custom modules for stuff you may want to use across multiple objects or need to use frequently in your codebase. One example of this is a transition script. You'll probably want to transition from one scene to another in a game quite often, writing it as a custom module makes much more sense as it can be accessed globally much easier and can be shared across codebases. + +## Bad practise +Before we finally come to some code, never write a custom module if you intend to do the following: +- Access hardcoded classes, scripts, assets, etc. from your project. Use arrays and dictionaries aswell as get(), set() and other functions for that. +- Depend on your project code. Modules are intended to be project independent and work in every environment where CORE runs. +- Never crash the entire framework on invalid user input, only crash when an internal error occurred. \ + Let's say that you have two functions: An internal `_display_scene` method and a `transition_between_scenes` method. \ + When the developer calls `transition_between_scenes` and supplies an invalid object to the method, you should call `logger.error` (and return the error) as the error comes from the developer and should be handled by the developer's code. \ + If however your `transition_between_scenes` calls `_display_scene` and supplies an invalid transition type, you should call `logger.crash` instead as your module code as an inconsistency and may not account for that. This ensures your code does not do stupid things *plus* you get a bug report or pull request from an annoyed developer :) +- Never create, instantiate or duplicate objects without removing them from the SceneTree and freeing them first. This ensures no memory leaks occur and makes your codebase a bit better. \ + If you wonder how to do this, look at CORE's logger. It frees unused `CoreLoggerInstance`s every time CORE's scheduler (`_schedule`) is executed and frees *all* `CoreLoggerInstance`s on framework cleanup (`_cleanup`). +- Not defining variable and method return types. Forcing a variable to match a `String` or a method to always return an `int` ensures your code always runs without flaws and invalid user input, saving you a few bugs while improving code quality. If a variable may contain or a method may return multiple types, use `Variant` for everything but objects and `Object` for everything that is an object. +- Not defining the containing type in for Arrays. Unless your array should hold multiple types, replace `Array` with `Array[Type]`. It also ensures no bugs occur and improves code quality. +- Not writing unit tests. Even if you don't like them (we neither), please unit test your custom module and your project(s) generally! Since we added unit tests to CORE we already identified [multiple](https://git.staropensource.de/StarOpenSource/CORE/commit/517e36e95dd65071e10ea065807ddbc4bd959f80) [bugs](https://git.staropensource.de/StarOpenSource/CORE/commit/1febf32f73510daf824280464005bc85f7eeff86) inside our framework. \ + If you are searching for a lightweight unit testing solution that is 100% compatible with CORE look try [Bessere Tests](https://git.staropensource.de/StarOpenSource/BessereTests) (made by StarOpenSource too). For reference, look at CORE's unit testing code [here](https://git.staropensource.de/StarOpenSource/CORE/src/branch/develop/tests) and config [here](https://git.staropensource.de/StarOpenSource/CORE/src/branch/develop/addons/besseretests_config.gd). +- Initialize your module using `_init` or `_ready`. This will only cause issues and break your module or even the entire framework. Please run your initialization code in `_initialize`. +- Annotate your variables using `@onready`. Please update your variables in `_initialize`, see the above note. + +## Writing an example module +Create a new folder in your project root and call it `COREmodules` (note that the script's location does not matter, it could be stored in `user://` even). In there, create a new file called `hello.gd` and remove all the pregenerated code (if there) except `extends Node`. Now you should have an empty script file with only `extends Node` sitting in front of you. Now replace it with `extends CoreBaseModule`. This marks your script as a valid CORE module and inherits it's variables and methods. +### Initializing the module +Now, create a new `void`-returning method called `_initialize` without any arguments. In there, write the following: +```gdscript +loggeri.info("Hello World!") +initialized = true # Required or the framework will wait for your module to initialize forever (see `complete_init`). +``` +What this code will do is print `Hello World!` as an informational message into the log and mark the module as initialized. +### Executing code in regular intervals +In case you don't know, the CORE Framework as something called the scheduler. It runs (by default) every minute and calls the `_cleanup` method on all loaded modules. This can be used to clean unused references or do other maintenance tasks. In our case however, we want `Hello Scheduler!` to be printed every time the scheduler does it's thing. To do that, create a new `void`-returning method called `_schedule` without any arguments and paste `loggeri.info("Hello Scheduler!")` into it. +### Uninitializing the module +Now, when the application shuts down or `quit_safely` is called, all modules will recieve a call to `_cleanup`, requesting the module to stop what it's doing and cleanup everything it did. In our case however, we want to say `Goodbye World!` on cleanup. For that to happen, create a new `void`-returning method called `_cleanup` without any arguments. Then paste `loggeri.info("Goodbye World!")` into the function. +### The full code +Now your `hello.gd` should have the following contents (except comments): +```gdscript +extends CoreBaseModule + +# Initialization +func _initialize() -> void: + # Print 'Hello World!' + loggeri.info("Hello World!") + # Mark the module as initialized + initialized = true + +# Maintenance tasks +func _schedule() -> void: + # Print 'Hello Scheduler!' + loggeri.info("Hello Scheduler") + +# Cleanup module +func _cleanup() -> void: + # Print 'Goodbye World!' + loggeri.info("Goodbye World!") +``` + +## Registering your custom module +Before you can use your custom module you need to register it. To do that, simply execute this code in your initialization script: +```gdscript +# Creates new CoreBaseModule object and assigns it to a new variable +var custom_module: CoreBaseModule = CoreBaseModule.new() +# Loads a script located at `res://COREmodules/hello.gd` and sets it as the script for 'custom_module' +custom_module.set_script(load("res://COREmodules/hello.gd")) +# Registers the custom module +core.register_custom_module("hello", "res://COREmodules/hello.gd", custom_module) +# Wait for all built-in and custom modules to fully initialize +await core.complete_init() + +# Wait one minute and five seconds for the scheduler message to appear and shut down, remove this when you are actually adding a custom module to your project. +await get_tree().create_timer(65).timeout +await core.quit_safely() +``` + +## Testing your custom module +Now run your project and you should see something like this somewhere in your console: +```plain +[14:52:11] [INFO res://COREmodules/hello.gd] Hello World! +``` +Now wait for about one minute and you should see this message in your console: +```plain +[14:53:11] [INFO res://COREmodules/hello.gd] Hello Scheduler! +``` +... which should be followed by this a few seconds later: +```plain +[14:53:16] [INFO src/core.gd] Shutting down (code 0) +[14:53:17] [INFO src/core.gd] Cleaning up +[14:53:17] [INFO res://COREmodules/hello.gd] Goodbye World! +``` + +If this matches your log output, then you've successfully created your own custom module. You can now modify the code of the custom module however you like. But before that, please read the [CoreBaseModule reference](/reference/basemodule). diff --git a/docs/docs/reference/_category_.json b/docs/docs/reference/_category_.json index 048423f..c6b1293 100644 --- a/docs/docs/reference/_category_.json +++ b/docs/docs/reference/_category_.json @@ -1,6 +1,6 @@ { "label": "Reference", - "position": 3, + "position": 4, "link": { "type": "generated-index", "description": "CORE Framework reference"