-
Notifications
You must be signed in to change notification settings - Fork 1
Home
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.
To avoid manual setup and ensure your project follows these standards, always start by generating your repository from the template.
- Go to Dream-Template.
- Click the green "Use this template" button.
- 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.
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. |
Dream-Platform uses Okaeri-Injector for DI. The standard practice is Constructor Injection.
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
}
}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.");
}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());
}
}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());
}
}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);
});
}
}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.
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.
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);
}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());
};
}
}This platform acts as a wrapper around several powerful libraries. For specific details on advanced features, please refer to their official repositories:
- Dependency Injection: Okaeri-Injector by Okaeri
- Configuration: Okaeri-Configs by Okaeri
- Persistence (Data): Okaeri-Persistence by Okaeri
- Commands: Dream-Command by DreamCode
- Menu: Dream-Menu by DreamCode
- Notice: Dream-Notice by DreamCode
- Utilties: Dream-Utilities by DreamCode
- Placeholders (text/notice processor): Okaeri-Placeholders by Okaeri
- XSeries (multiplatform support): XSeries by CryptoMorin
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)
//}
}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.