Interactions

An interaction (minecraft:interaction) is an invisible hitbox the game reports clicks on. It is the usual base for buttons, levers and clickable NPC areas in a datapack, since it needs no item, block or mob. Vanilla creates one with /summon, and the game then only records who clicked it last:

summon minecraft:interaction ~ ~ ~ {width:0.5f,height:0.5f,response:1b}
Mcfunction

Kore turns the click into a callback. interaction(name) { } declares the interaction, onRightClick and onLeftClick add what runs, and summon places one:

import io.github.ayfri.kore.helpers.interactions.interaction
import io.github.ayfri.kore.helpers.interactions.onRightClick
import io.github.ayfri.kore.helpers.interactions.summon

val lever = interaction("lever") {
	height = 0.5f
	width = 0.5f
	onRightClick { say("Pulled") }
}

function("place_lever") { lever.summon(vec3(0, 64, 0)) }
Kotlin
summon minecraft:interaction 0 64 0 {Tags:["my_pack.interaction.lever"],height:0.5f,width:0.5f,UUID:[I;-522001395,-648367109,-1705678205,-267231968]}
Mcfunction
Footprint
  • Objectives: none.
  • Storage: none.
  • Entities: only the interactions you summon, each tagged <pack>.interaction.<name>.
  • Forceloaded chunks: none.
  • Every tick: nothing, the game fires the advancement when a click happens.
  • Load and files: each click kind a definition uses (right_click, left_click) generates one advancement at interaction/<name>_<click> and three functions: interaction_<name>_<click> (reward), interaction_<name>_<click>_hit (callbacks) and interaction_<click>_target, shared by every definition of the pack. Callbacks briefly tag the player <pack>.interaction.source and the interaction <pack>.interaction.clicked.

Right and left clicks

onRightClick { } runs when a player uses the interaction, onLeftClick { } when a player attacks it. A definition can use both, and several callbacks of the same kind run in declaration order.

interaction("gate_button") {
	height = 0.5f
	response = true
	width = 0.5f

	onRightClick { fill(vec3(10, 65, 5), vec3(10, 66, 5), Blocks.AIR) }
	onLeftClick { fill(vec3(10, 65, 5), vec3(10, 66, 5), Blocks.IRON_BARS) }
}
Kotlin

The callbacks are generated when the interaction(name) { } block returns, so they only count when added inside it. An interaction built with the plain interaction { } (no name) is only a model: summon refuses it when it has callbacks, because nothing was generated for them.

Inside a callback

@s is the player who clicked and the position is the clicked interaction's, so relative coordinates and positioned work from the button. The lambda parameter, clicked by convention, is an Entity selecting that interaction:

interaction("crate") {
	onLeftClick { clicked -> clicked.kill() }
}
Kotlin
execute at @s on attacker run kill @e[limit=1,tag=my_pack.interaction.clicked,type=minecraft:interaction]
Mcfunction

A callback with several commands is run through a generated function, named after a hash of its commands so the pack builds identically every time. The interaction builder isn't reachable inside a callback of a plain interaction, onRightClick { width = 2f } doesn't compile. The click callbacks of an NPC are the exception, they reach the NPC builder to declare an objective(key).

Generated output

For the lever above, a right click produces this advancement at data/my_pack/advancement/interaction/lever_right_click.json:

{
	"criteria": {
		"right_click": {
			"trigger": "minecraft:player_interacted_with_entity",
			"conditions": {
				"entity": {
					"type": "minecraft:entity_properties",
					"entity": "this",
					"predicate": {
						"minecraft:entity_type": "minecraft:interaction",
						"minecraft:entity_tags": {
							"all_of": [
								"my_pack.interaction.lever"
							]
						}
					}
				}
			}
		}
	},
	"rewards": {
		"function": "my_pack:generated_scopes/interaction_lever_right_click"
	}
}
JSON

and these functions:

# interaction_right_click_target
execute on target if entity @s[tag=my_pack.interaction.source] run return 1
return fail

# interaction_lever_right_click_hit
tag @s add my_pack.interaction.clicked
execute at @s on target run say Pulled
data remove entity @s interaction
tag @s remove my_pack.interaction.clicked

# interaction_lever_right_click
advancement revoke @s only my_pack:interaction/lever_right_click
tag @s add my_pack.interaction.source
execute as @e[distance=..24,tag=my_pack.interaction.lever,type=minecraft:interaction] if function my_pack:generated_scopes/interaction_right_click_target run function my_pack:generated_scopes/interaction_lever_right_click_hit
tag @s remove my_pack.interaction.source
Mcfunction

