18 Home
Henri Schubin edited this page 2023-12-31 17:13:17 +02:00

Ascension Developer Guide

Code organization & structure

Common Modules (most important)

  • :api
    • API code (mostly interfaces, some implementations)
  • :common
    • Contains the bulk of all DiscordSRV code

Platform modules (eg. :bukkit, :bungee etc.)

  • :<platform>
    • Platform specific code
  • :<platform>:loader (rarely need to be touched)
    • For jarinjar loading, explained later in this guide

Misc. modules (rarely need to be touched)

  • :common:common-api
    • This contains the Slf4j implementation which needs to be included in loaders but doesn't need to be accessed by API users
  • :i18n
    • Standalone java app to generate a file for Crowdin from the platform's Configuration's

Packages

  • Classes and interfaces are organized into packages based on what they do or what they are related to
    • Examples:
      • Group sync goes in groupsync
      • Message forwarding from Minecraft -> Discord goes in messageforwarding.minecrafttodiscord
      • SQL goes in storage
      • Channel locking goes in channel
      • etc.
    • Some 'exceptions':
      • Configs are kept under config.<config name> (in each module)
      • Events are under event.events (in :api)
      • Module types are under module.type (in :api and :common)
      • Integrations are grouped together into integration (in each module)

Buildscript (Gradle)

Dependencies are using a version catalogue which is defined in the settings.gradle (Gradle docs)

Some common parts Gradle script stuff is in the buildscript/ folder (including most relocations)

Event bus

To simplify development for api users and DiscordSRV itself, DiscordSRV's event bus (DiscordSRV#eventBus) is used for listening to DiscordSRV's own events as well as JDA's events.

  • The JDA event listener system is blocked in favor of DiscordSRV's own event bus

Additionally the :api module includes a annotation processor which will cause compile time errors if the @Subscribe annotation used by the event bus is used incorrectly

Modules

Module is a type for easily building features and plugin integrations.

Modules...

  • can request gateway intents, cache flags and member caching policies (via methods)
  • are enabled and disabled when DiscordSRV reloads based on the module's isEnabled method's return value
    • triggers Module#enable, Module#disable and Module#reload
  • are automatically subscribed to the event bus

Modules can be used for building plugin integrations such as permissions providers,

  • The DiscordSRV class has methods to lookup modules by their type, for example: PermissionProvider permProvider = discordSRV.getModule(PermissionProvider.class);
  • Modules can specify their priorities for lookup via Module#priority(Class)

Config

DiscordSRV uses Configurate object serialization. Config translation is handled by taking the default config written in English and dumping the useful parts into a file that can be imported into Crowdin (:i18n module) and then loaded back into DiscordSRV (TranslatedConfigManager).

DiscordSRV has some useful extra's such as serialization for SendableDiscordMessage.Builder and the following annotations,

  • @DefaultOnly controls parts of the config that will not be added back into the config if they are removed
  • @Order for precise control over options when inheritance prevents ordering the fields as desired
  • @Untranslated to specify options and/or comments which shouldn't be translated

Configs are stored under each module in the config.<config name> package

  • Where <config name> is usually main, connection or messages
  • The other sub-packages are used to configure Configurate

Channels config & Channel name priority

To avoid conflicting channel names, the GameChannelLookupEvent will be used to determine which plugin 'owns' a given channel name, when a plugin isn't provided at the time of looking up a config value. Unless the plugin is specified in the config with a semicolon between the plugin name and channel name (eg. discordsrv:global) in which case there can be no conflict.

Example configuration

channels:
  global: # <-- This will be used for the first integration to respond to the GameChannelLookupEvent with a "global" channel
    ...
  "discordsrv:global": # <-- This will be used for DiscordSRV's global channel exclusively
    ...

The channels option in the main config works like this:

  • The value is equivalent to Map<String, BaseChannelConfig>
  • Keys other than default are of type ChannelConfig which also contains configuration for channel ids and threads
  • If a given key doesn't have a value for the requested option it will be looked up from the default key instead

