Skip to content

4.0.0-RC6 - Blizzard

Pre-release
Pre-release
Compare
Choose a tag to compare
@derklaro derklaro released this 20 Jan 20:12
· 182 commits to beta since this release
ba9d666

Update 4.0.0-RC6 - Blizzard

We are pleased to announce the sixth release candidate for CloudNet 4.0. This release mainly focuses on the migration from static instance usages to dependency injection - internally as well as for external api consumers. Please see the dedicated information section for dependency injection below for further info. We urge all users to install the update, as we will no longer provide support for RC5. Users who want to switch from 3.4.X to 4.0 can find instructions in the release information for RC1.

Cheers!
(Please remember, CloudNet is provided as-is - we are not responsible for data loss or corruption. You are encouraged to back up your files before any updates!)

Changelog

🐛 Fixes

  • Fixed an issue that SyncProxy configurations which were applied to velocity proxy instances could not be applied to Bungeecord or Waterdog proxies as-is
  • Fixed an issue that caused wildcard characters to not be escaped correctly when using the mysql database
  • Fixed an issue that caused an exception when the profile which should be assigned to a npc cannot be resolved
  • Fixed an issue with invalid npc info items which were causing exceptions
  • Fixed an issue which caused a player to be connected to all matching services when calling any PlayerExecutor#connect method rather than the best matching service
  • Fixed an issue that caused a start loop when using the smart module with auto stop enabled due to an invalid check if enough services are running to satisfy the min service count
  • Fixed an exception thrown when a channel message query got no response
  • Fixed an issue that caused expired permissions to not be removed from a player
  • Fixed an issue with SyncProxy permission tablist placeholders that were overridden by equivalently named bridge placeholders. The new placeholders are now prefixed with perms_group_

✨ Improvements

  • Add support for 1.19.3
  • Migrated from the usage of static instances to dependency injection
  • Improved accessibility to information in CommandPreProcess and CommandPostProcess event
  • Renamed the ColouredLogFormatter to ColoredLogFormatter and ConsoleColor.toColouredString to ConsoleColor.toColoredString to keep the spelling of color consistent

🔗 Links

Dependency Injection

❓ Why DI

There are a few reasons why we deided to go for dependency injection. The top three include:

  1. Improved testability: DI allows for more easily replacing dependencies with mock objects in unit tests, which can help to improve the quality of the code and reduce bugs.
  2. Increased flexibility: DI makes it easier to change the implementation of a dependency without affecting the code that uses it. This allows for more easily making changes and adapting the codebase to new requirements.
  3. Loose coupling: DI allows for a more loosely-coupled architecture, which makes it easier to change or extend the codebase. This can lead to more maintainable code and also facilitates reusability and scalability of the code.

💡 What to note when using DI

What is DI?

Dependency injection is a design pattern that allows objects to be constructed without needing to know how their dependencies are created. A dependency is an object that another object relies on in order to function properly. In traditional software development, objects are responsible for creating and managing their own dependencies. However, with dependency injection, the responsibility of creating and managing dependencies is shifted to a separate component, known as an injector.

The injector is responsible for providing the necessary dependencies to objects at runtime. This is done by injecting the dependencies directly into the objects that need them, rather than having the objects create or manage the dependencies themselves. By doing so, the objects become less tightly coupled and more flexible, making it easier to change or replace dependencies without affecting the rest of the application.

Constructor injection is the most common form of dependency injection, where dependencies are passed in through the constructor of a class. The class is defined with a constructor that takes the dependencies as arguments, and the injector is responsible for providing these dependencies when the class is instantiated. The target constructor to inject is detected by using either of:

  1. The constructor which is annotated as @Inject (⚠️ Make sure to use the correct inject annotation. It should either be dev.derklaro.aerogel.Inject or jakarta.inject.Inject).
  2. The constructor which takes no arguments.
  3. The constructor which takes all record components as its arguments (only applied for record classes).

