Skip to content
Ravis96 edited this page Dec 6, 2025 · 6 revisions

Dream-Platform

This documentation outlines the development standards and architecture for projects based on Dream-Platform.

The architecture focuses on Modularity, Dependency Injection (DI), and Configuration Management.

Find documentation example project structure here.


0. Quick Start (Recommended)

To avoid manual setup and ensure your project follows these standards, always start by generating your repository from the template.

  1. Go to Dream-Template.
  2. Click the green "Use this template" button.
  3. This will generate a complete, ready-to-use project structure containing:
    • Pre-configured Gradle build script (Kotlin DSL).
    • ShadowJar setup for bundling dependencies.
    • Main class with Dream-Platform extension.
    • Basic configuration and architecture examples.

1. Project Structure

The project is typically divided into specific packages to separate concerns. Below is an example structure based on our project:

Package Description
cc.dreamcode.project Main Package: Contains the main plugin class.
.config Configuration: Classes extending e.g. Okaeri-Config.
.model (e.g., .profile) Data Models: Objects representing database entities (e.g., Profile).
.controller Logic: Listeners that handle business logic.
.command Commands: Command executors using e.g. dream-command.
.menu GUIs: Inventory menus using e.g. dream-menu.
.task Tasks: Runnables and scheduled tasks.

2. Dependency Injection

Dream-Platform uses Okaeri-Injector for DI. The standard practice is Constructor Injection.

Using Lombok

Instead of writing boilerplate constructors, we use Lombok's @RequiredArgsConstructor.

  • Step 1: Declare dependencies as private final.
  • Step 2: Annotate the class with @RequiredArgsConstructor(onConstructor_ = @Inject).
  • Result: The injector automatically passes the required instances when creating the component.

The Service injects the Repository and Cache. It uses @PostConstruct to populate the cache when the plugin starts.

Example (Service Component):

@RequiredArgsConstructor(onConstructor_ = @Inject)
public class ProfileService {

    private final DreamLogger logger;        // Injected by Platform
    private final ProfileCache profileCache;       // Injected Component
    private final ProfileRepository profileRepository; // Injected Component

    /**
     * This method runs automatically after dependencies are injected.
     * Use it to load data, start tasks, or register internal handlers.
     */
    @PostConstruct
    public void init() {
        long startTime = System.currentTimeMillis();
        this.logger.info("Loading profiles from database...");

        // Logic: Fetch all profiles from DB -> Register into Cache
        this.profileRepository.findAll()
            .forEach(this.profileCache::add);

        this.logger.info("Loaded profiles in " + (System.currentTimeMillis() - startTime) + "ms.");
    }

    // Find existing profile or create new if not found
    public Profile findOrCreate(@NonNull Player player) {

        final Optional<Profile> profileOptional = this.profileCache.findProfile(player.getUniqueId());

        if (profileOptional.isPresent()) {
            return profileOptional.get();
        }

        final Profile profile = this.profileRepository.findOrCreate(player.getUniqueId(), player.getName()); // Async load recommended in production
        this.profileCache.add(profile);
        return profile;
    }

    // Business logic example
    public void addPoints(@NonNull Profile profile, int amount) {
        profile.setPoints(profile.getPoints() + amount);
        this.profileRepository.save(profile); // Async save recommended in production
    }
}

3. Configuration with Notices

To use advanced messaging (Chat, Actionbar, Titles), we use BukkitNotice in our configuration class.

Dream-Platform default supports Okaeri-Config for class based configs and objects serialization/deserialization.

Example: MessageConfig.java

@Configuration(child = "message.yml")
public class MessageConfig extends OkaeriConfig {

    @Comment("Message sent when points are added. Placeholders: {player}, {amount}")
    @CustomKey("points-added-notice")
    public BukkitNotice pointsAddedNotice = BukkitNotice.chat("&aAdded &e{amount} &acoins to &f{player}&a.");

    @Comment("Message when player is not found")
    @CustomKey("profile-not-found-notice")
    public BukkitNotice profileNotFoundNotice = BukkitNotice.chat("&cPlayer not found!");

    @Comment("Join message. Placeholders: {player}")
    @CustomKey("join-notice")
    public BukkitNotice joinNotice = BukkitNotice.chat("&7Player &f{player} &7joined the server.");
}

4. Commands

Commands are defined using annotations. Dependencies like ProfileService and PluginConfig are injected via constructor.

Example: PointsCommand.java

@Command(name = "points", aliases = {"p", "coins"})
@RequiredArgsConstructor(onConstructor_ = @Inject)
public class PointsCommand implements CommandBase {

