Skip to content
This repository was archived by the owner on Sep 21, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 36 additions & 4 deletions packs/guikit-datapack/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ storage `guikit:reg containers.<name>`. If the name is not registered it is take
| `trapped_chest` | `chest_minecart` | 27 | red | Trapped Chest |
| `shulker_box` | `chest_minecart` | 27 | magenta | Shulker Box |
| `copper_chest` | `chest_minecart` | 27 | orange | Copper Chest |
| `oak_chest_boat`, `spruce_...`, `birch_...`, `jungle_...`, `acacia_...`, `dark_oak_...`, `mangrove_...`, `cherry_...` `_chest_boat`, `bamboo_chest_raft` | the same id | 27 | gray | |
| `donkey`, `mule` | the same id | 15 | gray | registered, **not usable yet** (see below) |

The themed ones are cosmetic: Minecraft has no ender chest / barrel **entity**, and the framework needs
`/summon` + `item replace entity @s container.N`, so they are a `chest_minecart` with a different pad and title.
Expand All @@ -71,6 +73,13 @@ data modify storage guikit:reg menus."ns:shop" set value {alias:"ns_shop", conta
| `pad` | item id `widget/pad` puts in every slot | `minecraft:gray_stained_glass_pane` |
| `title` | optional **SNBT text component** (`{text:"Shop"}`), becomes the container title | none |

- **Chest boats / rafts** are in `#guikit:container`, but only minecarts follow their owner (`internal/follow`
teleports `chest_minecart` and `hopper_minecart`). A boat stays where it was summoned, so the owner has to ride it:
`ride @s mount <the owner's cart>` right after `api/open` (see `demo:click/open_chest_boat`; match the cart by
`guikit.uid`, the nearest cart can belong to another player).
- **`donkey` / `mule`** are registered but deliberately not in `#guikit:container` (their inventory slot numbering
under `container.N` is unconfirmed), so opening such a menu is refused by `internal/open_fail`. There is no `offset`
key.
- Registry names are plain keys (`shop`, `ns_shop`); quote the path if you use a colon: `containers."ns:shop"`.
- `title` must be a compound, not `'{"text":"Shop"}'`: in 26.3 a quoted string is a plain string and the title
would show the JSON itself.
Expand Down Expand Up @@ -108,10 +117,22 @@ Runs `as` the player. Result: `#cond guikit.tmp` = 1 / 0 (also the return value)
| `advancement` | `adv` | e.g. `minecraft:story/root` |
| `predicate` | `pred` | any predicate, so anything not listed can still be a condition |
| `level` | optional `min`, `max` | XP **level** (the number above the hotbar, not a scoreboard); with neither key any level passes. Not verified in a live client |
| `all` | `of:[{...}, {...}]` | passes when **every** element passes (empty list passes); stops at the first failure |
| `any` | `of:[{...}, {...}]` | passes when **at least one** element passes (empty list fails); stops at the first pass |

`not:1b` inverts. An unknown `type` or a missing key **fails** (result 0). Every type is its own small function
```mcfunction
data merge storage guikit:cond {type:"all", of:[{type:"tag", tag:"vip"}, {type:"level", min:5}, {type:"tag", tag:"banned", not:1b}]}
function guikit:cond/check
```

`all`/`any` are one level deep: an element must be a plain type, an element that is itself `all`/`any` is not
evaluated and counts as **failed**. Every element can carry its own `not`, and so can the `all`/`any` itself.
A missing `of` fails (`not` is not applied to it).

`not:1b` inverts. An unknown `type` or a missing key **fails** (result 0), except that a `not:1b` next to an
unknown type / missing key flips that 0 to 1 (known quirk of the leaf types; the `all`/`any` path does not have it). Every type is its own small function
(`guikit:cond/t_*`); `min`/`max` are two open-ended ranges, never a closed `A..B`.
New key -> add it to `guikit:internal/clear_cond` (and to `guikit:internal/btn_cond` if buttons should support it).
New key -> add it to `guikit:internal/clear_cond` and to `guikit:cond/load_cur` (that copy list is shared by buttons and by `all`/`any`).

## Command buttons (`guikit:widget/button`)

Expand Down Expand Up @@ -140,17 +161,23 @@ Definition keys:
| `url` | prints a clickable `open_url` link in chat instead (combine with `close:1b`, chat is hidden behind the menu) |
| `cond` | any condition from the table above (same keys, incl. `not`). Failing: `deny` message, nothing runs |
| `deny` | message when `cond` fails, default `Not available.` (no double quotes in it) |
| `cost` | `{obj:"coins", amount:5}` (score) or `{item:"minecraft:diamond", count:3}` (item, `count` defaults to 1). Charged after `cond` passed and before `cmd` / `url`; not enough -> `poor` message, nothing runs, nothing is taken |
| `poor` | message when the cost cannot be paid, default `You can't afford that.` (no double quotes) |
| `close` | `1b` = close the menu after the command |
| `timer` | reset the menu timeout to N ticks on a successful click |
| `locked_item` | what is drawn while `cond` fails (default `minecraft:barrier`). Cosmetic: the click re-checks `cond` |
| `locked_item` | what is drawn while `cond` fails **or the cost is not affordable** (default `minecraft:barrier`). Cosmetic: the click re-checks both |

Notes:
- `cmd` is macro-expanded raw. If it contains double quotes, write it as a single-quoted SNBT string:
`cmd:'tellraw @s {"text":"hi"}'`.
- `cmd` runs with the permission level datapack functions get (`function-permission-level`, default 2), not the
player's. **`cmd` / `url` must come from your menu code, never from player input** (command injection).
- An item `cost` first removes the widget items from the inventory (the clicked widget is still there while the
click is handled), so a button that costs its own item type neither counts nor takes the widget instead of your
items. The look check (`locked_item`) uses the same rule as the `item_count` condition. Score cost = `pay_score`,
item cost = `pay_item`.
- A working example is on page 2 of `examples/guikit-demo` (`demo:apple`, `demo:coin`,
`demo:sword`, `demo:vip`, `demo:link`). `/function demo:open`, then navigate to page 2.
`demo:sword`, `demo:vip`, `demo:lvl`, `demo:combo` = `all` condition + score cost, `demo:link`). `/function demo:open`, then navigate to page 2.

