Permissions
Decide who can use what, with permission nodes, defaults and permission plugins.
Permissions decide who may use which feature of your plugin. On this page you will name permissions properly, declare them in plugin.yml, check them in commands and listeners, give them out with LuckPerms, and build a tiered kit command and ranks with different limits.
What is a permission?
A permission is a piece of text, called a permission node, such as myplugin.heal. Every player either has that node or does not. Your plugin asks "does this player have myplugin.heal?" and acts on the answer. The server owner decides who has what, without touching your code.
Why not just check "is this player an operator"? Because operators can do everything, and servers have more roles than "operator" and "everyone": a moderator who may heal others but not stop the server, a donator rank with a bigger kit, a builder who may use /buildmode. Permissions let one plugin serve all of them.
Naming permission nodes
The convention is lowercase words separated by dots, from the general to the specific: plugin.feature.action. Starting with your plugin's name keeps your nodes from clashing with anyone else's.
| Node | Meaning |
|---|---|
homes.use | May use the homes feature at all. |
homes.set | May set a home. |
homes.delete.others | May delete other players' homes. Longer means more specific. |
homes.limit.5 | May have up to 5 homes (explained below). |
homes.* | Everything under homes, a wildcard (explained below). |
Good names are lowercase, use dots (not spaces or underscores to separate levels) and describe the right, not the command. A node called myplugin.heal is better than myplugin.healcommand, because tomorrow a button might also heal.
Declaring permissions in plugin.yml
You do not have to declare a permission to check it. But declaring it is how you say who has it by default, and how permission plugins show your nodes in their lists. This is the plugin.yml of the demo plugin for this page:
Where this file livespermissions-demosrcmainresourcesplugin.yml
Files in src/main/resources are copied into the jar exactly as they are. Paper looks for plugin.yml at the top of the jar.
- permissions-demo/
- src/main/
- java/com/example/permissionsdemo/Package com.example.permissionsdemo
- HomeLimitCommand.javaCommand (BasicCommand)
- Kit.javaEnum: a fixed list of choices
- KitCommand.javaCommand (Brigadier tree)
- NumberedPermissions.javaHelper class
- PermissionsDemoPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- resources/Files copied into the jar as they are
- plugin.ymlyou are hereTells Paper the plugin's name, version and main class
- java/com/example/permissionsdemo/Package com.example.permissionsdemo
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1name: PermissionsDemo2version: '1.0.0'3main: com.example.permissionsdemo.PermissionsDemoPlugin4api-version: '26.3'5description: Tiered /kit permissions and numbered home limits.6permissions:7 permissionsdemo.use:8 description: Lets a player use the demo commands at all.9 default: true10 permissionsdemo.kit.starter:11 description: Lets a player take the starter kit.12 default: true13 permissionsdemo.kit.vip:14 description: Lets a player take the VIP kit.15 default: false16 permissionsdemo.kit.builder:17 description: Lets a player take the builder kit.18 default: op19 permissionsdemo.kit.*:20 description: Lets a player take every kit.21 default: op22 children:23 permissionsdemo.kit.starter: true24 permissionsdemo.kit.vip: true25 permissionsdemo.kit.builder: true26 permissionsdemo.homes.limit.3:27 description: Allows 3 homes. Declared only so operators can test the numbered limit.28 default: op29 permissionsdemo.homes.limit.10:30 description: Allows 10 homes. Declared only so operators can test the numbered limit.31 default: op32 permissionsdemo.admin:33 description: Everything this plugin offers.34 default: op35 children:36 permissionsdemo.use: true37 permissionsdemo.kit.*: true- The list of permissions this plugin uses. Each entry is a node name with a few settings under it.
- A sentence for humans. Permission plugins show it, so write what the permission allows, not what it is called.
- Everyone has this unless a permissions plugin takes it away.
- Nobody has it, not even operators, until it is granted explicitly.
- Operators have it. Everyone else needs it granted.
- A wildcard node: a name ending in
.*that stands for a whole group. It is just a normal node withchildren. - Having this node also gives these nodes.
truemeans "gives it",falsemeans "takes it away". - Numbered nodes are declared here only so an operator can test them without a permissions plugin. A real server would give them out with LuckPerms instead.
- A parent of parents: this one node covers the whole plugin.
default | Who has it when nobody granted anything |
|---|---|
true | Everyone |
false | Nobody |
op | Operators only. This is also what a node you never declared gets. |
not op | Everyone who is not an operator |
The full list of plugin.yml settings is on plugin.yml explained. The plugin.yml Builder can write the permission block for you.
Checking a permission
The check itself is one method. Every player, and the console, has it: hasPermission("node") returns true or false.
1if (!player.hasPermission("permissionsdemo.kit.vip")) {2 player.sendRichMessage("<red>You need the VIP rank for this kit.");3 return;4}Behind that one method, Paper follows these steps:
This is why a node declared default: true can still be denied: a permissions plugin can explicitly set it to false for a player or a rank, and an explicit value wins over a default. Two related methods exist for rare cases: isPermissionSet("node") tells you whether the player has an entry for that node at all (an entry can be false, so this is not the same as having the permission), and getEffectivePermissions() lists every node the player has, which you will use for numbered permissions below.
In Brigadier commands: requires
For a Brigadier command, put the check in requires. When the check fails the player cannot run the command, and it disappears from their tab completion. The demo's /kit command does this, and also filters its suggestions by permission:
Where this file livespermissions-demosrcmainjavacomexamplepermissionsdemoKitCommand.java
The package com.example.permissionsdemo is the folder path com/example/permissionsdemo inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- permissions-demo/
- src/main/
- java/com/example/permissionsdemo/Package com.example.permissionsdemo
- HomeLimitCommand.javaCommand (BasicCommand)
- Kit.javaEnum: a fixed list of choices
- KitCommand.javayou are hereCommand (Brigadier tree)
- NumberedPermissions.javaHelper class
- PermissionsDemoPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/permissionsdemo/Package com.example.permissionsdemo
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
22static LiteralCommandNode<CommandSourceStack> create() {23 return Commands.literal("kit")24 .requires(source -> source.getSender().hasPermission("permissionsdemo.use"))25 .executes(KitCommand::listKits)26 .then(Commands.argument("name", StringArgumentType.word())27 .suggests(KitCommand::suggestKits)28 .executes(KitCommand::giveKit))29 .build();30}- A test that runs before anything below it. If it is false, the command is hidden from this player entirely.
- The sender is whoever ran the command. This lambda asks them for the node.
- Plain
/kitlists the kits. - The kit name the player types after
/kit. - Fills the tab-completion list. The method below only suggests kits this player is allowed to take.
In simple commands: permission()
With a BasicCommand, override permission() and return the node. Paper checks it for you before execute runs, so your code never sees a player who is not allowed. The demo's /homelimit command works this way:
Where this file livespermissions-demosrcmainjavacomexamplepermissionsdemoHomeLimitCommand.java
The package com.example.permissionsdemo is the folder path com/example/permissionsdemo inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- permissions-demo/
- src/main/
- java/com/example/permissionsdemo/Package com.example.permissionsdemo
- HomeLimitCommand.javayou are hereCommand (BasicCommand)
- Kit.javaEnum: a fixed list of choices
- KitCommand.javaCommand (Brigadier tree)
- NumberedPermissions.javaHelper class
- PermissionsDemoPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/permissionsdemo/Package com.example.permissionsdemo
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
24@Override25public String permission() {26 return "permissionsdemo.use";27}- Paper hides the command from, and refuses it for, anyone without this node.
Inside event handlers: if and return
Events have no requires, so check by hand. You saw this on the events page, where players without a permission were stopped from mining diamond ore: check, cancel, tell the player.
What players see when they are denied
A command hidden with requires or permission() behaves, for that player, as if it did not exist: it does not show up in tab completion, and typing it usually gives Minecraft's normal error for a command it does not know. That is good for secrets and bad for explaining. When you want to tell the player what is missing, check inside the command instead and send a message, as the kit command does:
Where this file livespermissions-demosrcmainjavacomexamplepermissionsdemoKitCommand.java
The package com.example.permissionsdemo is the folder path com/example/permissionsdemo inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- permissions-demo/
- src/main/
- java/com/example/permissionsdemo/Package com.example.permissionsdemo
- HomeLimitCommand.javaCommand (BasicCommand)
- Kit.javaEnum: a fixed list of choices
- KitCommand.javayou are hereCommand (Brigadier tree)
- NumberedPermissions.javaHelper class
- PermissionsDemoPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/permissionsdemo/Package com.example.permissionsdemo
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
54private static int giveKit(CommandContext<CommandSourceStack> context) {55 if (!(context.getSource().getExecutor() instanceof Player player)) {56 context.getSource().getSender().sendRichMessage("<red>Only players can take a kit.");57 return 0;58 }59 String name = StringArgumentType.getString(context, "name");60 Optional<Kit> found = Kit.byId(name);61 if (found.isEmpty()) {62 player.sendRichMessage("<red>There is no kit called <name>.",63 Placeholder.unparsed("name", name));64 return 0;65 }66 Kit kit = found.get();67 if (!player.hasPermission(kit.permission())) {68 player.sendRichMessage("<red>You do not have access to the <kit> kit.",69 Placeholder.unparsed("kit", kit.id()));70 return 0;71 }72 player.give(kit.items());73 player.sendRichMessage("<green>You received the <kit> kit.",74 Placeholder.unparsed("kit", kit.id()));75 return Command.SINGLE_SUCCESS;76}- An
Optionalis a box that may be empty. It is the safe way to say "maybe there is a kit with that name". - The player typed a name that is not a kit. Say so, with the name they typed.
- The per-kit check. Each kit has its own node, such as
permissionsdemo.kit.vip. - A message that says what is wrong. "No permission" with no detail is the most common complaint players have about servers.
- Brigadier commands return a number.
0means the command did not succeed, andCommand.SINGLE_SUCCESS(which is 1) means it did. - Puts the items in the player's inventory. Items that do not fit are dropped at their feet.
The kits themselves are an enum, and the permission node of each kit is built from its name, so adding a kit later is one new line:
Where this file livespermissions-demosrcmainjavacomexamplepermissionsdemoKit.java
The package com.example.permissionsdemo is the folder path com/example/permissionsdemo inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- permissions-demo/
- src/main/
- java/com/example/permissionsdemo/Package com.example.permissionsdemo
- HomeLimitCommand.javaCommand (BasicCommand)
- Kit.javayou are hereEnum: a fixed list of choices
- KitCommand.javaCommand (Brigadier tree)
- NumberedPermissions.javaHelper class
- PermissionsDemoPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/permissionsdemo/Package com.example.permissionsdemo
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
18String permission() {19 return "permissionsdemo.kit." + id();20}- Builds
permissionsdemo.kit.starter,permissionsdemo.kit.vipand so on from the kit's name.
- starter
- vip
- builder
You do not have access to the vip kit.
The green kits are the ones this player may take; the dark gray ones are locked. A player with the starter kit only gets the first one, and the denial appears when they try /kit vip anyway.
Operators and permission plugins
Out of the box, a Minecraft server knows two kinds of people: operators (ops) and everyone else. An op is made with /op Steve in the console, and has the nodes declared default: op, plus any undeclared ones. That is too crude for most servers. So almost every server installs a permissions plugin. The most popular is LuckPerms. It lets the owner create groups (such as default, vip, moderator), put nodes in groups and players in groups.
You write your plugin only against hasPermission. It works with any permissions plugin, because they all plug into the same system. Server owners then do the rest with commands such as these, typed in the console or in game by an operator:
| Command | What it does |
|---|---|
/lp user Steve permission set permissionsdemo.kit.vip true | Gives Steve the node. |
/lp user Steve permission set permissionsdemo.kit.vip false | Explicitly denies it, which beats any default or group. |
/lp user Steve permission unset permissionsdemo.kit.vip | Removes the setting again, so the defaults apply. |
/lp user Steve permission check permissionsdemo.kit.vip | Shows whether Steve has the node, and why. |
/lp creategroup vip | Creates a group called vip. |
/lp group vip permission set permissionsdemo.kit.vip true | Gives the node to everyone in the group. |
/lp user Steve parent add vip | Puts Steve in the group. |
Limits that depend on rank: numbered permissions
A common request: "regular players may set 1 home, VIPs 5, and the top rank 20". A yes/no permission cannot say that. The classic solution is numbered nodes: the server owner gives permissionsdemo.homes.limit.5 to the VIP group and permissionsdemo.homes.limit.20 to the top group, and the plugin reads the number out of the node name.
Paper has no method for "give me all permissions starting with this". So you loop over the player's getEffectivePermissions(), which lists everything the player has, and pick out the numbers:
Where this file livespermissions-demosrcmainjavacomexamplepermissionsdemoNumberedPermissions.java
The package com.example.permissionsdemo is the folder path com/example/permissionsdemo inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- permissions-demo/
- src/main/
- java/com/example/permissionsdemo/Package com.example.permissionsdemo
- HomeLimitCommand.javaCommand (BasicCommand)
- Kit.javaEnum: a fixed list of choices
- KitCommand.javaCommand (Brigadier tree)
- NumberedPermissions.javayou are hereHelper class
- PermissionsDemoPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/permissionsdemo/Package com.example.permissionsdemo
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
13static int highest(Player player, String prefix, int fallback) {14 int best = fallback;15 for (PermissionAttachmentInfo info : player.getEffectivePermissions()) {16 if (!info.getValue() || !info.getPermission().startsWith(prefix)) {17 continue;18 }19 String suffix = info.getPermission().substring(prefix.length());20 if (suffix.isEmpty() || suffix.length() > MAX_DIGITS || !suffix.chars().allMatch(Character::isDigit)) {21 continue;22 }23 best = Math.max(best, Integer.parseInt(suffix));24 }25 return best;26}- The answer if the player has no numbered node at all, such as 1 home for everyone.
- Every node the player has right now, as a set of
PermissionAttachmentInfoobjects. Each one holds a node name and a value. - Skip nodes that are explicitly
false. A denied node must not count. - Only look at nodes of the right family, like
permissionsdemo.homes.limit.. - Cuts the prefix off, leaving only the end, such as
5. - Makes sure the rest really is a number. A node like
homes.limit.*is skipped instead of crashing the parse. - If a player has several numbers, for example from two groups, the highest wins.
Where this file livespermissions-demosrcmainjavacomexamplepermissionsdemoHomeLimitCommand.java
The package com.example.permissionsdemo is the folder path com/example/permissionsdemo inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- permissions-demo/
- src/main/
- java/com/example/permissionsdemo/Package com.example.permissionsdemo
- HomeLimitCommand.javayou are hereCommand (BasicCommand)
- Kit.javaEnum: a fixed list of choices
- KitCommand.javaCommand (Brigadier tree)
- NumberedPermissions.javaHelper class
- PermissionsDemoPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/permissionsdemo/Package com.example.permissionsdemo
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
13@Override14public void execute(CommandSourceStack source, String[] args) {15 if (!(source.getExecutor() instanceof Player player)) {16 source.getSender().sendRichMessage("<red>Only players have a home limit.");17 return;18 }19 int limit = NumberedPermissions.highest(player, LIMIT_PREFIX, DEFAULT_LIMIT);20 player.sendRichMessage("<gray>You can set up to <white><limit></white> homes.",21 Placeholder.unparsed("limit", String.valueOf(limit)));22}- One call turns the player's rank into a number. A homes plugin would compare this number with how many homes the player already has.
You can set up to 10 homes.
The first line is for a player with no numbered node; the second after /lp user Steve permission set permissionsdemo.homes.limit.10 true (or, for an operator on the test server, thanks to the declared nodes). The same trick works for any rank-based number: cooldown lengths, maximum claims, number of warps. LuckPerms has a nicer tool, called meta, but reading it needs LuckPerms' own API; numbered nodes work with every permissions plugin.
The whole demo plugin
- permissions-demo/
- src/
- main/
- java/
- com/example/permissionsdemo/
- PermissionsDemoPlugin.javaRegisters /kit and /homelimit
- Kit.javaThe three kits and the node name of each
- KitCommand.javaBrigadier /kit with requires, suggestions and denial messages
- NumberedPermissions.javaReads the highest number from nodes like homes.limit.5
- HomeLimitCommand.java/homelimit
- com/example/permissionsdemo/
- resources/
- plugin.ymlDeclares nodes, defaults and children
- java/
- main/
- src/
Download the permissions demo as a Gradle project and run it. As an operator, /kit shows all three kits unlocked, because the demo's wildcard is default: op. To see the locked state, put LuckPerms on the server and take the node away with /lp user YourName permission set permissionsdemo.kit.vip false.
Mistakes to avoid
- Forgetting to declare a default. A node you never declare is
opby default. That is safe, but regular players will not understand why a feature does nothing. Declaredefault: truefor features everybody should have. - Typos in the node.
"myplugin.heal"in plugin.yml and"myplugin.heel"in code are two unrelated permissions, and Java cannot warn you. Keep node names in constants, as the demo does forLIMIT_PREFIX. - Checking
isOp()instead of a permission. It locks out every moderator who is not an operator. Check a permission and let the owner decide. - Checking the wildcard in code. Do not write
hasPermission("myplugin.*"). Check the specific node and let the wildcard grant it. - Silent denial. A command that does nothing for players without a permission feels broken. Tell them what is missing, unless you want the command hidden.
- Assuming the console is a player. The console has every permission, but it is not a
Player. A command that casts the sender toPlayermust check first. - Thinking
default: opmeans "no wildcard needed". Operators only get the nodes you declared (or never declared). A node declareddefault: falseis denied even to operators, unless something grants it on purpose: a permissions plugin, or a parent node that lists it as a child, like the demo'spermissionsdemo.kit.*.
Warmups by rank
Write /spawn. It teleports the player to the world spawn after a warmup, and the warmup length depends on rank: the player waits 5 seconds by default, but if they have nodes like permissionsexercise.warmup.2 or permissionsexercise.warmup.0, they wait that many seconds. If a player has several, the shortest wait wins.
Hint 1
The demo's NumberedPermissions.highest keeps the largest number. What single change makes it keep the smallest instead? Rename the method to match.
Hint 2
Use runTaskLater from the scheduler page to wait, and check player.isOnline() before teleporting. Convert seconds to ticks by multiplying by 20.
Show the solution
The helper is the same loop with Math.min, and its fallback (5) is the longest wait a player without any node gets:
Where this file livespermissions-exercisesrcmainjavacomexamplepermissionsexerciseNumberedPermissions.java
The package com.example.permissionsexercise is the folder path com/example/permissionsexercise inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- permissions-exercise/
- src/main/
- java/com/example/permissionsexercise/Package com.example.permissionsexercise
- ExercisePlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- NumberedPermissions.javayou are hereHelper class
- SpawnCommand.javaCommand (BasicCommand)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/permissionsexercise/Package com.example.permissionsexercise
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
13static int lowest(Player player, String prefix, int fallback) {14 int best = fallback;15 for (PermissionAttachmentInfo info : player.getEffectivePermissions()) {16 if (!info.getValue() || !info.getPermission().startsWith(prefix)) {17 continue;18 }19 String suffix = info.getPermission().substring(prefix.length());20 if (suffix.isEmpty() || suffix.length() > MAX_DIGITS || !suffix.chars().allMatch(Character::isDigit)) {21 continue;22 }23 best = Math.min(best, Integer.parseInt(suffix));24 }25 return best;26}- Keeps the smaller of the two numbers, so the best rank has the shortest wait.
And the command uses it to decide how long to delay the teleport:
Where this file livespermissions-exercisesrcmainjavacomexamplepermissionsexerciseSpawnCommand.java
The package com.example.permissionsexercise is the folder path com/example/permissionsexercise inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- permissions-exercise/
- src/main/
- java/com/example/permissionsexercise/Package com.example.permissionsexercise
- ExercisePlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- NumberedPermissions.javaHelper class
- SpawnCommand.javayou are hereCommand (BasicCommand)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/permissionsexercise/Package com.example.permissionsexercise
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
21@Override22public void execute(CommandSourceStack source, String[] args) {23 if (!(source.getExecutor() instanceof Player player)) {24 source.getSender().sendRichMessage("<red>Only players can teleport.");25 return;26 }27 int warmupSeconds = NumberedPermissions.lowest(player, WARMUP_PREFIX, DEFAULT_WARMUP_SECONDS);28 player.sendRichMessage("<gray>Teleporting to spawn in <white><seconds>s</white>...",29 Placeholder.unparsed("seconds", String.valueOf(warmupSeconds)));30 31 plugin.getServer().getScheduler().runTaskLater(plugin, () -> {32 if (player.isOnline()) {33 player.teleport(player.getWorld().getSpawnLocation());34 }35 }, warmupSeconds * TICKS_PER_SECOND);36}- The shortest warmup among the player's nodes, or the default of 5.
- Waits without freezing the server, then teleports if the player is still around.
- The spawn point of the world the player is in.
Choose the default
For each feature, decide which default fits best (true, false, op or not op) and say why: (1) /spawn that everyone uses, (2) /reload-config for the owner, (3) a feature you are still testing and want nobody to have by accident, (4) a "tutorial hints" feature that experienced staff do not want.
Show the solution
(1) true: everyone should be able to use it. (2) op: only the server owner has it by default and can hand it out. (3) false: not even operators have it until someone grants it on purpose. (4) not op: it is on for regular players and off for operators, who can still be given the opposite with a permissions plugin.
Recap
- A permission is a dotted text node such as
plugin.feature.action. Players have it or not; the server owner decides who. - Declare nodes in
plugin.ymlwith adescription, adefault(true,false,op,not op) and optionalchildren. Wildcards are nodes with children. - Check with
hasPermission, withrequiresin Brigadier commands and withpermission()inBasicCommand. - A value set by a permissions plugin always beats the default from
plugin.yml. - Never rely on
isOp(); check permissions, and tell players what they are missing. - LuckPerms hands out nodes with
/lp user ... permission setand groups with/lp group .... - Numbered nodes (
homes.limit.5) give ranks different limits: loop overgetEffectivePermissions()and read the number.
Quick quiz
A node is declared in
plugin.ymlwithdefault: true. LuckPerms explicitly sets it tofalsefor Steve. What doessteve.hasPermission(node)return?The default only applies when nobody has set the node for that player. Once LuckPerms sets it, that value is used.You use a permission node in code but never declare it in
plugin.yml. Who has it by default?Undeclared nodes are treated asop. Declare the node withdefault: trueif everyone should have it.What is the best way to restrict a Brigadier command to players with a node?
requireshides the command from players without the node.isOp()ignores permission plugins entirely, and a description is only text.Why does the home limit code skip a node like
permissionsdemo.homes.limit.*?The code only accepts suffixes made of digits. Without that check, parsing*as a number would throw an exception.A player is in two groups, one with
homes.limit.5and one withhomes.limit.20. How many homes does the demo allow?The helper keeps the highest number it finds, so the better rank wins. It does not add the numbers.
Next steps
- Patterns: cooldowns, toggles and player state: combine permissions with cooldowns so each rank waits a different time.
- Commands, part 2: Brigadier command trees: more on
requiresand suggestions. - Project: Homes: use numbered limits in a complete plugin.
- The LuckPerms wiki: everything you can do on the server owner's side.