    private final ProfileCache profileCache;
    private final ProfileService profileService;
    private final MessageConfig config;

    @Executor(path = "add", description = "Adding some points.")
    @Completion(arg = "playerName", value = "@players")
    void addPoints(CommandSender sender, @Arg String playerName, @Arg int amount) {

        Optional<Profile> profileOptional = this.profileCache.findProfileByName(playerName);

        if (!profileOptional.isPresent()) {
            // Sending simple notice without placeholders
            this.config.userNotFoundNotice.send(sender);
            return;
        }

        Profile profile = profileOptional.get();
        this.profileService.addPoints(profile, amount);

        // Sending notice with placeholders using MapBuilder
        this.config.pointsAddedNotice.send(sender, new MapBuilder<String, Object>()
                .put("player", playerName)
                .put("amount", amount)
                .build());
    }
}

5. Controllers (listeners)

The same pattern applies to Listeners. We inject the config and use BukkitNotice.

Example: PlayerController.java

@RequiredArgsConstructor(onConstructor_ = @Inject)
public class PlayerController implements Listener {

    private final ProfileService profileService;
    private final MessageConfig messageConfig;

    @EventHandler
    public void onPlayerJoin(PlayerJoinEvent event) {

        final Profile profile = this.profileService.findOrCreate(event.getPlayer());

        // Send welcome message using Dream-Notice
        this.messageConfig.joinNotice.send(event.getPlayer(), new MapBuilder<String, Object>()
                .put("player", profile.getName())
                .build());
    }
}

6. Schedulers

Tasks are used for repeating actions (e.g., annoucenemts).

Example: AnnounceTask.java

@Scheduler(delay = 1200L, interval = 6000L) // Delay: 1 min, Interval: 5 min (in ticks)
@RequiredArgsConstructor(onConstructor_ = @Inject)
public class AnnouncerTask implements Runnable {

    private final MessageConfig messageConfig;

    @Override
    public void run() {
        // Logic: Send announcement to all players
        Bukkit.getOnlinePlayers().forEach(player -> {
            this.messageConfig.announceNotice.send(player);
        });
    }
}

Advanced Tasks (Okaeri-Tasker)

While basic tasks are handled via @Scheduler, for complex operations we highly recommend exploring Okaeri-Tasker.

It allows creating Task Chains, which make it incredibly easy to switch between asynchronous processing (e.g., database calls) and synchronous execution (e.g., modifying the Bukkit world) in a single, readable flow:

// Example of a Chain
this.bukkitTasker.newChain()
    .supplyAsync(() -> this.database.loadData()) // 1. Run heavy logic async
    .acceptSync(data -> {
        // 2. Return to Main Thread to apply changes
        player.sendMessage("Data loaded: " + data);
    })
    .execute();

Check the official repository for more details on Chains and Safety features.


7. Menu System & Serialization

For advanced menu handling, we use the Setup + Holder pattern combined with custom Serialization. This allows us to define complex items and layouts directly in config.yml.

A. Custom Data Objects & Serialization

To create dynamic menus where specific items are defined in the configuration, we first create a data object and a serializer.

1. Data Object (MenuItem.java) A simple POJO to hold item data.

@Data
public class MenuItem {
    private final String displayName;
    private final ItemStack itemStack;
    private final int slotInMenu;
}

2. Serializer (MenuItemSerializer.java) To save/load MenuItem objects in the configuration, we implement ObjectSerializer from Okaeri-Config.

To make the MenuItemSerializer work, we must register it in the Main class.

public class MenuItemSerializer implements ObjectSerializer<MenuItem> {
    @Override
    public boolean supports(@NonNull Class<? super MenuItem> type) {
        return MenuItem.class.isAssignableFrom(type);
    }

    @Override
    public void serialize(@NonNull MenuItem object, @NonNull SerializationData data, @NonNull GenericsDeclaration generics) {
        data.add("display-name", object.getDisplayName());
        data.add("item", object.getItemStack());
        data.add("slot-in-menu", object.getSlotInMenu());
    }

    @Override
    public MenuItem deserialize(@NonNull DeserializationData data, @NonNull GenericsDeclaration generics) {
        return new MenuItem(
                data.get("display-name", String.class),
                data.get("item", ItemStack.class),
                data.get("slot-in-menu", Integer.class)
        );
    }
}

B. Configuration (PluginConfig.java) We define the menu layout using BukkitMenuBuilder and a list of our custom MenuItem objects.