The advancement fires for the clicking player and revokes itself so the next click fires it again. The game stores the last clicker on each interaction, not who clicked now, so the reward function tags the player and the target function (attacker for a left click) keeps the nearby interactions of the definition whose last clicker is that player. Their stored click is cleared after the callbacks ran, since on target and on attacker read it, so it can't fire twice.

A left click is the same with player_hurt_entity, on attacker and data remove entity @s attack.

Hitbox and response

width and height size the hitbox, in blocks: the box is centered on the position horizontally and grows upwards, and the game defaults to 1 for both. response = true makes the game react to a click, the attack sound for a left click and an arm swing for a right click.

NBT Kore Default Effect
height height 1 Height of the hitbox, upwards from the position
response response false Plays the attack sound and swings the arm on clicks
Tags tags none Extra scoreboard tags, the definition tag is added
width width 1 Width of the hitbox, centered on the position

tags is one of the entity fields every interaction shares with other entities, from the EntitySummonData base: customName, glowing, rotation(yaw, pitch) and the others are available too, and written before height.

Search radius

The reward function only looks at instances of the interaction within searchRadius blocks of the player, 24 by default. A click on an instance farther away runs no callback and leaves its stored click in place, which happens with a wide hitbox or a boosted entity_interaction_range. searchRadius is set per interaction, inside its block, and isn't part of the entity NBT:

interaction("gate") {
	searchRadius = 40
	onRightClick { say("Open") }
}
Kotlin
execute as @e[distance=..40,tag=my_pack.interaction.gate,type=minecraft:interaction] if function my_pack:generated_scopes/interaction_right_click_target run function my_pack:generated_scopes/interaction_gate_right_click_hit
Mcfunction

An NPC sets it the same way, npc("bob") { interaction { searchRadius = 40 } }.

Hover

Hover callbacks aren't available because the type_specific/player.looking_at predicate needs a per-player check every tick and only tests an entity, it can't tell which one is hovered. A tick function with the predicate does the job when the pack accepts the cost:

val lookingAtStatue = predicate("looking_at_statue") {
	entityProperties {
		typeSpecific {
			player {
				lookingAt {
					entityType(EntityTypes.INTERACTION)
					entityTags { allOf("my_pack.interaction.statue") }
				}
			}
		}
	}
}

tick("hover_check") {
	execute {
		asTarget(allPlayers { predicate = lookingAtStatue })
		run { say("Looking") }
	}
}
Kotlin
execute as @a[predicate=my_pack:looking_at_statue] run say Looking
Mcfunction

OOP handle

summon returns an InteractionEntity, which targets that instance by UUID and takes every Entity extension. The UUID comes from DataPack.entityUUID(), a hash of the pack name, the interaction and its position, so builds stay identical.

function("setup") {
	val button = lever.summon(vec3(0, 64, 0))
	button.kill()
}
Kotlin
kill @e[limit=1,nbt={UUID:[I;...]},type=minecraft:interaction]
Mcfunction

One summon call writes one summon line with one fixed UUID, so a function that runs at several places (execute at @a run function place_lever) creates only the first entity, and the next ones fail because the UUID is already taken. Placing one interaction at several positions from one line isn't possible with the handle, since it needs the UUID. The vanilla summon with the model's toNbt() has no UUID and no handle, so it works there, with the definition tag added to tags so the clicks still match:

lever.tags = lever.tags.orEmpty() + lever.definitionTag!!

function("place_lever") { summon(lever.entityType, vec3(), lever.toNbt()) }
Kotlin
summon minecraft:interaction ~ ~ ~ {Tags:["my_pack.interaction.lever"],height:0.5f,width:0.5f}
Mcfunction

InteractionEntity lives in the oop module, see Entities & Players.

A Wall Button for a Gate

Without a helper, a clickable gate button is an advancement filtering on the interaction, a reward function that revokes it and finds the clicking player among the interactions, and a function per click. With Kore it is one declaration:

val gateButton = interaction("gate_button") {
	height = 0.5f
	response = true
	width = 0.5f

	onRightClick {
		fill(vec3(10, 65, 5), vec3(10, 66, 5), Blocks.AIR)
		playSound(SoundEvents.Block.Lever.CLICK)
	}
	onLeftClick {
		fill(vec3(10, 65, 5), vec3(10, 66, 5), Blocks.IRON_BARS)
		playSound(SoundEvents.Block.Lever.CLICK)
	}
}

function("build_gate") { gateButton.summon(vec3(11, 65, 5)) }
Kotlin
# interaction_gate_button_right_click_hit
tag @s add my_pack.interaction.clicked
execute at @s on target run function my_pack:generated_scopes/generated_16d41647
data remove entity @s interaction
tag @s remove my_pack.interaction.clicked

# generated_16d41647
fill 10 65 5 10 66 5 minecraft:air
playsound minecraft:block.lever.click
Mcfunction

