Skip to content

Migration Guide 0.24 ➝ 0.25

Item and Block Types

Nova items and blocks now use Bukkit's ItemType and BlockType, just like vanilla types.

Replace Material-Based Type Access

ItemStack.type and Block.type return a Material, which cannot represent Nova's custom types. Use these extension properties provided by Nova instead:

  • ItemStack.itemType: ItemType gets an item stack's type.
  • Block.blockType: BlockType gets a block's type.

Both work for vanilla and Nova types. Update existing type checks too, so they do not mistake custom items or blocks for vanilla ones.

Nova now includes a Kotlin compiler plugin that warns about Material usage and other common migration mistakes. Nova's Gradle plugin enables it automatically.

val itemType: Material = stack.type
val blockType: Material = block.type
val itemType: ItemType = stack.itemType
val blockType: BlockType = block.blockType

Registries

Registration functions now return a RegistryEntry, a reference to the registered object, instead of the object itself. Keep this reference in your registration singleton. If a function needs the actual object, call entry.get() after the registry has loaded.

Do not resolve entries while initializing your registration singleton: the objects have not been created yet. Builder functions run later and may run again during registry reloads.

When comparing types, compare entries with entries or resolved values with values. Nova's ItemStack.itemTypeEntry: RegistryEntry.Paper<ItemType> extension property returns the entry for an item stack:

val isExampleItem = stack.novaItem == Items.EXAMPLE_ITEM
val isExampleItem = stack.itemTypeEntry == Items.EXAMPLE_ITEM

Configs

Configs are now read through ConfigProvider instead of Configurate's configuration nodes. Files remain YAML, but values are deserialized using kotlinx.serialization. See Accessing Configs.

If you have custom Configurate serializers, replace them with KSerializers and register them via CONFIGS.setSerializers(addon, serializersModule). See Custom Serializers.

Calling entry("path") without a default value is deprecated because a missing or invalid config value can throw an exception at runtime and break addon functionality. Pass a default with entry(default, "path") so Nova can fall back to it instead.

Behavior Factories

For ItemBehaviorFactory:

override fun create(item: NovaItem): MyBehavior =
    MyBehavior(item.config.entry<Int>("value"))
override fun create(entry: RegistryEntry.Paper<ItemType>, config: ConfigProvider): MyBehavior =
    MyBehavior(config.entry<Int>(1, "value"))

For BlockBehaviorFactory:

override fun create(block: NovaBlock): MyBlockBehavior =
    MyBlockBehavior(block.config.entry<Int>("value"))
override fun create(entry: RegistryEntry.Paper<BlockType>, config: ConfigProvider): MyBlockBehavior =
    MyBlockBehavior(config.entry<Int>(1, "value"))

Item Behaviors

  • Replace vanillaMaterialProperties with an override of modifyClientSideItemType that selects the client-side ItemType.
  • handleUseTick and handleUseStopped now return ItemAction?. Return null to skip cooldowns and other post-use effects. Return ItemAction.None to trigger those effects without changing the stack, or another ItemAction to also modify the stack.

Built-In Behaviors

  • The Enchantable factory takes registry entry sets, e.g. supportedEnchantments = registryEntrySetOf(EnchantmentEntries.SHARPNESS), instead of providers of enchantment sets.
  • Damageable.repairIngredient now takes a registry entry set instead of a RecipeChoice, e.g. registryEntrySetOf(ItemTypeEntries.IRON_INGOT).
  • Add AttributeModifiers() explicitly if your item uses the attribute_modifiers config entry.

Item Data

NamespacedCompound and the ItemStack.novaCompound extension property have been removed. Item data is now stored in Bukkit's persistent data container (PDC).

Existing Item Data Is Migrated Automatically

Nova automatically merges data from old item compounds into the item's PDC, keeping the same namespaced keys. You do not need to write a converter for this storage change.

Nova's item-data helper functions now read and write PDC values using CBF serialization. ItemBehavior.defaultCompound has also been removed, so supply defaults when reading missing values:

val key = Key.key(ExampleAddon, "value")
val value = itemStack.retrieveData<Int>(key) ?: 0

Tools and Weapons

Tool now controls mining through vanilla tool rules. Attack damage, attack speed, sweeping attacks, and durability loss on attack are handled by Weapon. Add it to tools that previously relied on Tool for these settings:

behaviors(Tool(), Damageable(), Enchantable())
behaviors(Tool(), Weapon(), Damageable(), Enchantable())

Move durability loss on block breaking from Damageable to Tool, and durability loss on attacking from Damageable to Weapon.

Custom Tool Categories

ToolCategory objects are no longer registered. Pass the category's name as a Key to Tool instead. Nova uses that name to find tags, which are named groups of items or blocks.