@Configuration(child = "config.yml")
public class PluginConfig extends OkaeriConfig {

    @CustomKey("menu-builder")
    public BukkitMenuBuilder menuBuilder = new BukkitMenuBuilder("Menu title", 3, new MapBuilder<Integer, ItemStack>()
            .put(MenuUtil.countSlot(3, 5), ItemBuilder.of(XMaterial.RED_DYE.parseItem())
                    .setName("&cClose menu")
                    .toItemStack())
            .build());

    @CustomKey("menu-close-item-slot")
    public int menuCloseItemSlot = MenuUtil.countSlot(3, 5);

    @CustomKey("menu-custom-items")
    public List<MenuItem> menuItemList = new ListBuilder<MenuItem>()
            .add(new MenuItem("Test item", XMaterial.DIAMOND.parseItem(), 0))
            .build();
}

C. Menu Logic (Setup & Holder)

1. Menu Setup (ExampleMenu.java) This class acts as a factory. It constructs the inventory instance based on the configuration.

@RequiredArgsConstructor(onConstructor_ = @Inject)
public class ExampleMenu implements BukkitMenuSetup {

    private final PluginConfig pluginConfig;

    @Override
    public BukkitMenu build() {
        // 1. Build base layout from config
        final BukkitMenuBuilder menuBuilder = this.pluginConfig.menuBuilder;
        final BukkitMenu bukkitMenu = menuBuilder.buildEmpty();

        // 2. Set static items from builder (with logic)
        menuBuilder.getItems().forEach((slot, item) -> {
            if (this.pluginConfig.menuCloseItemSlot == slot) {
                bukkitMenu.setItem(slot, ItemBuilder.of(item).fixColors().toItemStack(), event ->
                        event.getWhoClicked().closeInventory());
                return;
            }
            bukkitMenu.setItem(slot, ItemBuilder.of(item).fixColors().toItemStack());
        });

        // 3. Set custom serialized items
        this.pluginConfig.menuItemList.forEach(menuItem ->
                bukkitMenu.setItem(menuItem.getSlotInMenu(), ItemBuilder.of(menuItem.getItemStack())
                        .fixColors()
                        .toItemStack(), event -> {
                    final HumanEntity player = event.getWhoClicked();
                    player.sendMessage("Clicked " + menuItem.getDisplayName());
                }));

        return bukkitMenu;
    }
}

2. Menu Holder (ExampleMenuHolder.java) This class manages the active menu instance. It allows for reloading the menu without restarting the server.

@RequiredArgsConstructor(onConstructor_ = @Inject)
public class ExampleMenuHolder {

    private final ExampleMenu exampleMenu;
    private BukkitMenu menu;

    public void update() {
        if (this.menu != null) {
            new ArrayList<>(this.menu.getInventory().getViewers()).forEach(HumanEntity::closeInventory);
        }
        this.menu = this.exampleMenu.build();
    }

    public void open(HumanEntity player) {
        if (this.menu == null) {
            this.update();
        }
        this.menu.open(player);
    }
}

D. Usage menu in e.g. Commands

To open the menu for a player, simply inject the ExampleMenuHolder into your command class and call open(player).

    private final ExampleMenuHolder menuHolder;

    @Executor(path = "menu", description = "Opens menu.")
    void menu(Player player) {
        this.menuHolder.open(player);
    }

8. Main Class Registration

For the DI to work, all components must be registered in the main class extending DreamBukkitPlatform. The order matters if you have specific initialization needs, but Okaeri-Injector generally handles the graph automatically.

Important: By default, the main class only extends DreamBukkitPlatform. To register custom serializers (Serdes), you must manually implement the DreamBukkitConfig interface. It does not appear by default in the template but functions automatically once added.

public final class DreamProjectPlugin extends DreamBukkitPlatform implements DreamBukkitConfig {

    @Override
    public void load(@NonNull ComponentService componentService) {
        // default load logic
    }

