mirror of
https://github.com/Brettflan/WorldBorder.git
synced 2025-02-12 17:41:26 +01:00
New fill command, which generates all missing chunks within a world's border along with a configurable buffer beyond the border; can be run at different speeds as well, to allow for running on a server with players on it or to have it finish as quickly as possible
Support for newly available built-in Bukkit "superperms" permission system; uses the same nodes as for the Permissions plugin Support for colors in console messages Knockback distance is now required to be at least 1.0 A couple of other tweaks
This commit is contained in:
parent
c45c48be5c
commit
e2e1b60e2a
@ -45,6 +45,11 @@ public class BorderData
|
||||
this.DefiniteSquare = Math.sqrt(.5 * this.radiusSquared);
|
||||
}
|
||||
|
||||
public BorderData copy()
|
||||
{
|
||||
return new BorderData(x, z, radius, shapeRound);
|
||||
}
|
||||
|
||||
public double getX()
|
||||
{
|
||||
return x;
|
||||
@ -178,12 +183,12 @@ public class BorderData
|
||||
}
|
||||
|
||||
//these material IDs are acceptable for places to teleport player; breathable blocks and water
|
||||
private static LinkedHashSet<Integer> safeOpenBlocks = new LinkedHashSet<Integer>(Arrays.asList(
|
||||
private static final LinkedHashSet<Integer> safeOpenBlocks = new LinkedHashSet<Integer>(Arrays.asList(
|
||||
new Integer[] {0, 6, 8, 9, 27, 28, 31, 32, 37, 38, 39, 40, 50, 55, 59, 63, 64, 65, 66, 68, 69, 70, 71, 72, 75, 76, 77, 78, 83, 90, 93, 94}
|
||||
));
|
||||
|
||||
//these material IDs are ones we don't want to drop the player onto, like cactus or lava or fire
|
||||
private static LinkedHashSet<Integer> painfulBlocks = new LinkedHashSet<Integer>(Arrays.asList(
|
||||
private static final LinkedHashSet<Integer> painfulBlocks = new LinkedHashSet<Integer>(Arrays.asList(
|
||||
new Integer[] {10, 11, 51, 81}
|
||||
));
|
||||
|
||||
@ -198,7 +203,7 @@ public class BorderData
|
||||
);
|
||||
}
|
||||
|
||||
static final private int limTop = 120, limBot = 1;
|
||||
private static final int limTop = 120, limBot = 1;
|
||||
|
||||
// find closest safe Y position from the starting position
|
||||
private double getSafeY(World world, int X, int Y, int Z)
|
||||
|
@ -16,6 +16,9 @@ import org.bukkit.plugin.Plugin;
|
||||
import org.bukkit.util.config.Configuration;
|
||||
import org.bukkit.util.config.ConfigurationNode;
|
||||
|
||||
import org.bukkit.craftbukkit.command.ColouredConsoleSender;
|
||||
import org.bukkit.craftbukkit.CraftServer;
|
||||
|
||||
import org.anjocaido.groupmanager.GroupManager;
|
||||
import com.nijiko.permissions.PermissionHandler;
|
||||
import com.nijikokun.bukkit.Permissions.Permissions;
|
||||
@ -31,8 +34,11 @@ public class Config
|
||||
private static final Logger mcLog = Logger.getLogger("Minecraft");
|
||||
public static DecimalFormat coord = new DecimalFormat("0.0");
|
||||
private static int borderTask = -1;
|
||||
public static WorldFillTask fillTask = null;
|
||||
public static Set<String> movedPlayers = Collections.synchronizedSet(new HashSet<String>());
|
||||
|
||||
private static Runtime rt = Runtime.getRuntime();
|
||||
private static ColouredConsoleSender console = null;
|
||||
|
||||
// actual configuration values which can be changed
|
||||
private static boolean shapeRound = false;
|
||||
private static Map<String, BorderData> borders = Collections.synchronizedMap(new LinkedHashMap<String, BorderData>());
|
||||
@ -41,13 +47,13 @@ public class Config
|
||||
private static double knockBack = 3.0;
|
||||
private static int timerTicks = 4;
|
||||
|
||||
/* // for monitoring plugin efficiency
|
||||
public static long timeUsed = 0;
|
||||
// for monitoring plugin efficiency
|
||||
// public static long timeUsed = 0;
|
||||
|
||||
public static long Now()
|
||||
{
|
||||
return Calendar.getInstance().getTimeInMillis();
|
||||
return System.currentTimeMillis();
|
||||
}
|
||||
*/
|
||||
|
||||
public static void setBorder(String world, BorderData border)
|
||||
{
|
||||
@ -191,6 +197,39 @@ public class Config
|
||||
}
|
||||
|
||||
|
||||
public static void StopFillTask()
|
||||
{
|
||||
if (fillTask != null && fillTask.valid())
|
||||
fillTask.cancel();
|
||||
}
|
||||
|
||||
public static void StoreFillTask()
|
||||
{
|
||||
save(false, true);
|
||||
}
|
||||
public static void UnStoreFillTask()
|
||||
{
|
||||
save(false);
|
||||
}
|
||||
|
||||
public static void RestoreFillTask(String world, int fillDistance, int chunksPerRun, int tickFrequency, int x, int z, int length, int total)
|
||||
{
|
||||
fillTask = new WorldFillTask(plugin.getServer(), null, world, fillDistance, chunksPerRun, tickFrequency);
|
||||
if (fillTask.valid())
|
||||
{
|
||||
fillTask.continueProgress(x, z, length, total);
|
||||
int task = plugin.getServer().getScheduler().scheduleSyncRepeatingTask(plugin, fillTask, 20, tickFrequency);
|
||||
fillTask.setTaskID(task);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static int AvailableMemory()
|
||||
{
|
||||
return (int)((rt.maxMemory() - rt.totalMemory() + rt.freeMemory()) / 1024L / 1024L);
|
||||
}
|
||||
|
||||
|
||||
public static void loadPermissions(WorldBorder plugin)
|
||||
{
|
||||
if (GroupPlugin != null || Permissions != null || plugin == null)
|
||||
@ -224,29 +263,36 @@ public class Config
|
||||
return true;
|
||||
else if (player.isOp()) // Op, always permitted
|
||||
return true;
|
||||
else if (GroupPlugin != null) // GroupManager plugin available
|
||||
|
||||
if (GroupPlugin != null) // GroupManager plugin available
|
||||
{
|
||||
if (GroupPlugin.getWorldsHolder().getWorldPermissions(player).has(player, "worldborder." + request))
|
||||
return true;
|
||||
player.sendMessage("You do not have sufficient permissions to do that.");
|
||||
return false;
|
||||
}
|
||||
else if (Permissions != null) // Permissions plugin available
|
||||
{
|
||||
if (Permissions.permission(player, "worldborder." + request))
|
||||
return true;
|
||||
player.sendMessage("You do not have sufficient permissions to do that.");
|
||||
return false;
|
||||
}
|
||||
else
|
||||
if (player.hasPermission("worldborder." + request)) // built-in Bukkit superperms
|
||||
return true;
|
||||
|
||||
player.sendMessage("You do not have sufficient permissions.");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
private static final String logName = "WorldBorder";
|
||||
public static void Log(Level lvl, String text)
|
||||
{
|
||||
String name = (plugin == null) ? "WorldBorder" : plugin.getDescription().getName();
|
||||
mcLog.log(lvl, String.format("[%s] %s", name, text));
|
||||
if (console != null)
|
||||
{
|
||||
if (lvl != Level.INFO)
|
||||
text = "[" + lvl.getLocalizedName() + "] " + text;
|
||||
console.sendMessage(String.format("[%s] %s", logName, text));
|
||||
}
|
||||
else
|
||||
mcLog.log(lvl, String.format("[%s] %s", logName, text));
|
||||
}
|
||||
public static void Log(String text)
|
||||
{
|
||||
@ -265,7 +311,8 @@ public class Config
|
||||
public static void load(WorldBorder master, boolean logIt)
|
||||
{ // load config from file
|
||||
plugin = master;
|
||||
cfg = plugin.getConfiguration();
|
||||
console = new ColouredConsoleSender((CraftServer)plugin.getServer());
|
||||
cfg = plugin.getConfiguration();
|
||||
|
||||
int cfgVersion = cfg.getInt("cfg-version", 1);
|
||||
|
||||
@ -311,6 +358,22 @@ public class Config
|
||||
}
|
||||
}
|
||||
|
||||
// if we have an unfinished fill task stored from a previous run, load it up
|
||||
ConfigurationNode storedFillTask = cfg.getNode("fillTask");
|
||||
if (storedFillTask != null)
|
||||
{
|
||||
String worldName = storedFillTask.getString("world");
|
||||
int fillDistance = storedFillTask.getInt("fillDistance", 176);
|
||||
int chunksPerRun = storedFillTask.getInt("chunksPerRun", 5);
|
||||
int tickFrequency = storedFillTask.getInt("tickFrequency", 20);
|
||||
int fillX = storedFillTask.getInt("x", 0);
|
||||
int fillZ = storedFillTask.getInt("z", 0);
|
||||
int fillLength = storedFillTask.getInt("length", 0);
|
||||
int fillTotal = storedFillTask.getInt("total", 0);
|
||||
RestoreFillTask(worldName, fillDistance, chunksPerRun, tickFrequency, fillX, fillZ, fillLength, fillTotal);
|
||||
save(false);
|
||||
}
|
||||
|
||||
if (logIt)
|
||||
LogConfig("Configuration loaded.");
|
||||
|
||||
@ -319,6 +382,10 @@ public class Config
|
||||
}
|
||||
|
||||
public static void save(boolean logIt)
|
||||
{
|
||||
save(logIt, false);
|
||||
}
|
||||
public static void save(boolean logIt, boolean storeFillTask)
|
||||
{ // save config to file
|
||||
if (cfg == null) return;
|
||||
|
||||
@ -345,6 +412,20 @@ public class Config
|
||||
cfg.setProperty("worlds." + name.replace(".", "¨") + ".shape-round", bord.getShape());
|
||||
}
|
||||
|
||||
if (storeFillTask && fillTask != null && fillTask.valid())
|
||||
{
|
||||
cfg.setProperty("fillTask.world", fillTask.refWorld());
|
||||
cfg.setProperty("fillTask.fillDistance", fillTask.refFillDistance());
|
||||
cfg.setProperty("fillTask.chunksPerRun", fillTask.refChunksPerRun());
|
||||
cfg.setProperty("fillTask.tickFrequency", fillTask.refTickFrequency());
|
||||
cfg.setProperty("fillTask.x", fillTask.refX());
|
||||
cfg.setProperty("fillTask.z", fillTask.refZ());
|
||||
cfg.setProperty("fillTask.length", fillTask.refLength());
|
||||
cfg.setProperty("fillTask.total", fillTask.refTotal());
|
||||
}
|
||||
else
|
||||
cfg.removeProperty("fillTask");
|
||||
|
||||
cfg.save();
|
||||
|
||||
if (logIt)
|
||||
|
@ -32,7 +32,7 @@ public class WBCommand implements CommandExecutor
|
||||
Player player = (sender instanceof Player) ? (Player)sender : null;
|
||||
|
||||
String cmd = clrCmd + ((player == null) ? "wb" : "/wb");
|
||||
String cmdW = clrCmd + ((player == null) ? "wb " + clrReq + "<world>" : "/wb " + clrOpt + "[world]") + clrCmd;
|
||||
String cmdW = clrCmd + ((player == null) ? "wb " + clrReq + "<world>" : "/wb " + clrOpt + "[world]") + clrCmd;
|
||||
|
||||
// "set" command from player or console, world specified
|
||||
if (split.length == 5 && split[1].equalsIgnoreCase("set"))
|
||||
@ -305,13 +305,13 @@ public class WBCommand implements CommandExecutor
|
||||
}
|
||||
catch(NumberFormatException ex)
|
||||
{
|
||||
sender.sendMessage(clrErr + "The knockback must be a decimal value above 0.");
|
||||
sender.sendMessage(clrErr + "The knockback must be a decimal value of at least 1.0.");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (numBlocks <= 0.0)
|
||||
if (numBlocks < 1.0)
|
||||
{
|
||||
sender.sendMessage(clrErr + "The knockback must be a decimal value above 0.");
|
||||
sender.sendMessage(clrErr + "The knockback must be a decimal value of at least 1.0.");
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -399,6 +399,60 @@ public class WBCommand implements CommandExecutor
|
||||
sender.sendMessage("Border shape for world \"" + world + "\" is now set to \"" + (shape == null ? "default" : (shape.booleanValue() ? "round" : "square")) + "\".");
|
||||
}
|
||||
|
||||
// "fill" command from player or console, world specified
|
||||
else if (split.length >= 2 && split[1].equalsIgnoreCase("fill"))
|
||||
{
|
||||
if (!Config.HasPermission(player, "fill")) return true;
|
||||
|
||||
boolean cancel = false, confirm = false, pause = false;
|
||||
String pad = "", frequency = "";
|
||||
if (split.length >= 3)
|
||||
{
|
||||
cancel = split[2].equalsIgnoreCase("cancel");
|
||||
confirm = split[2].equalsIgnoreCase("confirm");
|
||||
pause = split[2].equalsIgnoreCase("pause");
|
||||
if (!cancel && !confirm && !pause)
|
||||
frequency = split[2];
|
||||
}
|
||||
if (split.length >= 4)
|
||||
pad = split[3];
|
||||
|
||||
String world = split[0];
|
||||
|
||||
cmdFill(sender, player, world, confirm, cancel, pause, pad, frequency);
|
||||
}
|
||||
|
||||
// "fill" command from player (or from console solely if using cancel or confirm), using current world
|
||||
else if (split.length >= 1 && split[0].equalsIgnoreCase("fill"))
|
||||
{
|
||||
if (!Config.HasPermission(player, "fill")) return true;
|
||||
|
||||
boolean cancel = false, confirm = false, pause = false;
|
||||
String pad = "", frequency = "";
|
||||
if (split.length >= 2)
|
||||
{
|
||||
cancel = split[1].equalsIgnoreCase("cancel");
|
||||
confirm = split[1].equalsIgnoreCase("confirm");
|
||||
pause = split[1].equalsIgnoreCase("pause");
|
||||
if (!cancel && !confirm && !pause)
|
||||
frequency = split[1];
|
||||
}
|
||||
if (split.length >= 3)
|
||||
pad = split[2];
|
||||
|
||||
String world = "";
|
||||
if (player != null)
|
||||
world = player.getWorld().getName();
|
||||
|
||||
if (!cancel && !confirm && !pause && world.isEmpty())
|
||||
{
|
||||
sender.sendMessage("You must specify a world! Example: " + cmdW+" fill " + clrOpt + "[freq] [pad]");
|
||||
return true;
|
||||
}
|
||||
|
||||
cmdFill(sender, player, world, confirm, cancel, pause, pad, frequency);
|
||||
}
|
||||
|
||||
// we couldn't decipher any known commands, so show help
|
||||
else
|
||||
{
|
||||
@ -418,7 +472,7 @@ public class WBCommand implements CommandExecutor
|
||||
page = 1;
|
||||
}
|
||||
|
||||
sender.sendMessage(clrHead + plugin.getDescription().getFullName() + " - commands (" + (player != null ? clrOpt + "[optional] " : "") + clrReq + "<required>" + clrHead + ")" + (page > 0 ? " " + page + "/2" : "") + ":");
|
||||
sender.sendMessage(clrHead + plugin.getDescription().getFullName() + " - commands (" + clrReq + "<required> " + clrOpt + "[optional]" + clrHead + ")" + (page > 0 ? " " + page + "/2" : "") + ":");
|
||||
|
||||
if (page == 0 || page == 1)
|
||||
{
|
||||
@ -437,6 +491,7 @@ public class WBCommand implements CommandExecutor
|
||||
|
||||
if (page == 0 || page == 2)
|
||||
{
|
||||
sender.sendMessage(cmdW+" fill " + clrOpt + "[freq] [pad]" + clrDesc + " - generate world out to border.");
|
||||
sender.sendMessage(cmd+" wshape " + ((player == null) ? clrReq + "<world>" : clrOpt + "[world]") + clrReq + " <round|square|default>" + clrDesc + " - shape override.");
|
||||
sender.sendMessage(cmd+" getmsg" + clrDesc + " - display border message.");
|
||||
sender.sendMessage(cmd+" setmsg " + clrReq + "<text>" + clrDesc + " - set border message.");
|
||||
@ -471,4 +526,110 @@ public class WBCommand implements CommandExecutor
|
||||
Config.setBorder(world, radius, x, z);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private String fillWorld = "";
|
||||
private int fillPadding = 16 * 11;
|
||||
private int fillFrequency = 20;
|
||||
|
||||
private void fillDefaults()
|
||||
{
|
||||
fillWorld = "";
|
||||
fillFrequency = 20;
|
||||
// with "view-distance=10" in server.properties and "Render Distance: Far" in client, hitting border during testing
|
||||
// was loading 11 chunks beyond the border in a couple of directions (10 chunks in the other two directions); thus:
|
||||
fillPadding = 16 * 11;
|
||||
}
|
||||
|
||||
private boolean cmdFill(CommandSender sender, Player player, String world, boolean confirm, boolean cancel, boolean pause, String pad, String frequency)
|
||||
{
|
||||
if (cancel)
|
||||
{
|
||||
sender.sendMessage(clrHead + "Cancelling the world map generation task.");
|
||||
fillDefaults();
|
||||
Config.StopFillTask();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (pause)
|
||||
{
|
||||
if (Config.fillTask == null || !Config.fillTask.valid())
|
||||
{
|
||||
sender.sendMessage(clrHead + "The world map generation task is not currently running.");
|
||||
return true;
|
||||
}
|
||||
Config.fillTask.pause();
|
||||
sender.sendMessage(clrHead + "The world map generation task is now " + (Config.fillTask.isPaused() ? "" : "un") + "paused.");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Config.fillTask != null && Config.fillTask.valid())
|
||||
{
|
||||
sender.sendMessage(clrHead + "The world map generation task is already running.");
|
||||
return true;
|
||||
}
|
||||
|
||||
// set padding and/or delay if those were specified
|
||||
try
|
||||
{
|
||||
if (!pad.isEmpty())
|
||||
fillPadding = Math.abs(Integer.parseInt(pad));
|
||||
if (!frequency.isEmpty())
|
||||
fillFrequency = Math.abs(Integer.parseInt(frequency));
|
||||
}
|
||||
catch(NumberFormatException ex)
|
||||
{
|
||||
sender.sendMessage(clrErr + "The frequency and padding values must be integers.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// set world if it was specified
|
||||
if (!world.isEmpty())
|
||||
fillWorld = world;
|
||||
|
||||
if (confirm)
|
||||
{ // command confirmed, go ahead with it
|
||||
if (fillWorld.isEmpty())
|
||||
{
|
||||
sender.sendMessage(clrErr + "You must first use this command successfully without confirming.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (player != null)
|
||||
Config.Log("Filling out world to border at the command of player \"" + player.getName() + "\".");
|
||||
|
||||
int ticks = 1, repeats = 1;
|
||||
if (fillFrequency > 20)
|
||||
repeats = fillFrequency / 20;
|
||||
else
|
||||
ticks = 20 / fillFrequency;
|
||||
|
||||
Config.fillTask = new WorldFillTask(plugin.getServer(), player, fillWorld, fillPadding, repeats, ticks);
|
||||
if (Config.fillTask.valid())
|
||||
{
|
||||
int task = plugin.getServer().getScheduler().scheduleSyncRepeatingTask(plugin, Config.fillTask, ticks, ticks);
|
||||
Config.fillTask.setTaskID(task);
|
||||
sender.sendMessage("WorldBorder map generation task started.");
|
||||
}
|
||||
else
|
||||
sender.sendMessage(clrErr + "The world map generation task failed to start.");
|
||||
|
||||
fillDefaults();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (fillWorld.isEmpty())
|
||||
{
|
||||
sender.sendMessage(clrErr + "You must first specify a valid world.");
|
||||
return false;
|
||||
}
|
||||
|
||||
String cmd = clrCmd + ((player == null) ? "wb" : "/wb");
|
||||
sender.sendMessage(clrHead + "World generation task is ready for world \"" + fillWorld + "\", padding the map out to " + fillPadding + " blocks beyond the border (default " + (16 * 11) + "), and the task will try to process up to " + fillFrequency + " chunks per second (default 20).");
|
||||
sender.sendMessage(clrHead + "This process can take a very long time depending on the world's border size. Also, depending on the chunk processing rate, players will likely experience severe lag for the duration.");
|
||||
sender.sendMessage(clrDesc + "You should now use " + cmd + " fill confirm" + clrDesc + " to start the process.");
|
||||
sender.sendMessage(clrDesc + "You can cancel at any time with " + cmd + " fill cancel" + clrDesc + ", or pause/unpause with " + cmd + " fill pause" + clrDesc + ".");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
@ -40,6 +40,8 @@ public class WorldBorder extends JavaPlugin
|
||||
PluginDescriptionFile desc = this.getDescription();
|
||||
System.out.println( desc.getName() + " version " + desc.getVersion() + " shutting down" );
|
||||
Config.StopBorderTimer();
|
||||
Config.StoreFillTask();
|
||||
Config.StopFillTask();
|
||||
}
|
||||
|
||||
// for other plugins to hook into
|
||||
|
@ -1,7 +1,7 @@
|
||||
name: WorldBorder
|
||||
author: Brettflan
|
||||
description: Efficient, feature-rich plugin for limiting the size of your worlds.
|
||||
version: 1.2.3
|
||||
version: 1.3.0
|
||||
main: com.wimbli.WorldBorder.WorldBorder
|
||||
commands:
|
||||
wborder:
|
||||
@ -20,4 +20,65 @@ commands:
|
||||
/<command> setmsg <text> - set border message.
|
||||
/<command> knockback <distance> - how far to move the player back.
|
||||
/<command> delay <amount> - time between border checks.
|
||||
/<command> wshape [world] <round|square|default> - override shape.
|
||||
/<command> wshape [world] <round|square|default> - override shape.
|
||||
/<command> [world] fill [freq] [pad] - generate world out to border.
|
||||
permissions:
|
||||
worldborder.*:
|
||||
description: Grants all WorldBorder permissions
|
||||
children:
|
||||
worldborder.set: true
|
||||
worldborder.radius: true
|
||||
worldborder.clear: true
|
||||
worldborder.list: true
|
||||
worldborder.shape: true
|
||||
worldborder.getmsg: true
|
||||
worldborder.setmsg: true
|
||||
worldborder.reload: true
|
||||
worldborder.debug: true
|
||||
worldborder.knockback: true
|
||||
worldborder.delay: true
|
||||
worldborder.wshape: true
|
||||
worldborder.fill: true
|
||||
worldborder.help: true
|
||||
worldborder.set:
|
||||
description: Can set borders for any world
|
||||
default: op
|
||||
worldborder.radius:
|
||||
description: Can set the radius of an existing border
|
||||
default: op
|
||||
worldborder.clear:
|
||||
description: Can remove any border
|
||||
default: op
|
||||
worldborder.list:
|
||||
description: Can view a list of all borders
|
||||
default: op
|
||||
worldborder.shape:
|
||||
description: Can set the default shape (round or square) for all borders
|
||||
default: op
|
||||
worldborder.getmsg:
|
||||
description: Can view the border crossing message
|
||||
default: op
|
||||
worldborder.setmsg:
|
||||
description: Can set the border crossing message
|
||||
default: op
|
||||
worldborder.reload:
|
||||
description: Can force the plugin to reload from the config file
|
||||
default: op
|
||||
worldborder.debug:
|
||||
description: Can enable/disable debug output to console
|
||||
default: op
|
||||
worldborder.knockback:
|
||||
description: Can set the knockback distance for border crossings
|
||||
default: op
|
||||
worldborder.delay:
|
||||
description: Can set the frequency at which the plugin checks for border crossings
|
||||
default: op
|
||||
worldborder.wshape:
|
||||
description: Can set an overriding border shape for a single world
|
||||
default: op
|
||||
worldborder.fill:
|
||||
description: Can fill in (generate) any missing map chunks out to the border
|
||||
default: op
|
||||
worldborder.help:
|
||||
description: Can view the command reference help pages
|
||||
default: op
|
||||
|
Loading…
Reference in New Issue
Block a user