When the injector constructed the type completely member injection is executed. Member injection will be done on

  1. all fields that are annoted as @Inject (Note that final fields might get ignored from injection)
  2. all methods that are annotated as @Inject will be executed. These methods are allowed to take arguments.
  3. all methods that are annotated as @PostConstruct will be executed. These methods are not allowed to take arguments.

The injection order of injectable methods and post construct listeners can be constrolled with the @Order annotation.

A quickstart guide for dependency injection

  1. Identify dependencies: Identify the dependencies that need to be injected into an object and create interfaces or abstract classes for them.
  2. Use constructor injection: Constructor injection is the most common form of dependency injection, where dependencies are passed in through the constructor of a class.
  3. Avoid tight coupling: Dependency injection allows for loose coupling between objects, making it easier to change or replace dependencies.
  4. Use interfaces: Use interfaces or abstract classes to define dependencies, rather than concrete implementations, to make it easier to change or replace them.
  5. Understand the difference between singleton and prototype scope: The singleton scope will give a single instance of an object throughout the application, whereas the prototype scope will give a new instance of an object every time it is requested.
  6. Use annotations: Annotations are used to mark a class, constructor or a method, in order to let the framework know that it should be injected and how. For example the @Inject annotation markes constructors and classes that should be injected and the @Singleton annotation lets aerogel know that only a single instance of the annotated type should be constructed.
  7. Avoid circular dependencies: Be careful when defining dependencies to avoid circular dependencies, which can cause issues with object creation and injection. This only applies if the requested type is not an interface and therefore not proxyable.

⛅ How did we implement it in CloudNet

Because of the little flexibility and many outside influences we have due to the supported Minecraft server/proxy implementation, we had to implement DI to be as flexible as possible.

For this reason, some decisions had to be made that would not have been a problem in a normal standalone application:

  • We use Aerogel as our DI library. This decision was made for the most part because:
    • Bi-directional proxies are supported, unlike (for example) Google Guice. This means that if a circular injection request arises, and (e.g.) the parameter of a constructor cannot be proxied, then the type from which the circular reference arises is tried to be proxied. See the Aerogel documentation for more information on how the injection tree works.
    • The library supports "specified" injectors. These are injectors that have certain types bound to them that do not get into the parent injector (e.g. useful for plugins: the description of a plugin should not be passed to the other plugin instances).
    • The library supports automated binding of classes to interfaces and factory methods. More information about this can be found below.
  • The complete creation of plugin information and main classes is now abstracted. This allows us to inject dependencies directly into the main classes of plugins without having to make an intermediate step (e.g. through another extra class).
  • More annotations have been added to extend the previously mentioned Binding System for Platforms. Thus, multiple implementations for (e.g.) an interface can be created, which are bound (and later injected) to the interface based on the platform. More information about this is also available below.

🔌 Support for Plugins

Support for plugins can be achieved with the platform inject api. This generates classes during compile time based on annotations added to classes (a list of these annotations is below). It is important that the dependency for the annotation processor is only available at compile time and not at runtime:

// adds the api for platform inject, do not compile!
compileOnly 'eu.cloudnetservice.cloudnet:platform-inject-api:4.0.0-RC6'
// adds the annotation processor for the inject-api annotations
annotationProcessor 'eu.cloudnetservice.cloudnet:platform-inject-processor:4.0.0-RC6'

