Implements world-specific permissions

Accounts for world specifiers where necessary
Makes the reload command work?
Stores the world as part of temporary permissions
Fixes sign break protection
Allows sign interaction with both right- and left-click
Fixes temporary permissions sometimes colliding
Cleans the main class a bit
Adds a "star" permission
Adds missing check for the use permission
This commit is contained in:
Kristian Knarvik 2022-01-15 14:49:46 +01:00
parent 112fd0d53d
commit 67e967a264
11 changed files with 243 additions and 99 deletions

View File

@ -5,6 +5,7 @@ import net.knarcraft.permissionsigns.command.PermissionSignsTabCompleter;
import net.knarcraft.permissionsigns.container.PermissionSign;
import net.knarcraft.permissionsigns.container.SignCreationRequest;
import net.knarcraft.permissionsigns.formatting.Translator;
import net.knarcraft.permissionsigns.listener.BlockListener;
import net.knarcraft.permissionsigns.listener.PlayerListener;
import net.knarcraft.permissionsigns.listener.SignListener;
import net.knarcraft.permissionsigns.manager.EconomyManager;
@ -16,8 +17,10 @@ import net.milkbowl.vault.economy.Economy;
import net.milkbowl.vault.permission.Permission;
import org.bukkit.Bukkit;
import org.bukkit.command.PluginCommand;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.plugin.PluginDescriptionFile;
import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.RegisteredServiceProvider;
import org.bukkit.plugin.ServicesManager;
import org.bukkit.plugin.java.JavaPlugin;
@ -35,6 +38,7 @@ public final class PermissionSigns extends JavaPlugin {
private static final Queue<SignCreationRequest> signCreationRequests = new PriorityQueue<>();
private static String pluginVersion;
private static PermissionSigns instance;
private static boolean perWorldPermissions;
/**
* Instantiates the permission signs class
@ -104,57 +108,74 @@ public final class PermissionSigns extends JavaPlugin {
}
}
/**
* Checks whether permissions should always be set for the world the sign belongs to
*
* @return <p>Whether permissions should be set for the current world</p>
*/
public static boolean usePerWorldPermissions() {
return perWorldPermissions;
}
@Override
public void reloadConfig() {
super.reloadConfig();
FileConfiguration config = this.getConfig();
String language = config.getString("language");
perWorldPermissions = config.getBoolean("perWorldPermissions");
Translator.loadLanguages(language);
}
@Override
public void onEnable() {
PluginDescriptionFile pluginDescriptionFile = this.getDescription();
pluginVersion = pluginDescriptionFile.getVersion();
// Plugin startup logic
//TODO: Add commands create, add and remove
// /ps create, /ps add, and /ps remove
// create initiates the creation, and add adds properties
// On creation, write "Creating PermissionSign", first asked for the name
// Then asked for the permission. Allow several comma-separated permissions
// Then asked for duration
// Then asked for cost
// Say "Sign completed! Right-click a sign to enable it."
// Then asked to right-click a sign to create the new permission-sign
// Perhaps ignore the old ways, and just have one command for creating permission signs:
// /ps create <name> <permission> <duration> <cost> to create a new permission-sign
// Right-click a sign to create it
// /ps cancel to cancel the sing creation
// Break the sign to remove it, check for permission first
// The name thing is probably useless, as the sign's location works as its id
//TODO: Display and register the permission-sign
// Start with [PermSign] in red
// Next line is the permission node. Last child, upper-cased
// Third line is n seconds
// Last line is the cost, including the unit
// Need to store any temporary permissions in a list/queue and have a thread which searches for expired
// permissions to de-register them
//Not persistent, but might work as things shouldn't persist anyway
//player.addAttachment(this, "essentials.fly", true, seconds * 20);
//Vault probably has some API to add permissions
//TODO: Store all temporary permissions with the time they were assigned and the time they should be granted.
// Set the permissions as permanent and remove when the timer expires? Might cause problems for players that has
// the permission inherited, unless we make sure the player does not have the permission before
// Alternatively: Manage all permissions ourselves, for temporary permissions, and add permission attachments
// on startup I guess.
//TODO: Start sign creation when the create command is used and save the data until an empty sign is right-clicked
//TODO: Check for existence of old permission signs when clicked and register them as new permission signs. If
// it has the permissionSigns header and a name matching contents in signs.yml, add it.
//TODO: Implement config file
//TODO: Account for per-world permissions if enabled (perhaps allow world specification as world:permission?)
//TODO: Allow for custom language files. Perhaps just look for strings.yml in the folder
FileConfiguration config = this.getConfig();
config.options().copyDefaults(true);
this.saveDefaultConfig();
String language = config.getString("language");
perWorldPermissions = config.getBoolean("perWorldPermissions");
//Check if vault is loaded
setupVault();
registerListeners();
Translator.loadLanguages(language);
registerCommands();
runThreads();
SignManager.loadSigns();
PermissionManager.loadTemporaryPermissions();
}
/**
* Starts all separate threads for executing tasks
*/
private void runThreads() {
BukkitScheduler scheduler = Bukkit.getScheduler();
scheduler.runTaskTimer(this, new SignCreationRequestTimeoutThread(signCreationRequests), 0L, 100L);
scheduler.runTaskTimer(this, new PermissionTimeoutThread(), 0L, 25L);
}
/**
* Registers all necessary listeners
*/
private void registerListeners() {
PluginManager pluginManager = getServer().getPluginManager();
pluginManager.registerEvents(new SignListener(), this);
pluginManager.registerEvents(new PlayerListener(), this);
pluginManager.registerEvents(new BlockListener(), this);
}
/**
* Sets up Vault by getting plugins from their providers
*/
private void setupVault() {
ServicesManager servicesManager = this.getServer().getServicesManager();
RegisteredServiceProvider<Permission> permissionProvider = servicesManager.getRegistration(Permission.class);
RegisteredServiceProvider<Economy> economyProvider = servicesManager.getRegistration(Economy.class);
@ -164,16 +185,6 @@ public final class PermissionSigns extends JavaPlugin {
} else {
throw new IllegalStateException("[PermissionSigns] Error: Vault could not be loaded");
}
getServer().getPluginManager().registerEvents(new SignListener(), this);
getServer().getPluginManager().registerEvents(new PlayerListener(), this);
Translator.loadLanguages("en");
registerCommands();
BukkitScheduler scheduler = Bukkit.getScheduler();
scheduler.runTaskTimer(this, new SignCreationRequestTimeoutThread(signCreationRequests), 0L, 100L);
scheduler.runTaskTimer(this, new PermissionTimeoutThread(), 0L, 25L);
SignManager.loadSigns();
PermissionManager.loadTemporaryPermissions();
}
/**

View File

@ -4,6 +4,7 @@ import net.knarcraft.permissionsigns.PermissionSigns;
import net.knarcraft.permissionsigns.container.PermissionSign;
import net.knarcraft.permissionsigns.formatting.StringFormatter;
import net.knarcraft.permissionsigns.formatting.TranslatableMessage;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
@ -57,6 +58,18 @@ public class CreateCommand implements CommandExecutor {
private PermissionSign parseSign(@NotNull CommandSender sender, @NotNull String[] args) {
String name = args[0];
String[] permissions = args[1].split(",");
for (String permission : permissions) {
if (permission.contains(":")) {
//TODO: Might need to account for world names with spaces in the name
String world = permission.split(":")[0];
if (!world.equalsIgnoreCase("all") && Bukkit.getWorld(world) == null) {
sender.sendMessage(StringFormatter.replacePlaceholder(StringFormatter.getTranslatedErrorMessage(
TranslatableMessage.PERMISSION_WORLD_INVALID), "{world}", world));
return null;
}
}
}
double cost = 0;
int duration = 0;
if (args.length > 2) {

View File

@ -1,7 +1,9 @@
package net.knarcraft.permissionsigns.command;
import net.knarcraft.permissionsigns.PermissionSigns;
import net.knarcraft.permissionsigns.formatting.StringFormatter;
import net.knarcraft.permissionsigns.formatting.TranslatableMessage;
import net.knarcraft.permissionsigns.manager.PermissionManager;
import net.knarcraft.permissionsigns.manager.SignManager;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
@ -16,8 +18,9 @@ public class ReloadCommand implements CommandExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (sender.hasPermission("permissionsigns.admin.reload")) {
//TODO: Perform a reload from disk
SignManager.loadSigns();
PermissionManager.loadTemporaryPermissions();
PermissionSigns.getInstance().reloadConfig();
} else {
sender.sendMessage(StringFormatter.getTranslatedErrorMessage(TranslatableMessage.COMMAND_PERMISSION_DENIED));
}

View File

@ -13,6 +13,7 @@ public class TemporaryPermission implements Comparable<TemporaryPermission> {
private final OfflinePlayer grantedPlayer;
private final String permissionNode;
private final int duration;
private final String world;
/**
* Instantiates a new temporary permission
@ -20,14 +21,14 @@ public class TemporaryPermission implements Comparable<TemporaryPermission> {
* @param player <p>The player the temporary permission was assigned to</p>
* @param permissionNode <p>The permission node granted to the player</p>
* @param duration <p>The duration, in seconds, the temporary permission should last</p>
* @param world <p>The world the permission should be added to</p>
*/
public TemporaryPermission(Player player, String permissionNode, int duration) {
public TemporaryPermission(Player player, String permissionNode, int duration, String world) {
grantedTime = System.currentTimeMillis();
this.grantedPlayer = player;
this.permissionNode = permissionNode;
this.duration = duration;
//TODO: Need to account for world. Perhaps have a config option to choose between setting permissions for all
// worlds, or just the world the sign and player is in
this.world = world;
}
/**
@ -37,12 +38,14 @@ public class TemporaryPermission implements Comparable<TemporaryPermission> {
* @param permissionNode <p>The permission node granted to the player</p>
* @param grantedTime <p>The time this temporary permission was granted</p>
* @param duration <p>The duration, in seconds, the temporary permission should last</p>
* @param world <p>The world the permission should be added to</p>
*/
public TemporaryPermission(OfflinePlayer player, String permissionNode, long grantedTime, int duration) {
public TemporaryPermission(OfflinePlayer player, String permissionNode, long grantedTime, int duration, String world) {
this.grantedPlayer = player;
this.permissionNode = permissionNode;
this.grantedTime = grantedTime;
this.duration = duration;
this.world = world;
}
/**
@ -81,6 +84,14 @@ public class TemporaryPermission implements Comparable<TemporaryPermission> {
return permissionNode;
}
/**
* Gets the world the permission node should be added to
*
* @return <p>The world the permission node should be added to</p>
*/
public String getWorld() {
return world;
}
@Override
public int compareTo(@NotNull TemporaryPermission other) {

View File

@ -90,6 +90,16 @@ public enum TranslatableMessage {
*/
PERMISSIONS_GRANTED,
/**
* The message to display if a player has specified an invalid world to set a permission for
*/
PERMISSION_WORLD_INVALID,
/**
* The message to display if a player is missing the permission required for interacting with permission signs
*/
INTERACT_PERMISSION_MISSING,
/**
* The message to display when a permission sign has been successfully created
*/

View File

@ -0,0 +1,48 @@
package net.knarcraft.permissionsigns.listener;
import net.knarcraft.permissionsigns.formatting.StringFormatter;
import net.knarcraft.permissionsigns.formatting.TranslatableMessage;
import net.knarcraft.permissionsigns.manager.SignManager;
import org.bukkit.Material;
import org.bukkit.Tag;
import org.bukkit.block.Block;
import org.bukkit.block.Sign;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent;
public class BlockListener implements Listener {
@EventHandler
public void onBlockBreak(BlockBreakEvent event) {
Block block = event.getBlock();
Player player = event.getPlayer();
if (event.isCancelled()) {
return;
}
Material material = block.getBlockData().getMaterial();
if (!Tag.SIGNS.isTagged(material) && !Tag.WALL_SIGNS.isTagged(material)) {
return;
}
Sign sign = (Sign) block.getState();
boolean registered = SignManager.getSign(sign.getLocation()) != null;
if (!registered) {
return;
}
if (!player.hasPermission("permissionsigns.admin.create")) {
event.setCancelled(true);
player.sendMessage(StringFormatter.getTranslatedErrorMessage(TranslatableMessage.PERMISSION_SIGN_DESTROY_DENY));
} else {
SignManager.removeSign(sign.getLocation());
player.sendMessage(StringFormatter.getTranslatedInfoMessage(TranslatableMessage.PERMISSION_SIGN_REMOVED));
}
//TODO: Need to protect against other things that might damage the sign, such as explosions
//TODO: Only allow the sign to be broken if shift-clicking?
}
}

View File

@ -20,7 +20,7 @@ public class PlayerListener implements Listener {
Queue<TemporaryPermission> temporaryPermissions = new PriorityQueue<>(PermissionManager.getTemporaryPermissions());
temporaryPermissions.removeIf((item) -> !item.getGrantedPlayer().getUniqueId().equals(event.getPlayer().getUniqueId()));
temporaryPermissions.forEach((item) -> PermissionManager.grantTemporaryPermission(event.getPlayer(),
item.getPermissionNode()));
item.getPermissionNode(), item.getWorld()));
}
}

View File

@ -16,7 +16,6 @@ import org.bukkit.block.Sign;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.player.PlayerInteractEvent;
import java.util.Arrays;
@ -46,32 +45,7 @@ public class SignListener implements Listener {
}
Sign sign = (Sign) block.getState();
if (event.getAction() == Action.RIGHT_CLICK_BLOCK) {
handleSignRightClick(sign, player);
} else if (event.getAction() == Action.LEFT_CLICK_BLOCK) {
handleSignLeftClick(sign, player, event);
}
}
/**
* Handles the left click of a sign that might be a permission sign
*
* @param sign <p>The clicked sign</p>
* @param player <p>The player that clicked the sign</p>
* @param event <p>The triggered interaction event</p>
*/
private void handleSignLeftClick(Sign sign, Player player, PlayerInteractEvent event) {
boolean registered = SignManager.getSign(sign.getLocation()) != null;
if (!registered) {
return;
}
if (!player.hasPermission("permissionsigns.admin")) {
event.setCancelled(true);
player.sendMessage(StringFormatter.getTranslatedErrorMessage(TranslatableMessage.PERMISSION_SIGN_DESTROY_DENY));
} else {
SignManager.removeSign(sign.getLocation());
player.sendMessage(StringFormatter.getTranslatedInfoMessage(TranslatableMessage.PERMISSION_SIGN_REMOVED));
}
handleSignClick(sign, player);
}
/**
@ -80,13 +54,18 @@ public class SignListener implements Listener {
* @param sign <p>The clicked sign</p>
* @param player <p>The player that clicked the sign</p>
*/
private void handleSignRightClick(Sign sign, Player player) {
private void handleSignClick(Sign sign, Player player) {
//Check if the sign is a registered permission sign
PermissionSign permissionSign = SignManager.getSign(sign.getLocation());
if (permissionSign != null) {
if (!player.hasPermission("permissionsigns.use")) {
player.sendMessage(StringFormatter.getTranslatedErrorMessage(TranslatableMessage.INTERACT_PERMISSION_MISSING));
return;
}
handlePermissionSignInteract(permissionSign, player);
return;
}
//TODO: Display information about granted permissions when shift-clicking?
//Check if the player has a creation request that can be fulfilled
SignCreationRequest request = PermissionSigns.getSignCreationRequest(player.getUniqueId());
@ -105,7 +84,12 @@ public class SignListener implements Listener {
//Don't allow players to pay for permissions they already own
boolean hasAllPermissions = true;
for (String permissionNode : permissionSign.getPermissionNodes()) {
if (!player.hasPermission(permissionNode)) {
if (permissionNode.contains(":")) {
String[] permissionParts = permissionNode.split(":");
if (!PermissionManager.hasPermission(player, permissionParts[1], permissionParts[0])) {
hasAllPermissions = false;
}
} else if (!player.hasPermission(permissionNode)) {
hasAllPermissions = false;
}
}
@ -132,7 +116,14 @@ public class SignListener implements Listener {
StringBuilder permissionsBuilder = new StringBuilder();
for (String permissionNode : permissionSign.getPermissionNodes()) {
//Only grant the permission if not already owned
if (!player.hasPermission(permissionNode)) {
boolean hasPermission;
if (permissionNode.contains(":")) {
String[] permissionParts = permissionNode.split(":");
hasPermission = PermissionManager.hasPermission(player, permissionParts[1], permissionParts[0]);
} else {
hasPermission = player.hasPermission(permissionNode);
}
if (!hasPermission) {
permissionsBuilder.append(permissionNode);
permissionsBuilder.append(", ");
if (permissionSign.getDuration() == 0) {

View File

@ -12,6 +12,7 @@ import org.bukkit.entity.Player;
import java.io.File;
import java.io.IOException;
import java.util.Queue;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.logging.Level;
@ -50,8 +51,13 @@ public class PermissionManager {
* @param permissionNode <p>The permission node to grant to the player</p>
*/
public static void addPermission(Player player, String permissionNode) {
//TODO: Account for world when granting permissions, if wanted
permission.playerAdd(null, player, permissionNode);
//Allow world:permission syntax
if (permissionNode.contains(":")) {
grantWorldPermission(player, permissionNode, false);
} else {
boolean perWorldPermissions = PermissionSigns.usePerWorldPermissions();
permission.playerAdd(perWorldPermissions ? player.getWorld().getName() : null, player, permissionNode);
}
}
/**
@ -59,9 +65,36 @@ public class PermissionManager {
*
* @param player <p>The player to add the temporary permission to</p>
* @param permissionNode <p>The permission node to grant</p>
* @param worldName <p>The world to add the permission node to</p>
*/
public static void grantTemporaryPermission(Player player, String permissionNode) {
permission.playerAddTransient(null, player, permissionNode);
public static void grantTemporaryPermission(Player player, String permissionNode, String worldName) {
//Allow world:permission syntax
if (permissionNode.contains(":")) {
grantWorldPermission(player, permissionNode, true);
} else {
boolean perWorldPermissions = PermissionSigns.usePerWorldPermissions();
permission.playerAddTransient(perWorldPermissions ? worldName : null, player, permissionNode);
}
}
/**
* Grants a permission for the world specified in the permission node
*
* @param player <p>The player to grant the permission node to</p>
* @param permissionNode <p>The permission node to grant</p>
* @param temporary <p>Whether the permission node should be set temporarily</p>
*/
private static void grantWorldPermission(Player player, String permissionNode, boolean temporary) {
String[] permissionParts = permissionNode.split(":");
String world = permissionParts[0];
if (world.equalsIgnoreCase("all")) {
world = null;
}
if (temporary) {
permission.playerAddTransient(world, player, permissionParts[1]);
} else {
permission.playerAdd(world, player, permissionParts[1]);
}
}
/**
@ -72,8 +105,9 @@ public class PermissionManager {
* @param duration <p>The duration for which the player should keep the given permission</p>
*/
public static void addTemporaryPermission(Player player, String permissionNode, int duration) {
grantTemporaryPermission(player, permissionNode);
temporaryPermissions.add(new TemporaryPermission(player, permissionNode, duration));
String world = player.getWorld().getName();
grantTemporaryPermission(player, permissionNode, world);
temporaryPermissions.add(new TemporaryPermission(player, permissionNode, duration, world));
try {
saveTemporaryPermissions();
} catch (IOException e) {
@ -95,6 +129,18 @@ public class PermissionManager {
}
}
/**
* Checks whether the given player has the given permission
*
* @param player <p>The player to check</p>
* @param permissionNode <p>The permission node to check for</p>
* @param world <p>The world to check for the permission</p>
* @return <p>True if the player has the permission</p>
*/
public static boolean hasPermission(Player player, String permissionNode, String world) {
return permission.playerHas(world, player, permissionNode);
}
/**
* Saves all temporary permissions to a file
*
@ -103,11 +149,14 @@ public class PermissionManager {
public static void saveTemporaryPermissions() throws IOException {
YamlConfiguration configuration = YamlConfiguration.loadConfiguration(permissionsFile);
ConfigurationSection permissionSection = configuration.createSection("permissions");
Random random = new Random();
temporaryPermissions.forEach((item) -> {
String key = item.getGrantedPlayer().getUniqueId() + "," + item.getGrantedTime();
//A random long is added to prevent any conflicts
String key = item.getGrantedPlayer().getUniqueId() + "," + item.getGrantedTime() + "," + random.nextLong();
permissionSection.set(key + ".permissionNode", item.getPermissionNode());
permissionSection.set(key + ".duration", item.getGrantedDuration());
permissionSection.set(key + ".world", item.getWorld());
});
configuration.save(permissionsFile);
}
@ -131,13 +180,13 @@ public class PermissionManager {
String permissionNode = permissionSection.getString(key + ".permissionNode");
long granted = Long.parseLong(identifierParts[1]);
int duration = permissionSection.getInt(key + ".duration", 1);
String world = permissionSection.getString(key + ".world", null);
//Skip any expired temporary permissions
if (currentTime > granted + (1000L * duration)) {
continue;
}
//TODO: Need to wait for the player to join the server before allocating transient permissions
temporaryPermissions.add(new TemporaryPermission(player, permissionNode, granted, duration));
temporaryPermissions.add(new TemporaryPermission(player, permissionNode, granted, duration, world));
}
try {
saveTemporaryPermissions();

View File

@ -18,6 +18,12 @@ commands:
description: Used for all permission sign commands
usage: /<command> <about/create/cancel/reload> - Used for all permission sign commands
permissions:
permissionsigns.*:
description: Grants all PermissionSigns permissions
default: false
children:
permissionsigns.use: true
permissionsigns.admin: true
permissionsigns.use:
description: Allows players to use the permission signs
default: true

View File

@ -16,4 +16,6 @@ en:
ALREADY_HAS_PERMISSIONS: "&7You already have all permissions given by this permission sign"
PERMISSIONS_GRANTED: "&7You have been granted the following permission nodes: {permissions} for {time}"
PERMISSION_SIGN_CREATED: "&7Permission sign successfully created!"
PERMISSION_WORLD_INVALID: "&7The world {world} is not a valid world name on this server!"
INTERACT_PERMISSION_MISSING: "&7You do not have the necessary permissions required to use permission signs"
CREATION_REQUEST_CANCELLED: "&7Your last permission sign creation request has been cancelled"