Skip to content

Build the Normal Stack

This is the intended full Daisy path for a real modern Paper plugin:

  1. DaisySeries owns canonical values and parsing
  2. DaisyConfig owns typed YAML, managed lifecycle, and module bundles
  3. 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.

One spawn module with:

  • settings.yml
  • lang.yml
  • DaisySeries-backed values in typed config
  • DaisyConfig-managed lifecycle
  • DaisyCore command output through the same shared text source
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, and Particle
  • DaisySeries codecs also parse Difficulty and BlockFace
  • DaisyConfig owns the file and typed decode
  • DaisyCore is not involved yet
modules/commands/spawn/settings.yml
config_version: 2
spawn:
delay: 3
icon: compass
ready_sound: entity_player_levelup
biome: cherry_grove
difficulty: hard
ready_particle: totem_of_undying
facing: north_east
modules/commands/spawn/lang.yml
config_version: 1
messages:
spawn:
ready: "<green>Spawn ready in %delay%s.</green>"
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
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
@DaisyCommandSet
class 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

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

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.