The supported annotations

  • @PlatformPlugin: This annotates the main class of the plugin. Due to the abstraction of CloudNet extended/implemented, the annotated class no longer implements an implementation-specific class (such as JavaPlugin) but always implements the interface PlatformEntrypoint. The following settings can be made via the annotation:
    • platform: The name of the platform for which the annotated class is the entry point. Supported are currently: bukkit, bungeecord, fabric, minestom, nukkit, sponge, velocity & waterdog. Note that for the platform a plugin info file will be generated as well (for example the plugin.yml for bukkit). A template file can be created in the resources directory with the name of the output file (see below for options) suffixed with .template. All configurations made in the template file will be copied to the final file and will not get overridden.
    • name: The name of the plugin. CloudNet automatically takes care that the name complies with the platform's policies (and adjusts the name if necessary) and also generates a plugin ID based on the name (if necessary).
    • version: The version of the plugin.
    • pluginFileNames: Sets the output file name for the plugin. By default, the file names used by CloudNet are used, which can be customized during copying to the target service (e.g. plugin.bungeecord.yml). In the array you can specify the names of the files where the plugin info for the platform should be written. Folders are also supported, these are simply specified via slash (e.g. myFolder/plugin.yml).
    • api: An optional api version that is required for the plugin to run. This setting can be interpreted differently or even ignored based on the target platform.
    • description: An optional description of the plugin.
    • homepage: An optional url to the homepage of the plugin.
    • authors: An optional array of the plugin authors.
    • providesScan: An optional set of patterns which should be considered when searching for @ProvidesFor annotations. By default the same package in which the plugin main class is located in will be used. Each entry in the given array can be in the formts:
      • <packageName>: In this case the given name will be taken and all classes and sub-packages in the given package will be scanned
      • <type>:<packageName>: The name is used depending on the given type. The type can either be:
        • r, regexp, pattern: the given package name is parsed as a pattern.
        • p, plain: the given package name is used in a literal way and all classes in the given and sub packages will be scanned.
        • g, glob: the given package name is parsed as a simplified glob. The only supported glob chars are: *, ?, . and \.
    • commands: An optional array of commands that is provided by the plugin. Depending on the platform the information can be used in different ways or even be igored.
    • dependencies: An optional array of plugin-level dependencies. The given information may not be used based on the plugin.
    • externalDependencies: An optional array of external dependencies that are required for the plugin to run. Based on the platform the information might be ignored.
  • @ProvidesFor: marks an implementation for the given type(s) which should only be present on the specified platform. The following settings can be made via the annotation:
    • platform: The platform on which the binding should be present.
    • types: The types to which the annotated class should get bound (Note that there will be no check if the type is actually assignable to the annotated type).
    • bindGenericSupertypes: If enabled the directly extended superclass and implemented interfaces will be scanned if they contain one of the types given via the types setting which is generic. In that case the generic version will be bound based on the extending declaration (for example if in the type array Map.class is specified and the class implements Map<String, String> then the map implementation with type parameters will be bound to the annotated type as well).
    • bindWildcardTypeOfParameterizedTypes: If enabled all generic, parameterized types will be bound fully wildcarded as well (for example if the class implements Map<String, Map<String, String>> the type Map<?, ?> will be bound to the type as well).
  • @ConstructionListener: The annotation can be added to the main class of a plugin (the same class as annotated with @PlatformPlugin) and specifies a single class will should get called when the plugin instance is created by the platform. Note that the target class must declare a constructor which takes the platform plugin data (which can vary based on the platform) as it's only argument. Only the constructor in the target class is called, all other steps are left to the constructor to do.

Annotations to use with @PlatformPlugin

The following annotations are used in the PlatformPlugin annotation and describe various configuration options for the info generation for each platform. Depending on the target platform, some of the information provided to the annotation might get dropped, or the complete annotation is ignored if unused:

  • @Command: Allows to specify a command which is provided by the plugin.
  • @Dependency: Allows to specify a dependency on another plugin.
  • @ExternalDependency: Allows to specify an external dependency which is required for the plugin in order to run.
    • @Repository: Sets the repository of the external dependency. Some platform are only allowing dependencies to get downloaded from maven central, in that case the information provided to the annotation is ignored.

An example plugin class might look like this:

@Singleton
@PlatformPlugin(
  platform = "velocity",
  name = "TestingPlugin",
  version = "1.0",
  authors = "derklaro",
  description = "Just a plugin to test some things",
  dependencies = @Dependency(name = "CloudNet-Bridge")
)
public final class VelocityPluginEntryPoint implements PlatformEntrypoint {

  @Inject
  public VelocityPluginEntryPoint(
    @NonNull @Named("plugin") Object pluginInstance, // the type of this varies based on the platform
    @NonNull ModuleHelper moduleHelper,
    @NonNull EventManager eventManager
  ) {
    // assign fields
  }