Right clicking the button opens the gate, left clicking it closes it again.

NPCs

An NPC is a mannequin you see and an interaction you click, summoned at the same position. npc(name) { } declares both: mannequin { } configures the body, onRightClick and onLeftClick add the callbacks, and summon places the pair.

import io.github.ayfri.kore.helpers.npcs.npc

val greeter = npc("greeter") {
	mannequin {
		customName("Bob", Color.GOLD)
		playerProfile("Ayfri")
	}
	onRightClick { say("Welcome!") }
}

function("place_greeter") { greeter.summon(vec3(0, 64, 0)) }
Kotlin
summon minecraft:mannequin 0 64 0 {CustomName:{text:"Bob",color:"gold"},Invulnerable:1b,Tags:["my_pack.npc.greeter"],immovable:1b,profile:{name:"Ayfri"},UUID:[I;698108564,1678478208,-1383230621,-2104385281]}
summon minecraft:interaction 0 64 0 {Tags:["my_pack.interaction.npc_greeter"],height:1.9f,width:0.7f,UUID:[I;-1579629773,-2071100508,-114521633,248843919]}
Mcfunction

The mannequin is immovable and invulnerable by default and carries the tag <pack>.npc.<name>. The interaction is the definition npc_<name>, so it generates what any interaction does. They share a position instead of riding each other, since a passenger sits at the attachment point and would shift the hitbox.

Kore has no hook run when a block ends, so the NPC is registered when the npc(name) { } block returns: the callbacks, objectives and sizes only count when declared inside it, and a name is unique in the pack.

Hitbox

The interaction is as wide and tall as the mannequin's pose plus 0.1, so a click reaches it before the mannequin. Setting width or height in interaction { } keeps your value for that side and sizes the other one from the pose.

Pose Mannequin hitbox (width x height) Interaction
STANDING (default) 0.6 x 1.8 0.7 x 1.9
CROUCHING 0.6 x 1.5 0.7 x 1.6
SWIMMING and FALL_FLYING 0.6 x 0.6 0.7 x 0.7
SLEEPING 0.2 x 0.2 0.3 x 0.3
STANDING, 0.7 x 1.9
CROUCHING, 0.7 x 1.6

A mannequin in a lying pose is drawn well outside its hitbox, the model of a swimming or sleeping one covers more than the interaction. Set interaction { width = 1.8f; height = 0.6f } to cover the drawing.

val sleeper = npc("sleeper") {
	mannequin { pose = MannequinPose.SLEEPING }
	interaction { width = 1.8f }
}
Kotlin

Here the interaction is 1.8 wide and 0.3 tall.

Inside an NPC callback

@s is the player and the position is the NPC's, like in any interaction callback, but clicked selects the mannequin, the entity a player sees, instead of the interaction. The selector is sorted by distance, so with two instances standing close together the one at the clicked position wins:

npc("bob") {
	onLeftClick { clicked -> clicked.kill() }
}
Kotlin
execute at @s on attacker run kill @e[distance=..0.5,limit=1,sort=nearest,tag=my_pack.npc.bob,type=minecraft:mannequin]
Mcfunction

State

objective(key) declares the dummy objective <pack>.npc.<name>.<key>, creates it in the load function npc_<name>_load and returns its name. The callback picks who holds the score, with the usual scoreboard commands:

Scope Holder Example
Per player and per NPC the clicking player, self() visits of each player to this NPC
Per NPC instance the mannequin, clicked how many times this very NPC was greeted
Per definition a fake player, fakePlayer("total") clicks on all the instances of this NPC
npc("counter") {
	val visits = objective("visits")
	val greeted = objective("greeted")
	val total = objective("total")

	onRightClick { clicked ->
		scoreboard.players.add(self(), visits, 1)
		scoreboard.players.add(clicked.asScoreHolder(), greeted, 1)
		scoreboard.players.add(fakePlayer("total").asScoreHolder(), total, 1)
	}
}
Kotlin
scoreboard players add @s my_pack.npc.counter.visits 1
scoreboard players add @e[distance=..0.5,limit=1,sort=nearest,tag=my_pack.npc.counter,type=minecraft:mannequin] my_pack.npc.counter.greeted 1
scoreboard players add #total my_pack.npc.counter.total 1
Mcfunction
# npc_counter_load
scoreboard objectives add my_pack.npc.counter.visits dummy
scoreboard objectives add my_pack.npc.counter.greeted dummy
scoreboard objectives add my_pack.npc.counter.total dummy
Mcfunction

For a value that is read back as a Kotlin property, see State Delegates.

An NPC that talks, with timed lines and choices per player, calls a conversation from onRightClick.