## Widget: single-select (`guikit:widget/radio`)
Generalizes `toggle` from a bool to N states: each option in the group calls it with its own
Expand Down Expand Up @@ -260,6 +287,11 @@ execute unless function guikit:widget/cooldown_start run function guikit:interna
client: `experience query @s levels` storing into a score, `playsound ... ui @s` argument order, the
`execute unless function ...` form used in the `cd_notify` example, and the `on_close` firing on
menu switch described above.
- **`all` / `any` conditions, button `cost` / `poor`, the `guikit.drop` detection branch in `tick_player`, chest boats /
rafts and the hopper `follow` are new and untested beyond `mecha .`** (all 83 macro lines in the pack were also
expanded with sample values and linted). Not checked in a real client: the `comp_step` recursion, the `cnd` scratch
storage, `cost` charging order (cond -> cost -> cmd), whether a boat can be ridden while the menu is open, and
whether the `minecraft.custom:minecraft.drop` statistic reacts to Q-dropping a widget out of the cart.
- **Still not done:** behavior in a real game (`clear` + `custom_data` match, `summon`, tick ordering,
multiplayer). Only the load step has been observed.
- **The container registry (`internal/containers_builtin`, `summon`, `pad_cart_dyn`, `open_fail`), the `radio`/`meter`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ scoreboard players operation @e[type=#guikit:container,tag=guikit.new,distance=.
tag @e[type=#guikit:container,tag=guikit.new,distance=..1] remove guikit.new

# state
# guikit.drop is the vanilla drop statistic: it also counts drops made while no menu was open, so start clean
scoreboard players set @s guikit.drop 0
execute store result score @s guikit.page run data get storage guikit:in page
execute store result score @s guikit.timer run data get storage guikit:in timer
data modify storage guikit:ctx menu set from storage guikit:in menu
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
# advancement {adv:"minecraft:story/root"}
# predicate {pred:"ns:name"}
# level {[min:N], [max:N]} XP level (not a scoreboard); both optional
# all / any {of:[{type:..}, {type:..}]} every / at least one element passes. Elements must be
# leaf types above (a nested all/any counts as failed)
# `not:1b` inverts the result. Unknown type / missing key = fails (closed).
#
# function guikit:internal/clear_cond
Expand All @@ -18,6 +20,8 @@
#
# One small function per type (no big execute chains), same as the rest of the pack.
scoreboard players set #cond guikit.tmp 0
execute if data storage guikit:cond {type:"all"} run return run function guikit:cond/t_all
execute if data storage guikit:cond {type:"any"} run return run function guikit:cond/t_any
execute if data storage guikit:cond {type:"score"} run function guikit:cond/t_score
execute if data storage guikit:cond {type:"item_count"} run function guikit:cond/t_item_count
execute if data storage guikit:cond {type:"tag"} if data storage guikit:cond tag run function guikit:cond/t_tag with storage guikit:cond
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# macro: $(i)
data remove storage guikit:cnd cur
$data modify storage guikit:cnd cur set from storage guikit:cnd items[$(i)]
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# guikit :: cond/comp_run as player #cmode 0 = all / 1 = any, #cacc = starting value
# Walks the list guikit:cond of, loading each element into guikit:cond and running cond/check on it.
# One level only: an element that is itself all/any is not evaluated and counts as FAILED. A missing
# `of` fails (return 0, `not` is not applied). Result in #cond guikit.tmp, also returned.
execute store success score #cnot guikit.tmp if data storage guikit:cond {not:1b}
data remove storage guikit:cnd items
data modify storage guikit:cnd items set from storage guikit:cond of
execute unless data storage guikit:cnd items run return 0
execute store result score #cn guikit.tmp run data get storage guikit:cnd items
scoreboard players set #ci guikit.tmp 0
function guikit:cond/comp_step
scoreboard players operation #cond guikit.tmp = #cacc guikit.tmp
execute if score #cnot guikit.tmp matches 1 run function guikit:cond/negate
data remove storage guikit:cnd items
data remove storage guikit:cnd cur
function guikit:internal/clear_cond
return run scoreboard players get #cond guikit.tmp
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# guikit :: cond/comp_step as player one element per call, recurses until the list is done or the result is decided
execute unless score #ci guikit.tmp < #cn guikit.tmp run return 0
execute store result storage guikit:cnd i int 1 run scoreboard players get #ci guikit.tmp
function guikit:cond/comp_pick with storage guikit:cnd
function guikit:internal/clear_cond
function guikit:cond/load_cur
scoreboard players set #cond guikit.tmp 0
execute unless data storage guikit:cond {type:"all"} unless data storage guikit:cond {type:"any"} run function guikit:cond/check
execute if score #cmode guikit.tmp matches 0 if score #cond guikit.tmp matches 0 run scoreboard players set #cacc guikit.tmp 0
execute if score #cmode guikit.tmp matches 1 if score #cond guikit.tmp matches 1 run scoreboard players set #cacc guikit.tmp 1
scoreboard players add #ci guikit.tmp 1
# decided already: all stops at the first failure, any at the first pass
execute if score #cmode guikit.tmp matches 0 if score #cacc guikit.tmp matches 0 run return 0
execute if score #cmode guikit.tmp matches 1 if score #cacc guikit.tmp matches 1 run return 0
function guikit:cond/comp_step
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# guikit :: cond/load_cur copies the condition compound in storage guikit:cnd cur into guikit:cond, key by key
# (a root-level `data modify storage X {} set from ...` is avoided on purpose, see README "Validation status").
# Call internal/clear_cond first. Keep this list in sync with internal/clear_cond.
execute if data storage guikit:cnd cur.type run data modify storage guikit:cond type set from storage guikit:cnd cur.type
execute if data storage guikit:cnd cur.obj run data modify storage guikit:cond obj set from storage guikit:cnd cur.obj
execute if data storage guikit:cnd cur.min run data modify storage guikit:cond min set from storage guikit:cnd cur.min
execute if data storage guikit:cnd cur.max run data modify storage guikit:cond max set from storage guikit:cnd cur.max
execute if data storage guikit:cnd cur.item run data modify storage guikit:cond item set from storage guikit:cnd cur.item
execute if data storage guikit:cnd cur.tag run data modify storage guikit:cond tag set from storage guikit:cnd cur.tag
execute if data storage guikit:cnd cur.mode run data modify storage guikit:cond mode set from storage guikit:cnd cur.mode
execute if data storage guikit:cnd cur.adv run data modify storage guikit:cond adv set from storage guikit:cnd cur.adv
execute if data storage guikit:cnd cur.pred run data modify storage guikit:cond pred set from storage guikit:cnd cur.pred
execute if data storage guikit:cnd cur.not run data modify storage guikit:cond not set from storage guikit:cnd cur.not
execute if data storage guikit:cnd cur.of run data modify storage guikit:cond of set from storage guikit:cnd cur.of
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# guikit :: cond/t_all storage guikit:cond {type:"all", of:[{...}, ...], [not]}
# Passes when EVERY element passes (empty list = passes). See cond/comp_run.
scoreboard players set #cmode guikit.tmp 0
scoreboard players set #cacc guikit.tmp 1
return run function guikit:cond/comp_run
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# guikit :: cond/t_any storage guikit:cond {type:"any", of:[{...}, ...], [not]}
# Passes when AT LEAST ONE element passes (empty list = fails). See cond/comp_run.
scoreboard players set #cmode guikit.tmp 1
scoreboard players set #cacc guikit.tmp 0
return run function guikit:cond/comp_run
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# guikit :: internal/btn_afford as player reads storage guikit:btn cur.cost
# cost:{obj:"coins", amount:5} score cost
# cost:{item:"minecraft:diamond", [count:3]} item cost (count defaults to 1)
# Result in #cond guikit.tmp (1 = enough). Does not charge.
scoreboard players set #cond guikit.tmp 1
execute unless data storage guikit:btn cur.cost.count run data modify storage guikit:btn cur.cost.count set value 1
execute if data storage guikit:btn cur.cost.obj run function guikit:internal/btn_afford_score with storage guikit:btn cur.cost
execute if data storage guikit:btn cur.cost.item run function guikit:internal/btn_afford_item
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# guikit :: internal/btn_afford_item as player
# Reuses the item_count condition, which does not count the clicked widget item (see cond/t_item_count_do).
scoreboard players set #cond guikit.tmp 0
function guikit:internal/clear_cond
data modify storage guikit:cond item set from storage guikit:btn cur.cost.item
data modify storage guikit:cond min set from storage guikit:btn cur.cost.count
function guikit:cond/t_item_count_do with storage guikit:cond
function guikit:internal/clear_cond
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# macro: $(obj) $(amount) as player
$execute unless score @s $(obj) matches $(amount).. run scoreboard players set #cond guikit.tmp 0
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,24 @@
# url:"https://.." prints a clickable link in chat instead (use close:1b so the player can see chat)
# cond:{...} any guikit:cond type, see cond/check. Fails -> `deny` message, nothing runs
# deny:"..." message when cond fails (no double quotes). Default "Not available."
# cost:{...} {obj:"coins", amount:5} or {item:"minecraft:diamond", [count:3]}; charged after cond passes,
# before cmd/url. Not enough -> `poor` message, nothing runs, nothing is taken
# poor:"..." message when the cost cannot be paid (no double quotes). Default "You can't afford that."
# close:1b close the menu after the command
# timer:N reset the menu timeout to N ticks on a successful click
# locked_item:"minecraft:barrier" look when cond fails (widget/button)
# locked_item:"minecraft:barrier" look when cond fails or the cost is not affordable (widget/button)
data remove storage guikit:btn cur
$data modify storage guikit:btn cur set from storage guikit:btn defs."$(id)"
execute unless data storage guikit:btn cur run return 0

function guikit:internal/btn_cond
execute if score #cond guikit.tmp matches 0 run return run function guikit:internal/btn_deny

# cost: charged only now (cond passed), before the command runs
scoreboard players set #paid guikit.tmp 1
execute if data storage guikit:btn cur.cost run function guikit:internal/btn_pay
execute if score #paid guikit.tmp matches 0 run return run function guikit:internal/btn_poor

# decide now: the command may overwrite guikit:btn cur (e.g. by running another button)
execute store success score #btn_close guikit.tmp if data storage guikit:btn cur{close:1b}
execute if data storage guikit:btn cur.timer run function guikit:internal/btn_timer with storage guikit:btn cur
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,9 @@
# guikit :: internal/btn_cond as player reads storage guikit:btn cur.cond
# Result: #cond guikit.tmp = 1 when the button has no condition or it passes, else 0.
# Copies key by key into guikit:cond (a root-level `data modify ... set from` is avoided on purpose,
# see README "Validation status"). Keep this list in sync with internal/clear_cond.
scoreboard players set #cond guikit.tmp 1
execute unless data storage guikit:btn cur.cond run return 1
function guikit:internal/clear_cond
execute if data storage guikit:btn cur.cond.type run data modify storage guikit:cond type set from storage guikit:btn cur.cond.type
execute if data storage guikit:btn cur.cond.obj run data modify storage guikit:cond obj set from storage guikit:btn cur.cond.obj
execute if data storage guikit:btn cur.cond.min run data modify storage guikit:cond min set from storage guikit:btn cur.cond.min
execute if data storage guikit:btn cur.cond.max run data modify storage guikit:cond max set from storage guikit:btn cur.cond.max
execute if data storage guikit:btn cur.cond.item run data modify storage guikit:cond item set from storage guikit:btn cur.cond.item
execute if data storage guikit:btn cur.cond.tag run data modify storage guikit:cond tag set from storage guikit:btn cur.cond.tag
execute if data storage guikit:btn cur.cond.mode run data modify storage guikit:cond mode set from storage guikit:btn cur.cond.mode
execute if data storage guikit:btn cur.cond.adv run data modify storage guikit:cond adv set from storage guikit:btn cur.cond.adv
execute if data storage guikit:btn cur.cond.pred run data modify storage guikit:cond pred set from storage guikit:btn cur.cond.pred
execute if data storage guikit:btn cur.cond.not run data modify storage guikit:cond not set from storage guikit:btn cur.cond.not
data remove storage guikit:cnd cur
data modify storage guikit:cnd cur set from storage guikit:btn cur.cond
function guikit:cond/load_cur
function guikit:cond/check
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# guikit :: internal/btn_gate as player reads storage guikit:btn cur
# Result: #cond guikit.tmp = 1 when the button's cond passes AND its cost is affordable, else 0.
# Nothing is charged here (used when drawing, to decide the locked look).
function guikit:internal/btn_cond
execute if score #cond guikit.tmp matches 0 run return 0
execute if data storage guikit:btn cur.cost run function guikit:internal/btn_afford
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# guikit :: internal/btn_pay as player reads storage guikit:btn cur.cost
# Charges the cost through the existing helpers. Result in #paid guikit.tmp (1 = paid, 0 = not enough).
execute unless data storage guikit:btn cur.cost.count run data modify storage guikit:btn cur.cost.count set value 1
execute if data storage guikit:btn cur.cost.obj run function guikit:internal/pay_score with storage guikit:btn cur.cost
execute if data storage guikit:btn cur.cost.item run function guikit:internal/btn_pay_item
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# guikit :: internal/btn_pay_item as player
# The clicked widget is still in the inventory here. Remove widget items first so pay_item can neither
# count it nor clear it instead of the player's real items (a button that costs its own item type).
function guikit:internal/safe_clear
function guikit:internal/pay_item with storage guikit:btn cur.cost
Loading
Loading