Build the Normal Stack
Build the Normal Stack
Section titled “Build the Normal Stack”This is the intended full Daisy path for a real modern Paper plugin:
- DaisySeries owns canonical values and parsing
- DaisyConfig owns typed YAML, managed lifecycle, and module bundles
- DaisyCore owns commands, UI, text rendering, and runtime systems
If your plugin only needs one layer, stop at that layer. Use the full stack when all three problems are real at once.
What this example builds
Section titled “What this example builds”One spawn module with:
settings.ymllang.yml- DaisySeries-backed values in typed config
- DaisyConfig-managed lifecycle
- DaisyCore command output through the same shared text source
1. Install the three products
Section titled “1. Install the three products”repositories { mavenCentral() maven("https://repo.papermc.io/repository/maven-public/") maven("https://jitpack.io")}
dependencies { implementation("cat.daisy:DaisySeries:0.4.0") implementation("cat.daisy:config-all:0.2.0") implementation("cat.daisy:DaisyCore:0.1.0")}Current suite release state:
- DaisySeries is released as
0.4.0 - DaisyConfig is released as
0.2.0 - DaisyCore is released as
0.1.0
2. Define a typed config shape with DaisySeries-backed fields
Section titled “2. Define a typed config shape with DaisySeries-backed fields”data class SpawnSettings( val delaySeconds: Int, val icon: Material, val readySound: Sound, val spawnBiome: Biome, val spawnDifficulty: Difficulty, val readyParticle: Particle, val spawnFacing: BlockFace,)
val spawnSettingsCodec = objectCodec { SpawnSettings( delaySeconds = defaulted("spawn.delay", intCodec(), 3), icon = required("spawn.icon", materialCodec()), readySound = required("spawn.ready_sound", soundCodec()), spawnBiome = required("spawn.biome", biomeCodec()), spawnDifficulty = required("spawn.difficulty", difficultyCodec()), readyParticle = required("spawn.ready_particle", particleCodec()), spawnFacing = required("spawn.facing", blockFaceCodec()), ) }Ownership here:
- DaisySeries codecs parse
Material,Sound,Biome, andParticle - DaisySeries codecs also parse
DifficultyandBlockFace - DaisyConfig owns the file and typed decode
- DaisyCore is not involved yet
3. Put the data in managed module files
Section titled “3. Put the data in managed module files”config_version: 2spawn: delay: 3 icon: compass ready_sound: entity_player_levelup biome: cherry_grove difficulty: hard ready_particle: totem_of_undying facing: north_eastconfig_version: 1messages: spawn: ready: "<green>Spawn ready in %delay%s.</green>"4. Load the module with DaisyConfig
Section titled “4. Load the module with DaisyConfig”class SpawnModuleRegistry( plugin: JavaPlugin,) { private val registry = DaisyModules.load(plugin) { module( DaisyModuleDefinition( category = "commands", module = "spawn", settings = DaisyManagedYamlFile( id = "commands/spawn/settings", path = "modules/commands/spawn/settings.yml", codec = spawnSettingsCodec, currentVersion = 2, migrations = listOf( DaisyYamlMigrations.move(1, 2, "spawn-delay", "spawn.delay"), ), ), lang = DaisyManagedYamlFile( id = "commands/spawn/lang", path = "modules/commands/spawn/lang.yml", codec = daisyTextConfigCodec(), ), ), ) }
val spawn: DaisyModuleHandle<SpawnSettings> get() = registry.require("commands", "spawn")}Ownership here:
- DaisyConfig creates missing files
- DaisyConfig merges missing keys
- DaisyConfig runs ordered migrations
- DaisyConfig exposes one typed module handle
5. Hand the lang source into DaisyCore
Section titled “5. Hand the lang source into DaisyCore”class SpawnPlugin : JavaPlugin() { private lateinit var daisy: DaisyPlatform private lateinit var modules: SpawnModuleRegistry
override fun onEnable() { modules = SpawnModuleRegistry(this)
daisy = DaisyPlatform.create(this) { messages(modules.spawn.textSource) commands() menus() scoreboards() tablists() } }
override fun onDisable() { daisy.close() }}This boundary matters:
- DaisyConfig stores and reloads the text
- DaisyCore still renders it
6. Use the typed settings in runtime code
Section titled “6. Use the typed settings in runtime code”@DaisyCommandSetclass SpawnCommands( private val plugin: SpawnPlugin,) : DaisyCommandGroup({ command("spawn") { description("Open the spawn flow")
player { val settings = plugin.modules.spawn.current.settings val text = plugin.modules.spawn.textSource.text("messages.spawn.ready") ?.replace("%delay%", settings.delaySeconds.toString()) ?: "<green>Spawn ready in ${settings.delaySeconds}s.</green>"
reply(text)
player.openMenu("Spawn", rows = 3) { slot(13) { item(settings.icon) { name("<gradient:#7dd3fc:#c4b5fd>Spawn</gradient>") } } }
player.playSound(player.location, settings.readySound, 1f, 1f) player.spawnParticle(settings.readyParticle, player.location, 4) } }})Ownership here:
- DaisySeries already normalized the values
- DaisyConfig already decoded and preserved runtime-safe state
- DaisyCore runs the command, menu, and text rendering
7. Adopt incrementally if you need to
Section titled “7. Adopt incrementally if you need to”You do not need the whole stack on day one.
- use only DaisyCore when the problem is pure runtime/UI
- add DaisySeries when parser glue starts leaking through config or command code
- add DaisyConfig when file lifecycle, versioning, and shared text ownership become the maintenance problem
Why this is the normal Daisy stack
Section titled “Why this is the normal Daisy stack”It keeps the suite boundaries honest:
- DaisySeries is the value layer
- DaisyConfig is the config lifecycle layer
- DaisyCore is the runtime layer
That is the intended stack for modern Minecraft plugin work.
Next steps
Section titled “Next steps”- Choose the right entrypoint first: Which product do I need?
- Start with parsing only: DaisySeries
- Start with typed config only: DaisyConfig
- Start with runtime only: DaisyCore