Getting Started

This guide takes you from zero to a real development workflow with Kore. Instead of stopping at a minimal "hello world," you will build a small but structured datapack, run it in-game, iterate quickly, and learn how to scale your project.

If you already have solid datapack experience and want an architecture-first migration guide, jump to From Datapacks to Kore.

What you will build

By the end of this page, you will have:

  • A Kotlin project configured for Kore.
  • A datapack with metadata and multiple functions.
  • A simple game loop entry point (load + user-triggered functions).
  • A practical local development cycle to edit, regenerate, and test quickly.
  • A clean starting structure you can keep expanding.

Prerequisites

  • Java 21 (JDK 21) or higher.
  • Gradle (wrapper recommended: ./gradlew).
  • IntelliJ IDEA (recommended) or another IDE with Kotlin support.
  • Basic understanding of Minecraft datapacks (helpful but not required).

Kotlin basics you need before Kore

If you are new to Kotlin, do not worry. You only need a small subset to be productive with Kore.

val and var

  • val means read-only reference (preferred by default).
  • var means mutable reference (use only when reassignment is needed).
val packName = "starter_kore"
var buildNumber = 1
buildNumber += 1
Kotlin

Functions

You declare functions with fun. Kore code is mostly function calls inside builders.

fun greet(name: String): String {
	return "Hello, $name"
}
Kotlin

Null safety

Kotlin distinguishes nullable (String?) and non-null (String) types.

val maybePlayer: String? = System.getenv("PLAYER_NAME")
val displayName = maybePlayer ?: "Player"
Kotlin

Use:

  • ?. for safe calls,
  • ?: for fallback values (Elvis operator).

Lambdas and builder blocks

Kore uses Kotlin lambdas heavily:

function("hello") {
	tellraw(allPlayers(), textComponent("Hello"))
}
Kotlin

The { ... } block is a lambda, and inside it Kore exposes a DSL context with helper functions.

Extension functions (very important in Kore projects)

Kotlin lets you add functions to existing types without inheritance. This is ideal for structuring large datapacks.

fun DataPack.registerWelcome() {
	function("feature/welcome") {
		tellraw(allPlayers(), textComponent("Welcome"))
	}
}
Kotlin

You can then call registerWelcome() inside your dataPack {} builder.

Step 1: Create your project

You have two ways to start.

The fastest option is the template repository, which is already configured for Kotlin + Gradle + Kore.

  1. Open Kore Template.
  2. Click "Use this template" (or clone directly).
  3. Open it in IntelliJ IDEA.
  4. Let Gradle sync.
  5. Run the main function in src/main/kotlin/Main.kt.

Option B: Add Kore to an existing Kotlin project

If you already have a Kotlin project, add Kore manually.

Core modules

  • kore - core DSL to generate datapacks.
  • oop - object-oriented gameplay abstractions.
  • helpers - helper utilities built on top of Kore.
  • bindings - experimental importer for existing datapacks.

For your first datapack, use only kore.

Gradle Kotlin DSL

dependencies {
	implementation("io.github.ayfri.kore:kore:VERSION")
}
Kotlin

Gradle Groovy DSL

dependencies {
    implementation 'io.github.ayfri.kore:kore:VERSION'
}
Groovy

Snapshot builds (latest unreleased changes)

If you want bleeding-edge features:

repositories {
	mavenCentral()
	maven("https://central.sonatype.com/repository/maven-snapshots/")
}

dependencies {
	implementation("io.github.ayfri.kore:kore:VERSION-SNAPSHOT")
}
Kotlin

Kotlin compiler and JVM settings

Kore relies on context parameters. Make sure your build.gradle.kts contains:

kotlin {
	compilerOptions {
		freeCompilerArgs.add("-Xcontext-parameters")
	}

	jvmToolchain(21)
}
Kotlin

Step 2: Write a first useful datapack

Instead of a single command, create a tiny but realistic pack with:

  • metadata (pack),
  • one function for player feedback,
  • one function for setup behavior.

If you are new to Kotlin syntax, read the code like this:

  • val datapack = ... stores the generated datapack object.
  • dataPack("starter_kore") { ... } creates a builder context.
  • each function("...") { ... } block writes one generated .mcfunction file.

Create Main.kt:

fun main() {
	val datapack = dataPack("starter_kore") {
		pack {
			description = textComponent("Starter datapack generated with Kore")
		}

		function("hello") {
			tellraw(allPlayers(), textComponent("Hello from Kore"))
		}

		function("setup") {
			tellraw(allPlayers(), textComponent("Setup complete"))
		}
	}

	datapack.generateZip()
}
Kotlin

Run main, then put the generated zip into your world's datapacks folder.

Step 3: Load and test in Minecraft

  1. Open your world.
  2. Run /reload.
  3. Trigger the function:
/function starter_kore:hello
Mcfunction

If everything is correct, chat displays your message.

Step 4: Use load {} as your real entry point

In Kore, using load {} is usually better than manually naming a startup function and wiring tags yourself. It directly creates and registers a function in minecraft:load, which makes your startup flow explicit and less error-prone.

Most datapacks become easier to maintain when you separate:

  • initialization (load),
  • recurring logic (tick when needed),
  • feature functions (your own namespaced functions).

Add this structure in Kore by defining dedicated functions and using consistent names:

fun main() {
	val datapack = dataPack("starter_kore") {
		pack {
			description = textComponent("Starter datapack generated with Kore")
		}

		load("bootstrap") {
			tellraw(allPlayers(), textComponent("[starter_kore] datapack loaded"))
			function("feature/give_welcome")
		}

		function("feature/give_welcome") {
			tellraw(allPlayers(), textComponent("Welcome to the world"))
		}

		tick("runtime/checks") {
			// Keep tick lightweight. Route heavy logic to dedicated functions.
		}
	}

	datapack.generate()
}
Kotlin