  @Override
  public void onLoad() {
    System.out.println("Hello from my test plugin!");
  }

Which types are bound for which platform by default?

The InjectionLayer for the plugin can be injected by using the layer type and requesting an instance called plugin.

Bukkit

⚠️ Only types that are available in 1.8 are bound by default, if other instances are needed it is recommended to inject the server instance and get the instance from there

Plugin Type

  • Plugin, PluginBase and JavaPlugin

Platform Types

  • Server
  • BukkitScheduler
  • PluginManager
  • ServicesManager
  • ScoreboardManager
Bungeecord

Plugin Type

  • Plugin

Platform Types

  • ProxyServer
  • ProxyConfig
  • TaskScheduler
  • PluginManager
Fabric

Fabric has no bindings by default as there is nothing to bind.

Minestom

Plugin Type

  • Extension

Platform Types

  • ComponentLogger
  • TagManager
  • TeamManager
  • BiomeManager
  • BlockManager
  • RecipeManager
  • BossBarManager
  • CommandManager
  • PacketProcessor
  • InstanceManager
  • ExceptionManager
  • ExtensionManager
  • BenchmarkManager
  • SchedulerManager
  • ConnectionManager
  • GlobalEventHandler
  • AdvancementManager
  • DimensionTypeManager
  • PacketListenerManager
Nukkit

Plugin Type

  • Plugin and PluginBase

Platform Types

  • Server
  • CommandMap
  • ServerScheduler
  • PluginManager
  • ServiceManager
  • CraftingManager
  • ResourcePackManager
Sponge

⚠️ Only types that are available in API 8 are bound by default

Plugin Type

  • PluginContainer

Platform Types

  • Scheduler (two variants: one named sync (for the sync scheduler) and one name async (for the async scheduler))
  • Game
  • Platform
  • SqlManager
  • DataManager
  • EventManager
  • ConfigManager
  • PluginManager
  • ChannelManager
  • BuilderProvider
  • FactoryProvider
  • MetricsConfigManager
  • ServiceProvider.GameScoped
  • MapStorage
  • UserManager
  • WorldManager
  • Server
  • RecipeManager
  • TeleportHelper
  • CommandManager
  • PackRepository
  • ResourceManager
  • CauseStackManager
  • GameProfileManager
  • GameProfileProvider
  • ServiceProvider
  • ServiceProvider.ServerScoped
Velocity

Plugin Type

  • PluginContainer
  • Object named plugin which is the plugin main class instance

Platform Types

  • ProxyServer
  • Scheduler
  • EventManager
  • PluginManager
  • CommandManager
  • ChannelRegistrar
Waterdog

Plugin Type

  • Plugin

Platform Types

  • ProxyServer
  • MainLogger
  • CommandMap
  • PackManager
  • LangConfig
  • EventManager
  • PlayerManager
  • ServerInfoMap
  • PluginManager
  • WaterdogScheduler
  • ConfigurationManager

🔌 Support for Modules

The CloudNet module system (whether on the wrapper or the node) has full support for dependency injection. The module's main class can have dependencies injected into the constructor, and all module tasks have the ability to inject dependencies. No further configuration is required.

An example module task might be:

  @ModuleTask(order = 127, event = ModuleLifeCycle.STARTED)
  public void initModule(
    @NonNull HttpServer httpServer,
    @NonNull ServiceRegistry serviceRegistry,
    @NonNull DataSyncRegistry dataSyncRegistry,
    @NonNull @Named("module") InjectionLayer<?> injectionLayer // injects the injection layer of the module, this is always named "module"
  ) {
  }

Commands

The node command system allows injection into command processing methods as well. This does not include suggestion processors or parameter parsers.

🔌 Support for Event Listeners

Event Listeners are allowed to take parameters via injection as well. The main catch here is, that the first parameter of an event listener method still needs to be the event which the method wants to get notified about. All other parameters are looked up when the listener should be invoked. An example listener that takes arguments:

  @EventListener
  public void handleChannelMessage(
    @NonNull ChannelMessageReceiveEvent event,
    @NonNull EventManager eventManager,
    @NonNull PermissionManagement permissionManagement
  ) {
    // do stuff
  }