Skip to content

Custom resource pack build logic

To run code during the resource pack build process, you'll need to register a PackTask.
Pack tasks are functions annotated with @PackTask and need to be located in a class that implements the PackTaskHolder interface. Those holders then need to be registered with ResourcePackBuilder.registerTaskHolders.

ResourcePackBuilder.registerTaskHolders(::CustomTaskHolder)
CustomTaskHolder.kt
class CustomTaskHolder(private val builder: ResourcePackBuilder) : PackTaskHolder {

    @PackTask
    fun customTask() {
        // Do something
    }

}

PackTask

The @PackTask annotation has three optional parameters: stage: BuildStage, runAfter: Array<String> and runBefore: Array<String>.

Build Stages

There are two build stages: PRE_WORLD and POST_WORLD. This is because some logic needs to be done before the world has been loaded (such as assigning block states to nova block types) but some other logic might need to interact with other plugins that are only loaded after the world has been loaded (for example rendering the WAILA textures for blocks of custom item services).
By default, the stage is BuildStage.AUTOMATIC, which means that the build stage will be determined based on the given runAfter and runBefore dependencies. If there are no dependencies, the stage will be PRE_WORLD.

Dependencies

You can define which tasks should be run before or after your task using the runAfter and runBefore parameters.
This is quite similar to the Initialization dependencies, with the exception that you don't define classes but task names. Tasks are named after their simple class- and function name, separated by a #. So if you have a class CustomTaskHolder with the function customTask, the task name would be CustomTaskHolder#customTask. The benefit of using strings instead of class references is that you can configure dependencies to specific tasks instead of the whole task holder class.

As an example, Nova's EnchantmentContent#write task requires language files to be loaded, certain font characters to have been created and char sizes to be calculated, but it also writes to the language files, which means that it needs to run before the LanguageContent#write task. This is how it's configured:

@PackTask(
    runAfter = ["LanguageContent#loadLangFiles", "EnchantmentContent#createBackgroundChars", "CharSizeCalculator#calculateCharSizes"],
    runBefore = ["LanguageContent#write"]
)
private fun write() {
    // ...
}

This is just an example, EnchantmentContent was made obsolete with data-driven enchantments in Minecraft 1.21

ResourcePackBuilder

As shown in the example above, you'll register a task holder constructor that accepts a ResourcePackBuilder instance as parameter. This builder instance organizes the build process and provides you with access to other task holders.

Accessing files

Resource pack building might take place entirely in memory or on disk in the plugins/Nova/resource_pack/.build/ directory, depending on the server configuration. In-memory resource pack generation is implemented using JIMFS and is the reason why all file access runs over java.nio.Path instead of java.io.File.
You can resolve any file using ResourcePackBuilder#resolve.

Retrieving PackTaskHolder instances

When creating a custom task, you might want to interact with existing task holders from your own addon, Nova, or other addons. To retrieve a task holder instance, call resourcePackBuilder.getHolder<HolderType>() with the holder class as type parameter.

class CustomTaskHolder(private val builder: ResourcePackBuilder) : PackTaskHolder {

    @PackTask
    fun customTask() {
        val languageContent = builder.getHolder<LanguageContent>()
        languageContent.setTranslation("en_us", "translation.key", "Hello World")
    }

}

Retrieving resource filters

Resource filters are used to filter out resources that should not be included in the resource pack. They can be configured by server admins and addon developers using ResourcePackBuilder.registerResourceFilter.
During the build process, you can retrieve the filters using resourcePackBuilder.getResourceFilters(stage). If you write additional files, you should check whether they are configured to be excluded.

Important built-in task holders

The following are the most important built-in task holders, which you might need to interact with in code.
You should prefer using these instead of direct file access, because they would probably overwrite your changes and are generally more convenient and performant to use.

All other pack tasks

A list of all pack tasks and their execution order will be shown in the console every time the resource pack is built.

ModelContent

Provides you with access to block and item models. Everything related to the creation of custom models should run through this.

Using ModelContent#rememberUsage, you can mark a model as used, so it (and all its parents) will be included in the resource pack. This is performed automatically for all models that are assigned to NovaItems or NovaBlocks.

LanguageContent

Provides you with access to language files.

@PackTask
fun task() {
    val languageContent = builder.getHolder<LanguageContent>()
    languageContent.setTranslation("en_us", "translation.key", "Hello World")
}

FontContent

Provides you with access to fonts.

vanillaFonts

This map shows the fonts that are part of the base game assets. These fonts are not included in Nova's resource pack.

customFonts

This map stores custom fonts and custom overrides to existing fonts from base packs, Nova, and addons.
You can get and create custom fonts using fontContent.get, fontContent.getOrCreate, fontContent.add, etc.

mergedFonts

This map shows both vanillaFonts and customFonts merged together in the same way they'd be merged for the client.
This might be useful if you want to add custom characters to the minecraft:default font without overriding existing characters. Using Font.findFirstUnoccupied or Font.findFirstUnoccupiedRange you could then search for an unoccupied range of code points.

MovedFontContent

Allows you to request vertically moved fonts.

@PackTask
fun task() {
    val movedFontContent = builder.getHolder<MovedFontContent>()
    movedFontContent.requestMovedFonts(ResourcePath("namespace", "name"), 0..19)
}

TextureIconContent

Allows you to request textures for Nova's texture-icon font.

@PackTask
fun task() {
    val textureIconContent = builder.getHolder<TextureIconContent>()
    textureIconContent.addIcons("minecraft:item/diamond", "minecraft:item/emerald")
}

They can then be retrieved from the TEXTURE_ICON_LOOKUP:

val component: Component = ResourceLookups.TEXTURE_ICON_LOOKUP[ResourceLocation("minecraft:item/diamond")].component