A programming language for Minecraft datapacks
Poplar compiles to Minecraft Java datapacks. Write functions, variables, conditions, loops and events instead of raw commands. Every command is checked when you build.
namespace deaths;
var #deaths = 0;
mcfun load() { onEvent("deathCount", playerDied);}
mcfun playerDied() { #deaths = #deaths + 1; /tellraw @a {{["Deaths: ", text(#deaths)]}};}How it works
Functions are Minecraft functions
Every mcfun becomes a normal .mcfunction file, so other datapacks can call it with /function. load and tick run when the pack loads and every tick.
Variables are scores or NBT
#mana is a scoreboard score and $item is NBT data. @s.#mana is the score of the entity running the function.
Commands are written as commands
A line that starts with / is a Minecraft command. {{...}} puts a value into it. The compiler checks it against the command tree of the target version.
namespace mana;
var #mana = 0;var #maxMana = 100;
mcfun tick() { execute (as @a) { regen(); }}
// Runs as each player. @s.#mana is the player's own mana.mcfun regen() { if (@s.#mana < #maxMana) { @s.#mana = @s.#mana + 1; } /title @s actionbar {{["Mana: ", text(@s.#mana), "/", text(#maxMana)]}};}$ poplar check ✗ src/main.poplar:4:21 Unknown mob_effect 'poision'. Did you mean 'poison'? ✗ 1 error
Features
Commands checked at build
Commands are checked against the command tree, including vanilla ids of items, blocks, effects and entities. Typos get a suggestion.
Events and triggers
onEvent runs a function when a scoreboard criteria counts for a player. onTrigger handles /trigger commands.
Standard library
Math and list functions, written in Poplar. Only the functions you use go into the pack. MIT-0 licensed.
Language server
poplar lsp reports errors as you type and handles hover, go to definition and completion.
Watch mode
poplar watch rebuilds on every save and copies the pack into your world's datapacks folder. Run /reload to load it.
Data files and namespaces
Declare recipes, loot tables, predicates and advancements in Poplar, and split a pack across several namespaces.
Examples
Each one is a complete project in the repository. Try one in a world with poplar watch examples/<name> --world <world folder>.
// The smallest pack: a greeting when the pack loads.namespace hello;
var #loads = 0;
mcfun load() { #loads = #loads + 1;
// A constant only exists while the pack is built. `{{...}}` writes it into the command. var name = "Poplar"; /say Hello from {{name}}!;
// A score is read in the game, so this line becomes a macro line. /say This pack has loaded {{#loads}} times;
// `for` repeats its body while the pack is built: three `say` commands. for (1..4) |step| { /say Step {{step}}; }}mana
Per-player scores, tick with execute (as @a), onTrigger, an action bar and text().
View the project on GitHub// Every player has mana that regenerates each tick. `/trigger cast` spends 30 of it on a speed boost.namespace mana;
var #mana = 0;var #maxMana = 100;
mcfun load() { onTrigger("cast", cast);}
mcfun tick() { execute (as @a) { regen(); }}
// Runs as each player. `@s.#mana` is the player's own mana.mcfun regen() { if (@s.#mana < #maxMana) { @s.#mana = @s.#mana + 1; } /title @s actionbar {{["Mana: ", text(@s.#mana), "/", text(#maxMana)]}};}
mcfun cast() { if (@s.#mana < 30) { /tellraw @s {text: "Not enough mana", color: "red"}; return; } @s.#mana = @s.#mana - 30; /effect give @s speed 5 1; /tellraw @s {text: "Whoosh!", color: "aqua"};}showoff
/trigger showoff: NBT data, if on a data path, slice, and a compound passed with to a macro function.
View the project on GitHubnamespace showoff;
var $shown = {};
mcfun load() { onTrigger("showoff", show);}
mcfun show() { $shown = {}; $shown.hover_event = @s.$SelectedItem; $shown.hover_event.action = "show_item";
if (@s.$SelectedItem.components."minecraft:custom_name") { $shown.name = @s.$SelectedItem.components."minecraft:custom_name"; named() with $shown; } else { $shown.id = slice(@s.$SelectedItem.id, 10); unnamed() with $shown; }}
// `$shown` is the macro compound, so `$(hover_event)` is `$shown.hover_event`.mcfun named() { /tellraw @a [{selector: "@s", color: "yellow"}, {text: " is showing off their ", color: "white"}, {text: "[", color: "aqua"}, {nbt: "shown.name", storage: "showoff:data", interpret: true, color: "aqua", italic: true, hover_event: $(hover_event)}, {text: "]", color: "aqua"}];}
mcfun unnamed() { /tellraw @a [{selector: "@s", color: "yellow"}, {text: " is showing off their ", color: "white"}, {text: "[", color: "aqua"}, {color: "aqua", translate: "block.minecraft.$(id)", fallback: "%s", with: [{translate: "item.minecraft.$(id)"}], hover_event: $(hover_event)}, {text: "]", color: "aqua"}];}deaths
onEvent("deathCount", ...), entity and global scores, std random and max, title and subtitle.
View the project on GitHub// Counts deaths for each player and for the whole server, with a random message.namespace deaths;
import std;
var #deaths = 0;var #worst = 0;
mcfun load() { onEvent("deathCount", playerDied);}
// Runs as the player who died, once per death.mcfun playerDied() { #deaths = #deaths + 1; @s.#deaths = @s.#deaths + 1; #worst = std:max() with {a: #worst, b: @s.#deaths};
var #message = std:random() with {low: 1, high: 3}; if (#message == 1) { /tellraw @a [{selector: "@s"}, " tripped."]; } else if (#message == 2) { /tellraw @a [{selector: "@s"}, " forgot to eat."]; } else { /tellraw @a [{selector: "@s"}, " met their end."]; }
// A subtitle shows with the next title, so it comes first. /title @s subtitle {{["Deaths: ", text(@s.#deaths)]}}; /title @s title {text: "You died", color: "red"}; /title @a actionbar {{["Server deaths: ", text(#deaths), ", the most by one player: ", text(#worst)]}};}crafting
Data files: a recipe with an item tag, a loot table override, a predicate and an advancement reward.
View the project on GitHub// Data files are JSON, so output from a generator such as misode.github.io pastes in as it is.namespace crafting;
// Any stick-like item works in the recipe below.itemTag handles { "values": ["minecraft:stick", "minecraft:blaze_rod", "minecraft:breeze_rod"] }
// The name `handles` is the id `crafting:handles`. A tag in a recipe starts with `#`.recipe cheapTrident { "type": "minecraft:crafting_shaped", "category": "equipment", "key": { "D": "minecraft:diamond", "H": "#crafting:handles" }, "pattern": [" D", " H ", "H "], "result": { "id": "minecraft:trident" }}
// A string id replaces a vanilla file: zombies now drop a diamond.lootTable "minecraft:entities/zombie" { "type": "minecraft:entity", "pools": [{ "rolls": 1, "entries": [{ "type": "minecraft:item", "name": "minecraft:diamond" }] }]}
predicate lucky { "type": "minecraft:random_chance", "chance": 0.25 }
// The reward runs the mcfun `firstTrident`, written as its function id.advancement craftedTrident { "criteria": { "trident": { "trigger": "minecraft:inventory_changed", "conditions": { "items": [{ "items": "minecraft:trident" }] } } }, "rewards": { "function": "crafting:first_trident" }}
mcfun firstTrident() { /tellraw @s {text: "You crafted a trident!", color: "aqua"}; if (predicate self:lucky) { /give @s minecraft:diamond; }}toolkit
Several files in one namespace, recursion, while with locals, and the string helpers.
View the project on GitHub// `/trigger held` describes the item in your hand, with the string helpers.namespace toolkit;
mcfun describeHeld() { // A bare data path in `if` checks that it exists, the same as `execute if data`. if (!@s.$SelectedItem) { /tellraw @s {text: "Your hand is empty", color: "gray"}; return; }
// "minecraft:diamond_sword" → "diamond_sword". The local is removed when the function ends. var $name = slice(@s.$SelectedItem.id, 10);
if (endsWith(@s.$SelectedItem.id, "_sword")) { /tellraw @s {{["A sword: ", text($name)]}}; } else if (startsWith($name, "golden_")) { /tellraw @s {{["Something shiny: ", text($name)]}}; } else { /tellraw @s {{["You hold ", text($name), ", ", text(@s.$SelectedItem.count), " of them"]}}; }}Read the documentation
The docs cover the whole language, the standard library, the CLI and what the compiled pack looks like.