With this approach:

  • load("bootstrap") runs once after /reload.
  • tick("runtime/checks") runs every game tick.
  • function("feature/...") stays your reusable API surface.
  • folder-like names keep generated files organized as the project grows.

Step 5: Organize code before it gets messy

A common beginner mistake is placing everything in one main function. Prefer small helpers and focused files early.

Example using extensions:

fun DataPack.registerCoreFunctions() {
	function("load") {
		tellraw(allPlayers(), textComponent("[starter_kore] datapack loaded"))
	}
}

fun DataPack.registerFeatureFunctions() {
	function("feature/give_welcome") {
		tellraw(allPlayers(), textComponent("Welcome to the world"))
	}
}

fun main() {
	val datapack = dataPack("starter_kore") {
		pack {
			description = textComponent("Starter datapack generated with Kore")
		}

		registerCoreFunctions()
		registerFeatureFunctions()
	}

	datapack.generateZip()
}
Kotlin

This pattern scales much better than one giant builder block.

Step 6: Use a fast development loop

During development, your loop should be:

  1. Edit Kotlin code.
  2. Re-run main.
  3. Copy or regenerate output into your world datapack folder.
  4. In-game: /reload.
  5. Run the function you are testing.

Tips:

  • Use .generate() when you want to inspect generated files locally.
  • Use .generateZip() when you want easy distribution.
  • Enable pretty JSON in Configuration when debugging generated resources.

Step 7: Build an advanced mini-pack with a custom enchantment

At this point, create a pack that is closer to production structure:

  • startup flow with load,
  • runtime flow with regular functions,
  • one custom data-driven feature (a custom enchantment).

Example:

fun main() {
	val datapack = dataPack("starter_kore") {
		pack {
			description = textComponent("Starter datapack with a custom enchantment")
		}

		// 1) Runtime functions
		function("feature/welcome") {
			tellraw(allPlayers(), textComponent("Welcome to starter_kore"))
		}

		function("feature/show_vampiric_hint") {
			tellraw(allPlayers(), textComponent("Try the Vampiric enchantment on a sword"))
		}

		// 2) Startup entry point
		load("bootstrap") {
			function("feature/welcome")
			function("feature/show_vampiric_hint")
		}

		// 3) Custom enchantment definition (data-driven content)
		enchantment("vampiric") {
			description(textComponent("Vampiric"))
			supportedItems(Tags.Item.SWORDS)
			primaryItems(Tags.Item.SWORD_ENCHANTABLE)
			weight = 2
			maxLevel = 3
			minCost(20, 15)
			maxCost(50, 15)
			anvilCost = 8
			slots(EquipmentSlot.MAINHAND)

			effects {
				// Bonus damage that scales by level
				damage {
					add(linearLevelBased(1, 1))
				}

				// Chance to heal attacker on hit
				postAttack {
					applyMobEffect(
						PostAttackSpecifier.ATTACKER,
						PostAttackSpecifier.ATTACKER,
						Effects.INSTANT_HEALTH,
					) {
						minAmplifier(0)
						maxAmplifier(0)
						minDuration(1)
						maxDuration(1)

						requirements {
							randomChance(linearLevelBased(0.08, 0.08))
						}
					}
				}
			}
		}
	}

	datapack.generateZip()
}
Kotlin

What this gives you:

  • a generated data/starter_kore/enchantment/vampiric.json,
  • startup player feedback via minecraft:load,
  • a clear split between command functions and data-driven definitions.

To validate in-game:

  1. Regenerate your datapack.
  2. Put it in your world.
  3. Run /reload.
  4. Check logs/chat for load output.
  5. Test enchantment behavior with commands or controlled test scenarios.

Going beyond a minimal datapack

Once your first commands work, move to data-driven features:

  • Add recipes, loot tables, predicates, and tags.
  • Create multiple function entry points per feature.
  • Separate "bootstrap" code from "gameplay" code.
  • Reuse helper functions to avoid duplicated command blocks.

Good expansion ideas after the custom enchantment:

  • A welcome system with per-player conditions.
  • A starter kit function with basic inventory setup.
  • A scoreboard-based progression mechanic.
  • A custom recipe that complements your enchantment.
  • A balancing pass for enchantment costs, rarity, and max level.

Kotlin tips that matter specifically in Kore projects

  • Prefer val by default; immutable declarations reduce accidental state issues.
  • Use extension functions on DataPack to keep your DSL composable.
  • If your IDE imports the wrong DSL symbol, qualify temporarily with this. in the builder scope, then fix the import.
  • Keep function names and file-like paths consistent (feature/x, system/y) to keep generated output predictable.
  • Re-declaring the same logical entry in Kore is idempotent: the last declaration wins.
  • Prefer load {} and tick {} builders over manual tag wiring for standard lifecycle hooks.

Quick Kotlin learning resources

Troubleshooting

Unresolved Kore DSL symbols

  • Check that the dependency exists in the correct module.
  • Verify the compiler flag -Xcontext-parameters.
  • Refresh/sync Gradle in your IDE.

Java/Kotlin toolchain errors

  • Confirm JDK 21 is installed and selected by Gradle.
  • Confirm jvmToolchain(21) is configured.

Datapack generates but does not work in-game

  • Confirm the namespace/function path in /function.
  • Run /reload after every regeneration.
  • Check the world folder path and datapack placement.
  • Open logs and verify no JSON or command syntax error is reported.

Start here next:

  1. Creating a Datapack
  2. Functions
  3. Commands
  4. Selectors
  5. Cookbook
  6. Recipes
  7. Enchantments

For the full index, see Home.

See also