-
Notifications
You must be signed in to change notification settings - Fork 0
Features
Bundles are stored in YAML files within the plugins/CommandBundle/commands/ directory. You can use multiple .yml
files to organize your commands. All files are automatically loaded and merged at startup (unless auto-load-commands
is disabled in config.yml).
Example structure:
commands:
example:
permission: my.plugin.example
actions:
- "...action line 1..."
- "...action line 2..."
subcommands:
sub1:
- "...action for /example sub1..."
sub2:
- "...action for /example sub2..."Files organization:
- Store bundles in any
.ymlfile in thecommands/directory (e.g.,commands.yml,events.yml,admin.yml) - When using
/bundle addcommand, bundles are saved to thedefault-commands-file(default:commands.yml) - All
.ymlfiles are merged into a single set of commands at runtime - Use
/bundle loadfile,/bundle enablefile, and/bundle disablefileto manage which files are loaded
Action lines structure:
- Each entry in
actions:(and in eachsubcommands:list) is a single action line. - An action line can contain:
-
Timing prefixes:
[delay:] -
Branching prefixes:
[if:],[else if:],[else],[branch],[endbranch] -
Logic prefixes:
[AND],[OR],[CONDSTART],[CONDEND] -
Special markers:
!(console),-(suppress output),+(variables),$(host commands),%(webhooks),#message(messages) -
File helpers:
,,(read files),;;(write files) -
Modifiers:
[random],[foreach:] -
Placeholders:
%player%,%var:name%,%players%,{math:...}
-
Timing prefixes:
The rest of this file documents these pieces in detail.
Add a delay before running this action.
[delay:5]say This line runs 5 seconds after the previous one
- Time is in seconds.
- Delays accumulate: if one action uses
[delay:5]and the next uses[delay:3], the second will run 8 seconds after the first non-delayed action.
If no [delay:] is present, the action runs immediately (relative to the timeline built by earlier delays).
Conditions use ConditionEvaluator under the hood and are expressed as text inside [if:...] or [else if:...].
To create multi-line if/else chains, wrap them in [branch] and [endbranch] markers:
[branch]
[if:<type>:<value>[:<extra>]]
<action when if is true>
[else if:<type>:<value>[:<extra>]]
<action when else if is true>
[else]
<action when nothing matched>
[endbranch]
How it works:
-
[branch]starts a conditional block -
[if:...]tests the first condition; if true, actions following it run until the next condition or[endbranch] -
[else if:...]tests only if no prior condition matched; can have multiple[else if]clauses -
[else]runs only if no condition matched -
[endbranch]ends the block - Only one branch executes; once a condition matches, all others are skipped
Example - Multiple conditions:
[branch]
[if:var:playerJSON.name:!=]
[if:var:playerJSON.level:!=]
#message:gold:Welcome back %var:playerJSON.name%, level %var:playerJSON.level%!
[else if:var:playerJSON.name:!=]
#message:yellow:Welcome back %var:playerJSON.name%! (No level data)
[else]
#message:red:New player detected!
[endbranch]
The plugin does not parse full expressions like %playercount% > 5. Instead, it uses typed conditions.
You can combine multiple conditions using AND or OR logic. By default, multiple [if:...] conditions in a chain
use OR logic (any condition can be true). To require all conditions to be true, use AND logic.
Using [CONDSTART] and [CONDEND]:
All conditions in a chain must be on a single action line with the [CONDSTART] and [CONDEND] markers:
[CONDSTART][OR][if:<condition1>][if:<condition2>][if:<condition3>][CONDEND]
<action when any condition is true>
[CONDSTART][AND][if:<condition1>][if:<condition2>][if:<condition3>][CONDEND]
<action when all conditions are true>
How it works:
-
[CONDSTART]marks the beginning of a condition chain -
[AND]or[OR]specifies the logic mode (default is[OR]if not specified) - All
[if:...]conditions must be on the same line as[CONDSTART]and[CONDEND] -
[CONDEND]closes the chain and the following action executes if the combined condition is met - This works both inside and outside of
[branch]blocks
Example - OR Logic (any condition matches):
[CONDSTART][OR][if:permission:vip.tier1][if:permission:vip.tier2][if:permission:vip.tier3][CONDEND]
#message:gold:VIP access granted!
!give %player% diamond 5
In this example, the player needs any one of the three VIP permissions to receive the rewards.
Example - AND Logic (all conditions must match):
[CONDSTART][AND][if:level:>=20][if:health:>15][if:world:adventure_world][CONDEND]
#message:green:All requirements met! Quest unlocked!
!give %player% enchanted_book 1
In this example, the player must satisfy all three conditions: be level 20+, have health >15, AND be in the adventure_world.
Example - Inside a Branch Block:
[branch]
[CONDSTART][AND][if:permission:quest.access][if:level:>=10][if:var:quest_completed:false][CONDEND]
#message:gold:Starting quest!
+quest_completed:true
[else if:var:quest_completed:true]
#message:yellow:You already completed this quest!
[else]
#message:red:Requirements not met. Need permission, level 10+, and quest not completed.
[endbranch]
Notes:
-
[OR]is the default behavior, so[CONDSTART]without[AND]or[OR]will use OR logic - You can mix AND/OR chains with regular if/else branches
- Empty condition chains (no
[if:...]lines) will evaluate to false - The condition chain must have at least one
[if:...]to be meaningful
-
permission/perm- Checks if sender has a permission.
permission:<node>- Example:
[if:permission:myplugin.vip]#message:green:You are VIP!
-
item- Checks if a player has an item (optionally with amount).
item:<material>[:<amount>]- Example:
[if:item:DIAMOND:10]#message:yellow:You have at least 10 diamonds!
-
world- Checks the player's world name.
world:<worldName>
-
gamemode/gm- Checks the player's game mode.
gamemode:<SURVIVAL|CREATIVE|ADVENTURE|SPECTATOR>
-
health- Numeric comparison on player's health.
-
health:<comparison>where<comparison>is one of:-
>N,>=N,<N,<=N,=N,==N, or justN(interpreted as>=N).
-
- Example:
[if:health:>10]#message:green:You are healthy!
-
level/xp- Numeric comparison on player's experience level.
- Same comparison rules as
health.
-
flying,sneaking- Boolean comparison.
-
flying:trueorflying:false. - Example:
[if:flying:true]#message:yellow:You are flying!
-
op- Checks whether
sender.isOp()matches the boolean value. -
op:trueorop:false.
- Checks whether
-
player- Compares the player's name.
player:<name>
-
var- Compares a stored variable to an expected value.
var:<name>:<expected>- Example:
[if:var:rank:vip]#message:gold:VIP perks active!
Note: For player-dependent types (
item,world,gamemode,health,level,flying,sneaking,player), conditions will be false when run from the console.
You can mark multiple actions as random options. From all random actions in the current execution list, only one is chosen.
[random]#message:yellow:You got message A
[random]#message:yellow:You got message B
[random:10]#message:gold:You got rare message C
- All actions with
[random]or[random:<weight>]are collected. - One is chosen based on
randomWeight(default100, or your specified number). - Non-random actions in the same list run normally alongside the selected random one.
Prefix the actual command with ! to run it as console instead of the sender.
!say This is executed by the console
!lp user %player% parent set vip
Without !, the command is dispatched as the sender of the bundle.
Prefix the action with - to suppress output for that command.
-!say This will run as console, but output is suppressed
-eco give %player% 100
- Internally,
executeCommandSilentlytemporarily redirectsSystem.out/System.errwhile running the command. - You can combine
-with!or other modifiers;-is handled before command dispatch. - Suppression only affects visible output – the command still runs.
Use #message to send chat messages directly instead of running a command.
#message:green:This goes to the command sender in green
#message@Steve:yellow:Hello Steve!
Syntax:
#message[@<targetPlayer>]:<colorSpec>:<text>
-
@<targetPlayer>(optional): send to a specific player name/placeholder instead of the sender. -
<colorSpec>: comma-separated color/style tokens (e.g.green,gold,BOLD). -
<text>: actual message text; supports all placeholders.
<colorSpec> is interpreted via Adventure's NamedTextColor and decorations:
- Colors:
BLACK,DARK_BLUE,DARK_GREEN,DARK_RED,DARK_PURPLE,GOLD,GRAY,DARK_GRAY,BLUE,GREEN,RED,LIGHT_PURPLE,YELLOW,WHITE. - Styles:
BOLD,ITALIC,UNDERLINED,STRIKETHROUGH,OBFUSCATED.
Example:
#message:red:You do not have access to this bundle!
#message@%player%:gold,BOLD:You triggered a special bundle!
You can store values into variables and reuse them later.
+myVar:Hello
#message:green:Value is %var:myVar%
- For players, variables are per-player (keyed by UUID).
- For console senders, variables are global.
-
nameandvalueboth support placeholders and will be processed before storage.
Append ~ to the variable name to avoid the "Variable set" chat message:
+secretVar~:hidden value
In this example, secretVar is stored but the sender is not notified.
Variables are read through placeholder syntax handled by VariableManager and the helper methods
replaceVariables / replaceVariablesForConsole:
#message:yellow:Your stored value is %var:myVar%
If a variable contains a JSON-like string, you can access nested fields using dot-notation:
#message:gold:Your profile name is %var:profile.name%
-
profileis the variable name. -
nameis the JSON key. - Multiple levels are supported:
profile.stats.kills.
The JSON parsing is simple and string-based – it works best with well-formed JSON and simple structures.
Placeholders like %SomeName% that are not reserved (e.g. not player, args, players, var, etc.) are also
resolved as variables:
- First from the player's variables.
- Then from global variables.
This allows you to write %MyCustomVar% without the var: prefix in many cases.
Actions starting with $ execute host OS commands (shell) instead of Minecraft commands.
$echo Hello from shell
$ls -1 plugins
Host commands only run if
host-commands-enabled: trueinconfig.yml.
Append >>variableName to capture the shell output into a variable:
$ls -1 plugins >>plugin_list
#message:gray:Plugins on host: %var:plugin_list%
- For player senders, the variable is stored as a player variable.
- For console, it becomes a global variable.
- If
suppressCommandOutputis false, the plugin prints either the captured output or a success message. - Combine with
-to suppress all extra messages.
Command substitution runs a host command and inlines its output into the text.
#message:gray:Host date is &(date +"%Y-%m-%d")
- Pattern:
&(<shell command>). - Only available if
host-commands-enabled: true. - Output lines are joined with spaces and substituted into the surrounding text.
- If substitution fails, an empty string is inserted and a warning is logged.
The earlier
@(command)syntax you might see in older docs is not used by the current implementation; use&(...)instead.
Actions starting with % represent webhook calls. The string after % is parsed by WebhookData.parse(...).
General format:
%<url>[>><varName>][::Header1:Value1,Header2:Value2][::body]
Examples:
%https://example.com/hook
%https://example.com/hook>>responseVar
%https://example.com/hook::Content-Type:application/json::{"player":"%player%"}
%https://example.com/hook>>!silentVar::Content-Type:application/json::{"uuid":"%uuid%"}
-
url– required, parsed into ajava.net.URIand called via HTTPPOST. -
>>varName– optional; store the response body into a variable:- With a leading
!(e.g.>>!varName), the webhook is silent (no chat message about storage). - If
varNameitself contains%...%, it is treated as a dynamic variable name, resolved at runtime.
- With a leading
-
::Header1:Value1,Header2:Value2– optional headers. -
::body– optional request body (string), processed through placeholders.
Behavior (from executeWebhook):
- Always uses HTTP
POST. - 2xx responses are treated as success; body is read into a string.
- If
>>varNameis set and the sender is a player, the response is stored as a player variable under that name. - If no variable is set but webhook is not silent, the response (or error) is sent to the sender as a chat message.
Webhooks only run if
webhooks-enabled: trueinconfig.yml.
You can loop over a list of values and run an action once per item.
[foreach:Player1,Player2,Player3:player]say Hello %player%
Syntax:
[foreach:<list>:<variable>] <action using %<variable>%>
-
<list>: comma-separated values, after placeholder replacement. -
<variable>: the placeholder name you’ll use inside the action.
Execution (from executeLoopAction):
-
<list>is processed throughreplacePlaceholders. - If it starts with
&(, it is intended to come from command substitution and is split by commas/newlines. - Otherwise, it is split by commas.
- For each item:
- Whitespace is trimmed; empty items are skipped.
-
%<variable>%is replaced with the item inside the action template. - The result is processed again with full placeholder replacement.
- The final command is executed (as the sender).
Examples:
[foreach:%players%:player]#message:green:Hello %player%
[foreach:%teams%:team]#message:yellow:Team found: %team%
All non-control parts of an action line go through replacePlaceholders(...) before execution. This includes:
- Processing escape sequences.
- Argument placeholders.
- Player-specific placeholders.
- Variable placeholders.
- Server/team placeholders.
- File read/write helpers.
- Command substitution.
- Math expressions.
- Un-escaping preserved characters.
If your bundle command is /mybundle arg1 arg2, you can access arguments as:
-
%args%– all arguments joined by spaces. -
%arg1%,%arg2%, ... – individual arguments.
There are also more advanced patterns implemented in replaceArguments:
-
%argN-%– remaining arguments from positionNto the end (or empty if not present). -
%argN-::<default>%– remaining arguments from positionN, or<default>if not present. -
%argN::<default>%– single argument atN, or<default>if not present.
Examples:
#message:yellow:First argument: %arg1%.
#message:yellow:Rest: %arg2-%.
#message:yellow:Optional arg: %arg3::none%
When executed by a player, these placeholders are available:
-
%player%– player name -
%uuid%,%player_uuid%– UUID -
%world%– world name -
%x%,%y%,%z%– block coordinates -
%health%– current health -
%level%– XP level -
%gamemode%– game mode name
Plus any player variables set earlier (via + or webhooks/host commands).
Available regardless of player/console:
-
%players%– comma-separated list of online player names. -
%players_uuid%– comma-separated list of online player UUIDs. -
%playercount%– number of online players. -
%teams%– comma-separated list of scoreboard team names. -
%teamplayers:<team>%– comma-separated players in a team. -
%teamplayers_uuid:<team>%– comma-separated UUIDs in a team.
Team placeholders also support nesting – the team name itself can contain placeholders. The code resolves any nested
%...% before looking up team members.
replaceFileRead provides a compact way to read file contents or YAML values into the text.
Pattern:
,,<path>[::<yamlPath>]
Examples:
#message:gray:Config value: ,,config.yml::mysetting.path
#message:gray:File contents: ,,notes.txt
- If
<path>is not absolute, it is resolved relative to the plugin data folder. - If
::<yamlPath>is present, the file is loaded as YAML and the value atyamlPathis inserted. - Otherwise, the entire file is read as text and inserted.
handleFileWrite lets you write to files or YAML directly from actions.
Pattern:
;;<path>::<content>
;;<path>::<yamlPath>::<value>
Examples:
;;notes.txt::Hello world
;;config.yml::mysetting.enabled::true
- If
<path>is not absolute, it is resolved relative to the plugin data folder. - For YAML writes (
::<yamlPath>::<value>):- Existing file is loaded if present; otherwise a new YAML file is created.
-
valueis parsed as int/double/boolean when possible, or stored as a string.
- For plain writes (
::<content>):- File contents are replaced by
content.
- File contents are replaced by
Math expressions are handled by MathEvaluator and applied via replaceMathExpressions(...).
Pattern:
{math:<expression>}
Examples:
#message:yellow:Two plus two is {math:2+2}
#message:yellow:Level next: {math:%level%+1}
MathEvaluator supports:
- Basic operators:
+,-,*,/,%,^. - Ranges:
A..B– expanded as a numeric range (e.g.1..3→1,2,3). - Functions:
sqrt(x),int(x)(truncate),round(x).
The expression receives placeholders after other replacements (so %level% in the example above is already a
number).
Below are some examples that combine multiple features.
Check multiple conditions on stored JSON data:
+playerJSON~:{"name": "Steve", "level": 5}
[branch]
[if:var:playerJSON.name:!=]
[if:var:playerJSON.level:!=]
#message:gold:Welcome back %var:playerJSON.name%, level %var:playerJSON.level%!
[else if:var:playerJSON.name:!=]
#message:yellow:Welcome back %var:playerJSON.name%! Complete the tutorial to set your level.
[else]
#message:red:New player! Please set up your profile.
[endbranch]
[branch]
[if:permission:server.admin]
#message:red,BOLD:Admin commands available
!give %player% diamond 64
[else if:permission:server.vip]
#message:gold:VIP rewards!
!give %player% gold_ingot 32
[else if:permission:server.member]
#message:green:Member gift
!give %player% iron_ingot 16
[else]
#message:gray:Join our community for rewards!
[endbranch]
[branch]
[if:health:>15]
[if:level:>=10]
#message:green:You're strong and experienced!
!effect give %player% minecraft:strength 60 1
[else if:health:>15]
#message:yellow:Healthy but inexperienced. Keep playing!
[else if:level:>=10]
#message:gold:Experienced but low health. Be careful!
!effect give %player% minecraft:regeneration 30 1
[else]
#message:red:Low health and level. Here's some help!
!give %player% cooked_beef 16
!effect give %player% minecraft:regeneration 60 2
[endbranch]
[delay:3]+welcomeMsg:Welcome %player%!
[delay:3]#message:green:%var:welcomeMsg%
[delay:4]!give %player% bread 16
- '%http://localhost:8080/api>>playerJSON::Content-Type:application/json::{"uuid":"%uuid%"}' #or +playerJSON~:{"name": "Steve", "level": 5}'
- '[delay:2]' #wait for webhook response
- '[branch]'
- '[CONDSTART][AND][if:var:playerJSON.name:!=][if:var:playerJSON.level:!=][CONDEND]'
- '#message:gold:Welcome back %var:playerJSON.name%, level %var:playerJSON.level%!'
- '[else if:var:playerJSON.name:!=]'
- '#message:yellow:Welcome back %var:playerJSON.name%! (No level data)'
- '[else]'
- '#message:red:New player detected!'
- '[endbranch]'
$ls -1 world/playerdata >>playerFiles
[foreach:%var:playerFiles%:file]#message:gray:Found data file: %file%
Check that all conditions are met before granting access:
[CONDSTART][AND][if:permission:quest.access][if:level:>=20][if:health:>15][if:world:dungeon][CONDEND]
#message:green:All requirements met! Entering hard mode dungeon.
!tp %player% -100 64 -100
!effect give %player% minecraft:resistance 300 1
Grant rewards if player has any of several VIP tiers:
[CONDSTART][OR][if:permission:vip.bronze][if:permission:vip.silver][if:permission:vip.gold][if:permission:vip.platinum][CONDEND]
#message:gold:VIP member detected! Daily reward granted.
!give %player% diamond 5
!give %player% emerald 10
Using AND logic within a branch structure:
[branch]
[CONDSTART][AND][if:permission:event.participate][if:level:>=15][if:var:event_joined:false][CONDEND]
#message:aqua:Joining the event!
+event_joined:true
!tp %player% 0 100 0
[else if:var:event_joined:true]
#message:yellow:You're already in the event!
[else]
#message:red:Requirements not met. Need permission, level 15+, and not already joined.
[endbranch]
Combining multiple AND checks with different actions:
[CONDSTART][AND][if:world:survival][if:gamemode:SURVIVAL][if:health:>10][CONDEND]
#message:green:Survival mode active - teleporting to safe zone
!tp %player% spawn
[CONDSTART][AND][if:world:creative][if:gamemode:CREATIVE][CONDEND]
#message:aqua:Creative mode active - granting builder tools
!give %player% worldedit:wand 1
For multi-line conditions, always use [branch]...[endbranch]:
# CORRECT - Multi-line with branch block
[branch]
[if:var:name:!=]
#message:gold:Hello %var:name%!
[else]
#message:yellow:Hello stranger!
[endbranch]
# ALSO WORKS - Single line (no branch needed)
[if:var:name:!=] #message:gold:Hello %var:name%!
Without [branch]...[endbranch], multi-line if/else chains may not work correctly.
Use ~ suffix to suppress "Variable set" messages:
+playerJSON~:{"name":"Steve"}
This stores the variable silently without showing feedback to the player.