NpcEntity

summon returns an NpcEntity holding the two handles, mannequin and interaction, each targeting its instance by UUID. kill() and teleportTo(position, rotation) act on both, the rotation only turns the mannequin.

function("move_greeter") {
	val bob = greeter.summon(vec3(0, 64, 0))
	bob.teleportTo(vec3(0, 64, 5))
	bob.kill()
}
Kotlin
teleport @e[limit=1,nbt={UUID:[I;698108564,1678478208,-1383230621,-2104385281]},type=minecraft:mannequin] 0 64 5
teleport @e[limit=1,nbt={UUID:[I;-1579629773,-2071100508,-114521633,248843919]},type=minecraft:interaction] 0 64 5
kill @e[limit=1,nbt={UUID:[I;698108564,1678478208,-1383230621,-2104385281]},type=minecraft:mannequin]
kill @e[limit=1,nbt={UUID:[I;-1579629773,-2071100508,-114521633,248843919]},type=minecraft:interaction]
Mcfunction

Any other entity command goes through bob.mannequin or bob.interaction. NpcEntity isn't an Entity because it stands for two entities.

The two UUIDs are fixed per summon call, so a function that runs at several places (execute at @a run function place_npc) creates only the first NPC, and the next ones fail because the UUIDs are already taken. Placing one NPC at several positions from one line isn't possible with summon, since the handle needs the UUIDs. The vanilla summon with the toNbt() of both models has no handle and works there, with the interaction tag added to tags so the clicks still match:

greeter.interaction.tags = greeter.interaction.tags.orEmpty() + greeter.interaction.definitionTag!!

function("place_npc") {
	summon(EntityTypes.MANNEQUIN, vec3(), greeter.mannequin.toNbt())
	summon(EntityTypes.INTERACTION, vec3(), greeter.interaction.toNbt())
}
Kotlin
summon minecraft:mannequin ~ ~ ~ {CustomName:{text:"Bob",color:"gold"},Invulnerable:1b,Tags:["my_pack.npc.greeter"],immovable:1b,profile:{name:"Ayfri"}}
summon minecraft:interaction ~ ~ ~ {Tags:["my_pack.interaction.npc_greeter"],height:1.9f,width:0.7f}
Mcfunction

A Shopkeeper that Opens a Menu

A shopkeeper by hand is a mannequin, an interaction, the advancement and functions of the click, a trigger objective for the dialog and a score per player. With Kore it is one declaration: a right click opens a menu and counts the visit of the player, a left click answers in the chat.

val shop = menu("shop", "Bob's Shop", Color.GOLD) {
	text("Everything is for sale.")
	button("Heal me") { say("healed") }
}

val shopkeeper = npc("shopkeeper") {
	val visits = objective("visits")

	mannequin {
		customName("Bob", Color.GOLD)
		customNameVisible = true
		playerProfile("Ayfri")
		pose = MannequinPose.CROUCHING
	}

	onRightClick {
		scoreboard.players.add(self(), visits, 1)
		shop.open()
	}
	onLeftClick { tellraw(self(), textComponent("Bob: Right click to trade.", Color.GRAY)) }
}

function("place_bob") { shopkeeper.summon(vec3(0, 64, 0)) }
Kotlin
# place_bob
summon minecraft:mannequin 0 64 0 {CustomName:{text:"Bob",color:"gold"},CustomNameVisible:1b,Invulnerable:1b,Tags:["my_pack.npc.shopkeeper"],immovable:1b,pose:"crouching",profile:{name:"Ayfri"},UUID:[I;1986223431,821987320,-494237298,-530125415]}
summon minecraft:interaction 0 64 0 {Tags:["my_pack.interaction.npc_shopkeeper"],height:1.6f,width:0.7f,UUID:[I;-725450195,-1726051519,-1557154389,87209692]}

# generated_460f5ccb
scoreboard players add @s my_pack.npc.shopkeeper.visits 1
dialog show @s my_pack:shop

# interaction_npc_shopkeeper_left_click_hit
tag @s add my_pack.interaction.clicked
execute at @s on attacker run tellraw @s {type:"text",color:"gray",text:"Bob: Right click to trade."}
data remove entity @s attack
tag @s remove my_pack.interaction.clicked

# npc_shopkeeper_load
scoreboard objectives add my_pack.npc.shopkeeper.visits dummy
Mcfunction
Footprint
  • Objectives: one dummy objective per objective(key), named <pack>.npc.<name>.<key>.
  • Entities: two per summon, a mannequin tagged <pack>.npc.<name> and an interaction tagged <pack>.interaction.npc_<name>.
  • Load and files: npc_<name>_load creates the objectives, only when there are some. The rest is the interaction's, see the footprint above.