    @Override
    public void enable(@NonNull ComponentService componentService) {
        // 1. Setup Debug & Core Providers
        componentService.setDebug(false);

        this.registerInjectable(BukkitTasker.newPool(this));
        this.registerInjectable(BukkitMenuProvider.create(this));
        this.registerInjectable(BukkitCommandProvider.create(this));

        // 2. Register Extensions
        componentService.registerExtension(DreamCommandExtension.class);

        // 3. Register Config Resolvers & Messages
        componentService.registerResolver(ConfigurationResolver.class);
        componentService.registerComponent(MessageConfig.class);

        // 4. Register Command Handlers & Notices
        componentService.registerComponent(BukkitNoticeResolver.class);
        componentService.registerComponent(InvalidInputHandlerImpl.class);
        componentService.registerComponent(InvalidPermissionHandlerImpl.class);
        componentService.registerComponent(InvalidSenderHandlerImpl.class);
        componentService.registerComponent(InvalidUsageHandlerImpl.class);

        // 5. Register Main Config & Initialize Persistence
        // The lambda executes immediately after PluginConfig is loaded.
        componentService.registerComponent(PluginConfig.class, pluginConfig -> {

            // Register the StorageConfig (from PluginConfig) as an injectable
            this.registerInjectable(pluginConfig.storageConfig);

            // Register Persistence Resolvers
            componentService.registerResolver(DocumentPersistenceResolver.class);
            componentService.registerComponent(DocumentPersistence.class);
            componentService.registerResolver(DocumentRepositoryResolver.class);
        });

        // 6. Register Data Components (Repositories & Caches)
        // These rely on DocumentPersistence being ready
        componentService.registerComponent(ProfileRepository.class);
        componentService.registerComponent(ProfileCache.class);

        // 7. Register Logic Components (Services)
        // ProfileService @PostConstruct will fire here, loading data from Repo -> Cache
        componentService.registerComponent(ProfileService.class);

        // 8. Register Menu Components
        componentService.registerComponent(ExampleMenu.class);
        componentService.registerComponent(ExampleMenuHolder.class);

        // 9. Register Commands & Listeners
        // These rely on ProfileService being ready
        componentService.registerComponent(PlayerController.class);
        componentService.registerComponent(PointsCommand.class);

        // 10. Register Tasks
        componentService.registerComponent(AnnouncerTask.class);
    }

    @Override
    public void disable() {
        // default disable logic
    }

    @Override
    public @NonNull DreamVersion getDreamVersion() {
        return DreamVersion.create("plugin", "1.0", "author");
    }

    // Override this method to register custom serializers
    @Override
    public OkaeriSerdesPack getConfigSerdesPack() {
        return it -> {
            it.register(new MenuItemSerializer());
        };
    }
}

9. Further Reading & Building

A. External Libraries

This platform acts as a wrapper around several powerful libraries. For specific details on advanced features, please refer to their official repositories:

NOTE: Please ensure to remove unnecessary NMS support, rename any instances of the phrase "template" to match our specific project, and remove any unused libraries that may unnecessarily increase the plugin's weight.

Example of Optimization (What to remove while using Dream-Template)

Below is an example of what is considered unnecessary bloat versus what is essential, based on the provided gradle files.

settings.gradle.kts

include(":plugin-core")

// [UNNECESSARY] NMS modules
//include(":plugin-core:nms:api")
//include(":plugin-core:nms:v1_8_R3")
//include(":plugin-core:nms:v1_12_R1")
//include(":plugin-core:nms:v1_16_R3")
//include(":plugin-core:nms:v1_17_R1")
//include(":plugin-core:nms:v1_18_R2")
//include(":plugin-core:nms:v1_19_R2")
//include(":plugin-core:nms:v1_19_R3")
//include(":plugin-core:nms:v1_20_R1")
//include(":plugin-core:nms:v1_20_R2")
//include(":plugin-core:nms:v1_20_R3")
//include(":plugin-core:nms:v1_20_R5")
//include(":plugin-core:nms:v1_21_R1")
//include(":plugin-core:nms:v1_21_R2")
//include(":plugin-core:nms:v1_21_R3")
//include(":plugin-core:nms:v1_21_R4")
//include(":plugin-core:nms:v1_21_R5")

build.gradle.kts (project)

// [UNNECESSARY] unknown modules after delete
//project(":plugin-core:nms").subprojects {
//
//    val minor = name.split("_").getOrNull(1)?.toInt() ?: 0
//    val patch = name.split("R").getOrNull(1)?.toInt() ?: 0
//
//    if (name == "api" || minor < 17) {
//        return@subprojects
//    }
//
//    apply(plugin = "io.papermc.paperweight.userdev")
//
//    if (minor >= 21 || minor == 20 && patch >= 4) {
//        java {
//            sourceCompatibility = JavaVersion.VERSION_21
//            targetCompatibility = JavaVersion.VERSION_21
//
//            withSourcesJar()
//            withJavadocJar()
//        }
//    }
//    else {
//        java {
//            sourceCompatibility = JavaVersion.VERSION_17
//            targetCompatibility = JavaVersion.VERSION_17
//
//            withSourcesJar()
//            withJavadocJar()
//        }
//    }
//}