To migrate a category named example_addon:hammer, create two tags:

  • A block tag, example_addon:mineable/hammer, containing the blocks that hammers should mine efficiently.
  • An item tag, example_addon:hammers, for the hammer items. Tool adds its items to this tag automatically, but the tag must already be registered.

Custom Tool Tiers

ToolTier registration and the numeric tier hierarchy in tool_levels.yml have been removed. Tiers are now identified by a Key, and block tags determine which blocks each tier is too weak to collect drops from.

For a tier named example_addon:emerald, this tag is example_addon:incorrect_for_emerald_tool. Put blocks in it if their drops should require a stronger tool than emerald.

For an existing emerald tier equivalent to diamond, reuse diamond's restrictions:

ToolTiers.kt
@Init(stage = InitStage.PRE_PACK)
object ToolTiers {
    val EMERALD = ExampleAddon.registerToolTier("emerald")
}
BlockTags.kt
@Init(stage = InitStage.PRE_PACK)
object BlockTags {
    val INCORRECT_FOR_EMERALD_TOOL = ExampleAddon.blockTag("incorrect_for_emerald_tool") {
        add(BlockTypeTags.INCORRECT_FOR_DIAMOND_TOOL)
    }
}

The tool_tier: example_addon:emerald config setting can stay the same; it now selects the tier's block tag by name.

When configuring the tier in Kotlin:

ExampleAddon.item("emerald_pickaxe") { }
behaviors(Tool(toolTier = Key.key(ExampleAddon, "emerald")))

See Custom Tool Categories and Tiers for the tag setup, or Tool Behavior for explicit rules and the tool_rules config format.

Blocks

BlockPos has been replaced by Bukkit's Block throughout the addon API.

Block access no longer goes through WorldDataManager. Nova provides these extension properties on Bukkit's Block:

  • Block.blockType: BlockType returns the block's type, whether vanilla or custom.
  • Block.novaBlockState: NovaBlockState? returns the custom block's state, or null for a vanilla block.
  • Block.novaTileEntity: TileEntity? returns the Nova tile-entity at that block, or null if there is none.

Block Registration

Move breaking properties and sounds out of the behavior list and configure them directly in the block builder:

ExampleAddon.block(...) { }
behaviors(
    Breakable(3.0, setOf(VanillaToolCategories.PICKAXE), VanillaToolTiers.STONE, true, Material.STONE),
    BlockSounds(SoundGroup.STONE)
)
ExampleAddon.block(...) { }
breakable(
    hardness = 3.0,
    toolCategories = setOf(Key.key("pickaxe")),
    toolTier = Key.key("stone"),
    requiresToolForDrops = true,
    hitParticles = ItemTypeEntries.STONE,
    breakParticles = BlockTypeEntries.STONE
)
sounds(SoundGroup.STONE)

The TileEntityLimited block behavior has been removed; tile-entity limit tracking is now enabled by default.

The stateSelector passed to entityBacked, entityItemBacked, or modelLess must now return Bukkit BlockData instead of an NMS block state. For example, return BlockType.BARRIER.createBlockData() for a barrier backing block.

Block State Properties

The split between scoped and unscoped block state properties has been removed. Specify allowed values when creating the property:

BlockStateProperties.kt
object BlockStateProperties {
    val LEVEL = IntProperty(Key.key("example_addon", "level"))
}

object ScopedBlockStateProperties {
    val LEVEL = BlockStateProperties.LEVEL.scope(0..2)
}
BlockStateProperties.kt
object BlockStateProperties {
    val LEVEL = IntProperty(Key.key("example_addon", "level"), 0..2)
}

Use DefaultBlockStateProperties for built-in properties. The function that chooses a property's value when a block is placed is now an Autofiller; read placement parameters with ctx.resolve(...) instead of ctx[...].

The Waterloggable behavior has been removed. Keep the DefaultBlockStateProperties.WATERLOGGED property; it now enables waterlogging on its own.

Block States

Block States Are Now Mutable

NovaBlockState now implements Bukkit's BlockData. Changing a property modifies the existing state object instead of creating a different one. This does not update the block in the world automatically.

Outside a tile-entity, apply the state with block.blockData = state. Inside a tile-entity:

updateBlockState(blockState.with(BlockStateProperties.ACTIVE, active))
val state = blockState
state[BlockStateProperties.ACTIVE] = active
updateBlockState(state)

Block Contexts

Block parameters now use Bukkit types. BLOCK_POS is replaced by BLOCK, and the separate vanilla / Nova type and state parameters are replaced by BLOCK_TYPE (BlockType) and BLOCK_STATE (BlockData).