To access the channels config option:

  1. DiscordSRV#channelConfig instead of DiscordSRV#config
  2. ChannelConfigHelper#get(GameChannel) or ChannelConfigHelper#resolve(DiscordMessageChannel) or ChannelConfigHelper#resolve(String)

If access to channel ids / threads is needed, check if BaseChannelConfig is a instance of IChannelConfig, cast to that if it is, and use the methods from that interface.

Placeholder service

To make DiscordSRV as configurable as possible, and to avoid the pains of converting between Minecraft and Discord formatting.

Some points of how the placeholder service works,

  • works by giving in a input string and 'context' which will be used by placeholders depending on their requirements
  • 'context' can be more than just Minecraft Players, mainly allows using Discord users and server members as context
  • includes some special party ticks such as OR on placeholders eg. %player_display_name|player_name% (%player_display_name% or %player_name% if the display name isn't present)
  • recursive placeholders out of the box %awesomelevels_player_level_{linked_player_name}%
  • hooks directly into external plugins such as PlaceholderAPI
  • allows declaring placeholders directly in types, example:
@PlaceholderPrefix("player_")
public interface Player {

    @Placeholder("name") // This will be %player_name% due to the PlaceholderPrefix
    String username();
...

Logging

In order to deal with dependencies which use Slf4j, DiscordSRV has it's own implementation of it, that is relocated to com.discordsrv.dependencies.org.slf4j(.impl) which redirects log messages to DiscordSRV's own logger as shown in the below illustration.

DiscordSRV's own logging happens through DiscordSRV#logger and Modules and other types should use the NamedLogger proxy to specify the name of the component the log messages are for. This is the replacement for the Debug categories in DiscordSRV1.

All log messages flow through DiscordSRVLogger, which...

  • Filters & cleans up log messages (from dependencies)
  • Stores them as log files for debugging (with logger names)
    • Keeps 3 files, rolling over when DiscordSRV is initialized
  • And forwards them to the platform's logger (without logger names)

Illustration of the above

The main class

The main class structure, from top to bottom

  • DiscordSRVApi (interface)

    • In the :api module
    • Exposes limited methods for API users (using API types)
  • DiscordSRV (interface)

    • In the :common module
    • "Upgrades" some of the methods in DiscordSRVApi to implementation types (eg. IProfileManager -> ProfileManagerImpl)
    • Exposes internal methods that aren't platform dependent
    • Nearly everything is accessible from here, logger(), config(), jda(), httpClient(), placeholderService() etc.
    • The primary type that is passed to (almost) everything via dependency injection
      • Avoids the nasty generics of AbstractDiscordSRV
  • AbstractDiscordSRV<C, CC> (abstract class)

    • In the :common module
    • C and CC are the configuration types which will be specified by the PlatformDiscordSRV class
    • Implements DiscordSRV and DiscordSRVApi methods which don't rely on platform code
    • The 'true' main class of DiscordSRV
  • ServerDiscordSRV<C, CC> and ProxyDiscordSRV<C, CC> (abstract classes)

    • In the :common module
    • Contains server or proxy specific code
      • for example: server switch messages are only required on the proxy so registering them happens in ProxyDiscordSRV
  • PlatformDiscordSRV (BukkitDiscordSRV, BungeeDiscordSRV etc.) (regular classes)

    • In the platform's module (:bukkit, :bungee etc.)
    • The platform implementation
    • Dependency injected into plugin hooks and platform specific implementations

Illustration of the above:

JarInJar loading

Very similar to LuckPerms. Required to safely load dependencies at runtime on platforms that don't expose their classloader to adding new urls (Velocity & Fabric do, and this section does not apply to them)

The :<platform>:loader module's output file is installed on the server and it contains the .jarinjar (avoids shadow behavior of flattening jars) from the :<platform> module's output.

The :<platform>:loader output jar contains,

  • the :api module's code and it's dependencies (as code from the :<platform> module and dependencies loaded at runtime cannot be accessed by other plugins),
  • the bare minimum of code to launch the :<platform> module's .jarinjar.

Illustration of the above