build.gradle.kts (module)

dependencies {
    // [UNNECESSARY] Complex NMS compilation logic
    //rootProject.project(":plugin-core:nms").subprojects.forEach {
    //    api(it)
    //}

    // [ESSENTIAL] Spigot-API
    compileOnly("io.papermc.paper:paper-api:1.20.6-R0.1-SNAPSHOT")

    // [ESSENTIAL] Core Platform
    implementation("cc.dreamcode.platform:bukkit:1.13.8")
    // [UNNECESSARY] Platform library support modules, softdepend hooks
    //implementation("cc.dreamcode.platform:bukkit-hook:1.13.8")
    //implementation("cc.dreamcode.platform:bukkit-config:1.13.8")
    //implementation("cc.dreamcode.platform:dream-command:1.13.8")
    //implementation("cc.dreamcode.platform:persistence:1.13.8")

    // [ESSENTIAL] Core Utilities (can change to bukkit module to avoid adventure support)
    //implementation("cc.dreamcode:utilities-adventure:1.5.8")

    // [UNNECESSARY] Notice support
    //implementation("cc.dreamcode.notice:bukkit:1.7.4")
    //implementation("cc.dreamcode.notice:bukkit-serializer:1.7.4")

    // [UNNECESSARY] Commands support
    //implementation("cc.dreamcode.command:bukkit:2.2.3")

    // [UNNECESSARY] Menu support
    //implementation("cc.dreamcode.menu:bukkit:1.4.5")
    //implementation("cc.dreamcode.menu:bukkit-serializer:1.4.5")
}

tasks.withType<ShadowJar> {

    archiveFileName.set("Dream-Template-${project.version}.jar")
    mergeServiceFiles()

    // [ESSENTIAL] Core relocations
    relocate("eu.okaeri", "cc.dreamcode.template.libs.eu.okaeri")
    // [UNNECESSARY] Library relocations
    //relocate("com.cryptomorin", "cc.dreamcode.template.libs.com.cryptomorin")
    //relocate("net.kyori", "cc.dreamcode.template.libs.net.kyori")

    // [ESSENTIAL] Core relocations
    relocate("cc.dreamcode.platform", "cc.dreamcode.template.libs.cc.dreamcode.platform")
    relocate("cc.dreamcode.utilities", "cc.dreamcode.template.libs.cc.dreamcode.utilities")
    // [UNNECESSARY] Library relocations
    //relocate("cc.dreamcode.menu", "cc.dreamcode.template.libs.cc.dreamcode.menu")
    //relocate("cc.dreamcode.command", "cc.dreamcode.template.libs.cc.dreamcode.command")
    //relocate("cc.dreamcode.notice", "cc.dreamcode.template.libs.cc.dreamcode.notice")

    // [UNNECESSARY] Persistence (db) relocations
    //relocate("org.bson", "cc.dreamcode.template.libs.org.bson")
    //relocate("com.mongodb", "cc.dreamcode.template.libs.com.mongodb")
    //relocate("com.zaxxer", "cc.dreamcode.template.libs.com.zaxxer")
    //relocate("org.slf4j", "cc.dreamcode.template.libs.org.slf4j")
    //relocate("org.json", "cc.dreamcode.template.libs.org.json")
    //relocate("com.google.gson", "cc.dreamcode.template.libs.com.google.gson")

    // [UNNECESSARY] Without NMS use minimize() only
    minimize {
    //    parent!!.project(":plugin-core:nms").subprojects.forEach {
    //        exclude(project(it.path))
    //    }
    }

    // [UNNECESSARY] Persistence (db) meta-inf transform
    //transform(PropertiesFileTransformer::class.java) {
    //    paths.set(listOf("META-INF/native-image/org.mongodb/bson/native-image.properties"))
    //    mergeStrategy.set(PropertiesFileTransformer.MergeStrategy.Append)
    //}
}

B. Building the Project (ShadowJar)

Since the project is based on the template, the build process is standardized. To generate the final .jar file ready for your server, you must use the ShadowJar task. This ensures all dependencies (Okaeri, Dream-Platform, utilities) are "shaded" (bundled) inside your plugin.

Command: Run the following in your terminal:

  • Windows: gradlew shadowJar
  • Linux/Mac: ./gradlew shadowJar

Output: The compiled plugin will appear in the build/libs/ directory. Look for the file usually named project-name-version.jar.