val ctx = Context.intention(BlockPlace)
    .param(BlockPlace.BLOCK_POS, pos)
    .param(BlockPlace.BLOCK_TYPE_NOVA, Blocks.EXAMPLE_BLOCK)
    .build()
val ctx = Context.intention(BlockPlace)
    .param(BlockPlace.BLOCK, block)
    .param(BlockPlace.BLOCK_TYPE, Blocks.EXAMPLE_BLOCK.get())
    .build()

Contexts

Context autofillers now run when build() is called, instead of lazily when a parameter is read.

Custom Parameter Registration

Custom parameter types are now registered on the intention itself:

ContextParamTypes.kt
object ContextParamTypes {
    val RECURSIVE_BREAK = DefaultingContextParamType<Boolean, BlockBreak>(
        Key.key("example_addon", "recursive_break"),
        default = false
    )
}
ContextParamTypes.kt
object ContextParamTypes {
    val RECURSIVE_BREAK = BlockBreak.addDefaultingParamType(false)
}

For optional or required parameters, use addOptionalParamType or addRequiredParamType, respectively.

Tile Entities

Tile-entity constructors now receive a Bukkit Block, and the pos property is now block:

class ExampleTileEntity(pos: BlockPos, blockState: NovaBlockState, data: Compound)
    : NetworkedTileEntity(pos, blockState, data)
class ExampleTileEntity(block: Block, blockState: NovaBlockState, data: Compound)
    : NetworkedTileEntity(block, blockState, data)

TileEntity.coroutineSupervisor is now nullable instead of lateinit.

Tile Entity Networks

In network side configurations, replace Map<BlockFace, ...> with CubeFaceMap and Map<BlockSide, ...> with BlockSideMap (likewise CubeFaceSet / BlockSideSet for sets).

For custom network types, replace holderTypes with extractHolders. Nova calls this function with a NetworkEndPoint. Return the data holders your network needs from that end point, such as its ItemHolder, or null if a required holder is missing. Pass an Int for tickDelay instead of a provider.

GUIs

InvUI's reactive Kotlin DSL is now preferred over the previous builder-based approach with manual GUI updates. All Nova functions built around this DSL now use items that expect a GUI texture to provide the background, instead of items with the inventory background built into their textures.

Client-side items are no longer directly exposed to addons. Use regular server-side item stacks and item providers, including in GUIs.

Replace item.model.clientsideProvider with item.guiItemProvider for GUI icons. guiItemProvider only differs from itemProvider by hiding Nova's advanced tooltip; it does not create a separate client-side item.

EnergyBar, FluidBar, and direct SideConfigMenu construction are deprecated in favor of energyBar, fluidBar, and openSideConfigItem.

GUI Textures

Update existing GUI texture registrations like this:

GuiTextures.kt
@Init(stage = InitStage.PRE_PACK)
object GuiTextures {
    val EXAMPLE = ExampleAddon.guiTexture("example") {
        path("gui/custom_example")
    }
}
GuiTextures.kt
@Init(stage = InitStage.PRE_PACK)
object GuiTextures {
    val EXAMPLE = ExampleAddon.guiTexture("example") {
        texture { path("gui/custom_example") }
    }
}

getTitle now returns a Provider<Component> instead of a Component:

window(player) { }
title by GuiTextures.EXAMPLE.getTitle(Component.text("Example"))

Tile Entity Menus

Replace annotated inner menu classes with an overridden menu property:

Inside ExampleTileEntity
@TileEntityMenuClass
inner class ExampleMenu : GlobalTileEntityMenu() {
    override val gui = Gui.builder()
        .setStructure("i i i i i i i i i")
        .addIngredient('i', inventory)
        .build()
}
Inside ExampleTileEntity
override val menu = TileEntityMenu.cachedWindow {
    upperGui by gui("i i i i i i i i i") {
        'i' by inventory
    }
}

Each player gets their own window. If your menu opens additional windows, call menu.register(window) so Nova can close them when the tile-entity is removed or the player moves out of range. See Tile Entity Menu.

Recipes

Register custom recipe types during PRE_WORLD instead of POST_WORLD. See Creating a custom Recipe Type.

Packet Entities

Fake entities have been replaced by packet entities:

val entity = FakeItem(spawnLocation, false) { _, data ->
    data.item = itemStack
    data.hasNoGravity = true
}
entity.register()
entity.updateEntityData { item = newItemStack }
entity.remove()
val entity = packetItemEntity {
    location by spawnLocation
    metadata {
        item by itemStack
        noGravity by true
    }
}
entity.spawn()
entity.metadata.item = newItemStack
entity.despawn()

Notable Dependency Updates