Implement Command Cooldowns. Resolves #110

This commit is contained in:
Ali Moghnieh 2016-06-28 03:40:47 +01:00 committed by Trent Hensler
parent 864dadab80
commit 09acbcdb05
30 changed files with 302 additions and 21 deletions

View File

@ -4,6 +4,7 @@ import com.earth2me.essentials.textreader.IText;
import com.earth2me.essentials.textreader.KeywordReplacer; import com.earth2me.essentials.textreader.KeywordReplacer;
import com.earth2me.essentials.textreader.TextInput; import com.earth2me.essentials.textreader.TextInput;
import com.earth2me.essentials.textreader.TextPager; import com.earth2me.essentials.textreader.TextPager;
import com.earth2me.essentials.utils.DateUtil;
import com.earth2me.essentials.utils.LocationUtil; import com.earth2me.essentials.utils.LocationUtil;
import net.ess3.api.IEssentials; import net.ess3.api.IEssentials;
import org.bukkit.GameMode; import org.bukkit.GameMode;
@ -26,11 +27,15 @@ import org.bukkit.inventory.InventoryHolder;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import java.io.IOException; import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map.Entry;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import java.util.regex.Pattern;
import static com.earth2me.essentials.I18n.tl; import static com.earth2me.essentials.I18n.tl;
@ -399,10 +404,48 @@ public class EssentialsPlayerListener implements Listener {
broadcast = false; broadcast = false;
} }
} }
final User user = ess.getUser(player);
if (update) { if (update) {
final User user = ess.getUser(player);
user.updateActivity(broadcast); user.updateActivity(broadcast);
} }
if (ess.getSettings().isCommandCooldownsEnabled() && pluginCommand != null) {
int argStartIndex = event.getMessage().indexOf(" ");
String args = argStartIndex == -1 ? event.getMessage() // No arguments present
: event.getMessage().substring(argStartIndex); // arguments start at argStartIndex; substring from there.
String fullCommand = pluginCommand.getName() + " " + args;
// Used to determine whether a user already has an existing cooldown
// If so, no need to check for (and write) new ones.
boolean cooldownFound = false;
// Iterate over a copy of getCommandCooldowns in case of concurrent modifications
for (Entry<Pattern, Long> entry : new HashMap<>(user.getCommandCooldowns()).entrySet()) {
// Remove any expired cooldowns
if (entry.getValue() <= System.currentTimeMillis()) {
user.clearCommandCooldown(entry.getKey());
// Don't break in case there are other command cooldowns left to clear.
} else if (entry.getKey().matcher(fullCommand).matches()) {
// User's current cooldown hasn't expired, inform and terminate cooldown code.
if (entry.getValue() > System.currentTimeMillis()) {
String commandCooldownTime = DateUtil.formatDateDiff(entry.getValue());
user.sendMessage(tl("commandCooldown", commandCooldownTime));
cooldownFound = true;
event.setCancelled(true);
break;
}
}
}
if (!cooldownFound) {
Entry<Pattern, Long> cooldownEntry = ess.getSettings().getCommandCooldownEntry(fullCommand);
if (cooldownEntry != null) {
Date expiry = new Date(System.currentTimeMillis() + cooldownEntry.getValue());
user.addCommandCooldown(cooldownEntry.getKey(), expiry, ess.getSettings().isCommandCooldownPersistent(fullCommand));
}
}
}
} }
@EventHandler(priority = EventPriority.NORMAL) @EventHandler(priority = EventPriority.NORMAL)

View File

@ -10,7 +10,9 @@ import org.bukkit.event.EventPriority;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry;
import java.util.Set; import java.util.Set;
import java.util.regex.Pattern;
public interface ISettings extends IConf { public interface ISettings extends IConf {
@ -247,4 +249,12 @@ public interface ISettings extends IConf {
boolean isSpawnOnJoin(); boolean isSpawnOnJoin();
boolean isTeleportToCenterLocation(); boolean isTeleportToCenterLocation();
boolean isCommandCooldownsEnabled();
long getCommandCooldownMs(String label);
Entry<Pattern, Long> getCommandCooldownEntry(String label);
boolean isCommandCooldownPersistent(String label);
} }

View File

@ -7,9 +7,11 @@ import org.bukkit.Location;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.regex.Pattern;
public interface IUser { public interface IUser {
@ -138,6 +140,14 @@ public interface IUser {
Map<String, Object> getConfigMap(); Map<String, Object> getConfigMap();
Map<String, Object> getConfigMap(String node); Map<String, Object> getConfigMap(String node);
Map<Pattern, Long> getCommandCooldowns();
Date getCommandCooldownExpiry(String label);
void addCommandCooldown(Pattern pattern, Date expiresAt, boolean save);
boolean clearCommandCooldown(Pattern pattern);
/* /*
* PlayerExtension * PlayerExtension

View File

@ -16,10 +16,13 @@ import org.bukkit.inventory.ItemStack;
import java.io.File; import java.io.File;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.*; import java.util.*;
import java.util.Map.Entry;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import static com.earth2me.essentials.I18n.tl; import static com.earth2me.essentials.I18n.tl;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
public class Settings implements net.ess3.api.ISettings { public class Settings implements net.ess3.api.ISettings {
@ -532,6 +535,7 @@ public class Settings implements net.ess3.api.ISettings {
customQuitMessage = _getCustomQuitMessage(); customQuitMessage = _getCustomQuitMessage();
isCustomQuitMessage = !customQuitMessage.equals("none"); isCustomQuitMessage = !customQuitMessage.equals("none");
muteCommands = _getMuteCommands(); muteCommands = _getMuteCommands();
commandCooldowns = _getCommandCooldowns();
} }
private List<Integer> itemSpawnBl = new ArrayList<Integer>(); private List<Integer> itemSpawnBl = new ArrayList<Integer>();
@ -1173,4 +1177,89 @@ public class Settings implements net.ess3.api.ISettings {
public boolean isTeleportToCenterLocation() { public boolean isTeleportToCenterLocation() {
return config.getBoolean("teleport-to-center", true); return config.getBoolean("teleport-to-center", true);
} }
private Map<Pattern, Long> commandCooldowns;
private Map<Pattern, Long> _getCommandCooldowns() {
if (!config.isConfigurationSection("command-cooldowns")) {
return null;
}
ConfigurationSection section = config.getConfigurationSection("command-cooldowns");
Map<Pattern, Long> result = new LinkedHashMap<>();
for (String cmdEntry : section.getKeys(false)) {
Pattern pattern = null;
/* ================================
* >> Regex
* ================================ */
if (cmdEntry.startsWith("^")) {
try {
pattern = Pattern.compile(cmdEntry.substring(1));
} catch (PatternSyntaxException e) {
ess.getLogger().warning("Command cooldown error: " + e.getMessage());
}
} else {
// Escape above Regex
if (cmdEntry.startsWith("\\^")) {
cmdEntry = cmdEntry.substring(1);
}
String cmd = cmdEntry
.replaceAll("\\*", ".*"); // Wildcards are accepted as asterisk * as known universally.
pattern = Pattern.compile(cmd + "( .*)?"); // This matches arguments, if present, to "ignore" them from the feature.
}
/* ================================
* >> Process cooldown value
* ================================ */
Object value = section.get(cmdEntry);
if (!(value instanceof Number) && value instanceof String) {
try {
value = Double.parseDouble(value.toString());
} catch (NumberFormatException ignored) {
}
}
if (!(value instanceof Number)) {
ess.getLogger().warning("Command cooldown error: '" + value + "' is not a valid cooldown");
continue;
}
double cooldown = ((Number) value).doubleValue();
if (cooldown < 1) {
ess.getLogger().warning("Command cooldown with very short " + cooldown + " cooldown.");
}
result.put(pattern, (long) cooldown * 1000); // convert to milliseconds
}
return result;
}
@Override
public boolean isCommandCooldownsEnabled() {
return commandCooldowns != null;
}
@Override
public long getCommandCooldownMs(String label) {
Entry<Pattern, Long> result = getCommandCooldownEntry(label);
return result != null ? result.getValue() : -1; // return cooldown in milliseconds
}
@Override
public Entry<Pattern, Long> getCommandCooldownEntry(String label) {
if (isCommandCooldownsEnabled()) {
for (Entry<Pattern, Long> entry : this.commandCooldowns.entrySet()) {
// Check if label matches current pattern (command-cooldown in config)
if (entry.getKey().matcher(label).matches()) {
return entry;
}
}
}
return null;
}
@Override
public boolean isCommandCooldownPersistent(String label) {
// TODO: enable per command cooldown specification for persistence.
return config.getBoolean("command-cooldown-persistence", true);
}
} }

View File

@ -2,6 +2,7 @@ package com.earth2me.essentials;
import com.earth2me.essentials.utils.NumberUtil; import com.earth2me.essentials.utils.NumberUtil;
import com.earth2me.essentials.utils.StringUtil; import com.earth2me.essentials.utils.StringUtil;
import com.google.common.collect.ImmutableMap;
import net.ess3.api.IEssentials; import net.ess3.api.IEssentials;
import net.ess3.api.InvalidWorldException; import net.ess3.api.InvalidWorldException;
import net.ess3.api.MaxMoneyException; import net.ess3.api.MaxMoneyException;
@ -13,6 +14,8 @@ import org.bukkit.inventory.ItemStack;
import java.io.File; import java.io.File;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.*; import java.util.*;
import java.util.Map.Entry;
import java.util.regex.Pattern;
import static com.earth2me.essentials.I18n.tl; import static com.earth2me.essentials.I18n.tl;
@ -85,6 +88,7 @@ public abstract class UserData extends PlayerExtension implements IConf {
ignoredPlayers = _getIgnoredPlayers(); ignoredPlayers = _getIgnoredPlayers();
logoutLocation = _getLogoutLocation(); logoutLocation = _getLogoutLocation();
lastAccountName = _getLastAccountName(); lastAccountName = _getLastAccountName();
commandCooldowns = _getCommandCooldowns();
} }
private BigDecimal money; private BigDecimal money;
@ -792,6 +796,89 @@ public abstract class UserData extends PlayerExtension implements IConf {
return new HashMap<String, Object>(); return new HashMap<String, Object>();
} }
// Pattern, Date. Pattern for less pattern creations
private Map<Pattern, Long> commandCooldowns;
private Map<Pattern, Long> _getCommandCooldowns() {
if (!config.isConfigurationSection("timestamps.command-cooldowns")) {
return null;
}
// See saveCommandCooldowns() for deserialization explanation
List<Map<?, ?>> section = config.getMapList("timestamps.command-cooldowns");
HashMap<Pattern, Long> result = new HashMap<>();
for (Map<?, ?> map : section) {
Pattern pattern = Pattern.compile(map.get("pattern").toString());
long expiry = ((Number) map.get("expiry")).longValue();
result.put(pattern, expiry);
}
return result;
}
public Map<Pattern, Long> getCommandCooldowns() {
if (this.commandCooldowns == null) {
return Collections.emptyMap();
}
return Collections.unmodifiableMap(this.commandCooldowns);
}
public Date getCommandCooldownExpiry(String label) {
if (commandCooldowns != null) {
for (Entry<Pattern, Long> entry : this.commandCooldowns.entrySet()) {
if (entry.getKey().matcher(label).matches()) {
return new Date(entry.getValue());
}
}
}
return null;
}
public void addCommandCooldown(Pattern pattern, Date expiresAt, boolean save) {
if (this.commandCooldowns == null) {
this.commandCooldowns = new HashMap<>();
}
this.commandCooldowns.put(pattern, expiresAt.getTime());
if (save) {
saveCommandCooldowns();
}
}
public boolean clearCommandCooldown(Pattern pattern) {
if (this.commandCooldowns == null) {
return false; // false for no modification
}
if(this.commandCooldowns.remove(pattern) != null) {
saveCommandCooldowns();
return true;
}
return false;
}
private void saveCommandCooldowns() {
// Serialization explanation:
//
// Serialization is done as a map list instead of a config section due to limitations.
// When serializing patterns (which commonly include full stops .) Bukkit/Essentials config framework
// interprets it as a path separator, thus it breaks up the regex into sub nodes causing invalid syntax.
// Thus each command cooldown is instead stored as a Map of {pattern: .., expiry: ..} to work around this.
List<Object> serialized = new ArrayList<>();
for (Entry<Pattern, Long> entry : this.commandCooldowns.entrySet()) {
// Don't save expired cooldowns
if (entry.getValue() < System.currentTimeMillis()) {
continue;
}
Map<?, ?> map = ImmutableMap.builder()
.put("pattern", entry.getKey().pattern())
.put("expiry", entry.getValue())
.build();
serialized.add(map);
}
config.setProperty("timestamps.command-cooldowns", serialized);
save();
}
public UUID getConfigUUID() { public UUID getConfigUUID() {
return config.uuid; return config.uuid;
} }

View File

@ -484,6 +484,24 @@ send-fly-enable-on-join: true
# Give someone permission to teleport to a world with essentials.time.world.<worldname>. # Give someone permission to teleport to a world with essentials.time.world.<worldname>.
world-time-permissions: false world-time-permissions: false
# Specify cooldown for both Essentials commands and external commands as well.
# All commands do not start with a Forward Slash (/). Instead of /msg, write msg
#
# Wildcards are supported. E.g.
# - '*i*': 50
# adds a 50 second cooldown to all commands that include the letter i
#
# EssentialsX supports regex by starting the command with a caret ^
# For example, to target commands starting with ban and not banip the following would be used:
# '^ban([^ip])( .*)?': 60 # 60 seconds /ban cooldown.
# Note: If you have a command that starts with ^, then you can escape it using backslash (\). e.g. \^command: 123
command-cooldowns:
# feed: 100 # 100 second cooldown on /feed command
# '*': 5 # 5 Second cooldown on all commands
# Whether command cooldowns should be persistent past server shutdowns
command-cooldown-persistence: true
############################################################ ############################################################
# +------------------------------------------------------+ # # +------------------------------------------------------+ #
# | EssentialsHome | # # | EssentialsHome | #

View File

@ -573,3 +573,4 @@ msgEnabled=\u00a76Receiving messages \u00a7cenabled\u00a76.
msgEnabledFor=\u00a76Receiving messages \u00a7cenabled \u00a76for \u00a7c{0}\u00a76. msgEnabledFor=\u00a76Receiving messages \u00a7cenabled \u00a76for \u00a7c{0}\u00a76.
msgIgnore=\u00a7c{0} \u00a74has messages disabled. msgIgnore=\u00a7c{0} \u00a74has messages disabled.
minimumPayAmount=\u00a7cThe minimum amount you can pay is {0}. minimumPayAmount=\u00a7cThe minimum amount you can pay is {0}.
commandCooldown=\u00a7cYou cannot type that command for {0}.

View File

@ -561,4 +561,5 @@ createdKit=\u00a76Created kit \u00a7c{0} \u00a76with \u00a7c{1} \u00a76entries a
spectator=spectator spectator=spectator
kitContains=\u00a76Kit \u00a7c{0} \u00a76contains: kitContains=\u00a76Kit \u00a7c{0} \u00a76contains:
kitItem=\u00a76- \u00a7f{0} kitItem=\u00a76- \u00a7f{0}
invalidBanner=\u00a74Invalid banner syntax. invalidBanner=\u00a74Invalid banner syntax.
commandCooldown=\u00a7cYou cannot type that command for {0}.

View File

@ -561,4 +561,5 @@ createdKit=\u00a76Created kit \u00a7c{0} \u00a76with \u00a7c{1} \u00a76entries a
spectator=spectator spectator=spectator
kitContains=\u00a76Kit \u00a7c{0} \u00a76contains: kitContains=\u00a76Kit \u00a7c{0} \u00a76contains:
kitItem=\u00a76- \u00a7f{0} kitItem=\u00a76- \u00a7f{0}
invalidBanner=\u00a74Invalid banner syntax. invalidBanner=\u00a74Invalid banner syntax.
commandCooldown=\u00a7cYou cannot type that command for {0}.

View File

@ -566,3 +566,4 @@ spectator=Zuschauer
kitContains=\u00a76Ausr\u00fcstung \u00a7c{0} \u00a76enth\u00e4lt: kitContains=\u00a76Ausr\u00fcstung \u00a7c{0} \u00a76enth\u00e4lt:
kitItem=\u00a76- \u00a7f{0} kitItem=\u00a76- \u00a7f{0}
invalidBanner=\u00a74Ung\u00fcltige Banner-Syntax. invalidBanner=\u00a74Ung\u00fcltige Banner-Syntax.
commandCooldown=\u00a7cYou cannot type that command for {0}.

View File

@ -566,3 +566,4 @@ msgDisabled=\u00a76Receiving messages \u00a7cdisabled\u00a76.
msgDisabledFor=\u00a76Receiving messages \u00a7cdisabled \u00a76for \u00a7c{0}\u00a76. msgDisabledFor=\u00a76Receiving messages \u00a7cdisabled \u00a76for \u00a7c{0}\u00a76.
msgEnabled=\u00a76Receiving messages \u00a7cenabled\u00a76. msgEnabled=\u00a76Receiving messages \u00a7cenabled\u00a76.
msgEnabledFor=\u00a76Receiving messages \u00a7cenabled \u00a76for \u00a7c{0}\u00a76. msgEnabledFor=\u00a76Receiving messages \u00a7cenabled \u00a76for \u00a7c{0}\u00a76.
commandCooldown=\u00a7cYou cannot type that command for {0}.

View File

@ -561,4 +561,5 @@ createdKit=\u00a76Created kit \u00a7c{0} \u00a76with \u00a7c{1} \u00a76entries a
spectator=spectator spectator=spectator
kitContains=\u00a76Kit \u00a7c{0} \u00a76contains: kitContains=\u00a76Kit \u00a7c{0} \u00a76contains:
kitItem=\u00a76- \u00a7f{0} kitItem=\u00a76- \u00a7f{0}
invalidBanner=\u00a74Invalid banner syntax. invalidBanner=\u00a74Invalid banner syntax.
commandCooldown=\u00a7cYou cannot type that command for {0}.

View File

@ -561,4 +561,5 @@ createdKit=\u00a76Created kit \u00a7c{0} \u00a76with \u00a7c{1} \u00a76entries a
spectator=spectator spectator=spectator
kitContains=\u00a76Kit \u00a7c{0} \u00a76contains: kitContains=\u00a76Kit \u00a7c{0} \u00a76contains:
kitItem=\u00a76- \u00a7f{0} kitItem=\u00a76- \u00a7f{0}
invalidBanner=\u00a74Invalid banner syntax. invalidBanner=\u00a74Invalid banner syntax.
commandCooldown=\u00a7cYou cannot type that command for {0}.

View File

@ -561,4 +561,5 @@ createdKit=\u00a76Created kit \u00a7c{0} \u00a76with \u00a7c{1} \u00a76entries a
spectator=spectator spectator=spectator
kitContains=\u00a76Kit \u00a7c{0} \u00a76contains: kitContains=\u00a76Kit \u00a7c{0} \u00a76contains:
kitItem=\u00a76- \u00a7f{0} kitItem=\u00a76- \u00a7f{0}
invalidBanner=\u00a74Invalid banner syntax. invalidBanner=\u00a74Invalid banner syntax.
commandCooldown=\u00a7cYou cannot type that command for {0}.

View File

@ -567,3 +567,4 @@ msgDisabled=\u00a76R\u00e9ception des messages \u00a7cd\u00e9sactiv\u00e9e\u00a7
msgDisabledFor=\u00a76R\u00e9ception des messages \u00a7cd\u00e9sactiv\u00e9e \u00a76pour \u00a7c{0}\u00a76. msgDisabledFor=\u00a76R\u00e9ception des messages \u00a7cd\u00e9sactiv\u00e9e \u00a76pour \u00a7c{0}\u00a76.
msgEnabled=\u00a76R\u00e9ception des messages \u00a7cactiv\u00e9e\u00a76. msgEnabled=\u00a76R\u00e9ception des messages \u00a7cactiv\u00e9e\u00a76.
msgEnabledFor=\u00a76R\u00e9ception des messages \u00a7cactiv\u00e9e \u00a76pour \u00a7c{0}\u00a76. msgEnabledFor=\u00a76R\u00e9ception des messages \u00a7cactiv\u00e9e \u00a76pour \u00a7c{0}\u00a76.
commandCooldown=\u00a7cYou cannot type that command for {0}.

View File

@ -561,4 +561,5 @@ createdKit=\u00a76Created kit \u00a7c{0} \u00a76with \u00a7c{1} \u00a76entries a
spectator=spectator spectator=spectator
kitContains=\u00a76Kit \u00a7c{0} \u00a76contains: kitContains=\u00a76Kit \u00a7c{0} \u00a76contains:
kitItem=\u00a76- \u00a7f{0} kitItem=\u00a76- \u00a7f{0}
invalidBanner=\u00a74Invalid banner syntax. invalidBanner=\u00a74Invalid banner syntax.
commandCooldown=\u00a7cYou cannot type that command for {0}.

View File

@ -561,4 +561,5 @@ createdKit=\u00a76Created kit \u00a7c{0} \u00a76with \u00a7c{1} \u00a76entries a
spectator=spectator spectator=spectator
kitContains=\u00a76Kit \u00a7c{0} \u00a76contains: kitContains=\u00a76Kit \u00a7c{0} \u00a76contains:
kitItem=\u00a76- \u00a7f{0} kitItem=\u00a76- \u00a7f{0}
invalidBanner=\u00a74Invalid banner syntax. invalidBanner=\u00a74Invalid banner syntax.
commandCooldown=\u00a7cYou cannot type that command for {0}.

View File

@ -561,4 +561,5 @@ createdKit=\u00a76Created kit \u00a7c{0} \u00a76with \u00a7c{1} \u00a76entries a
spectator=spectator spectator=spectator
kitContains=\u00a76Kit \u00a7c{0} \u00a76contains: kitContains=\u00a76Kit \u00a7c{0} \u00a76contains:
kitItem=\u00a76- \u00a7f{0} kitItem=\u00a76- \u00a7f{0}
invalidBanner=\u00a74Invalid banner syntax. invalidBanner=\u00a74Invalid banner syntax.
commandCooldown=\u00a7cYou cannot type that command for {0}.

View File

@ -561,4 +561,5 @@ createdKit=\u00a76Created kit \u00a7c{0} \u00a76with \u00a7c{1} \u00a76entries a
spectator=spectator spectator=spectator
kitContains=\u00a76Kit \u00a7c{0} \u00a76contains: kitContains=\u00a76Kit \u00a7c{0} \u00a76contains:
kitItem=\u00a76- \u00a7f{0} kitItem=\u00a76- \u00a7f{0}
invalidBanner=\u00a74Invalid banner syntax. invalidBanner=\u00a74Invalid banner syntax.
commandCooldown=\u00a7cYou cannot type that command for {0}.

View File

@ -561,4 +561,5 @@ createdKit=\u00a76Created kit \u00a7c{0} \u00a76with \u00a7c{1} \u00a76entries a
spectator=spectator spectator=spectator
kitContains=\u00a76Kit \u00a7c{0} \u00a76contains: kitContains=\u00a76Kit \u00a7c{0} \u00a76contains:
kitItem=\u00a76- \u00a7f{0} kitItem=\u00a76- \u00a7f{0}
invalidBanner=\u00a74Invalid banner syntax. invalidBanner=\u00a74Invalid banner syntax.
commandCooldown=\u00a7cYou cannot type that command for {0}.

View File

@ -561,4 +561,5 @@ createdKit=\u00a76Created kit \u00a7c{0} \u00a76with \u00a7c{1} \u00a76entries a
spectator=spectator spectator=spectator
kitContains=\u00a76Kit \u00a7c{0} \u00a76contains: kitContains=\u00a76Kit \u00a7c{0} \u00a76contains:
kitItem=\u00a76- \u00a7f{0} kitItem=\u00a76- \u00a7f{0}
invalidBanner=\u00a74Invalid banner syntax. invalidBanner=\u00a74Invalid banner syntax.
commandCooldown=\u00a7cYou cannot type that command for {0}.

View File

@ -561,4 +561,5 @@ createdKit=\u00a76Created kit \u00a7c{0} \u00a76with \u00a7c{1} \u00a76entries a
spectator=spectator spectator=spectator
kitContains=\u00a76Kit \u00a7c{0} \u00a76contains: kitContains=\u00a76Kit \u00a7c{0} \u00a76contains:
kitItem=\u00a76- \u00a7f{0} kitItem=\u00a76- \u00a7f{0}
invalidBanner=\u00a74Invalid banner syntax. invalidBanner=\u00a74Invalid banner syntax.
commandCooldown=\u00a7cYou cannot type that command for {0}.

View File

@ -561,4 +561,5 @@ createdKit=\u00a76Created kit \u00a7c{0} \u00a76with \u00a7c{1} \u00a76entries a
spectator=spectator spectator=spectator
kitContains=\u00a76Kit \u00a7c{0} \u00a76contains: kitContains=\u00a76Kit \u00a7c{0} \u00a76contains:
kitItem=\u00a76- \u00a7f{0} kitItem=\u00a76- \u00a7f{0}
invalidBanner=\u00a74Invalid banner syntax. invalidBanner=\u00a74Invalid banner syntax.
commandCooldown=\u00a7cYou cannot type that command for {0}.

View File

@ -561,4 +561,5 @@ createdKit=\u00a76Created kit \u00a7c{0} \u00a76with \u00a7c{1} \u00a76entries a
spectator=spectator spectator=spectator
kitContains=\u00a76Kit \u00a7c{0} \u00a76contains: kitContains=\u00a76Kit \u00a7c{0} \u00a76contains:
kitItem=\u00a76- \u00a7f{0} kitItem=\u00a76- \u00a7f{0}
invalidBanner=\u00a74Invalid banner syntax. invalidBanner=\u00a74Invalid banner syntax.
commandCooldown=\u00a7cYou cannot type that command for {0}.

View File

@ -561,4 +561,5 @@ createdKit=\u00a76Created kit \u00a7c{0} \u00a76with \u00a7c{1} \u00a76entries a
spectator=spectator spectator=spectator
kitContains=\u00a76Kit \u00a7c{0} \u00a76contains: kitContains=\u00a76Kit \u00a7c{0} \u00a76contains:
kitItem=\u00a76- \u00a7f{0} kitItem=\u00a76- \u00a7f{0}
invalidBanner=\u00a74Invalid banner syntax. invalidBanner=\u00a74Invalid banner syntax.
commandCooldown=\u00a7cYou cannot type that command for {0}.

View File

@ -561,4 +561,5 @@ createdKit=\u00a76Created kit \u00a7c{0} \u00a76with \u00a7c{1} \u00a76entries a
spectator=spectator spectator=spectator
kitContains=\u00a76Kit \u00a7c{0} \u00a76contains: kitContains=\u00a76Kit \u00a7c{0} \u00a76contains:
kitItem=\u00a76- \u00a7f{0} kitItem=\u00a76- \u00a7f{0}
invalidBanner=\u00a74Invalid banner syntax. invalidBanner=\u00a74Invalid banner syntax.
commandCooldown=\u00a7cYou cannot type that command for {0}.

View File

@ -561,4 +561,5 @@ createdKit=\u00a76Created kit \u00a7c{0} \u00a76with \u00a7c{1} \u00a76entries a
spectator=spectator spectator=spectator
kitContains=\u00a76Kit \u00a7c{0} \u00a76contains: kitContains=\u00a76Kit \u00a7c{0} \u00a76contains:
kitItem=\u00a76- \u00a7f{0} kitItem=\u00a76- \u00a7f{0}
invalidBanner=\u00a74Invalid banner syntax. invalidBanner=\u00a74Invalid banner syntax.
commandCooldown=\u00a7cYou cannot type that command for {0}.

View File

@ -561,4 +561,5 @@ createdKit=\u00a76Created kit \u00a7c{0} \u00a76with \u00a7c{1} \u00a76entries a
spectator=spectator spectator=spectator
kitContains=\u00a76Kit \u00a7c{0} \u00a76contains: kitContains=\u00a76Kit \u00a7c{0} \u00a76contains:
kitItem=\u00a76- \u00a7f{0} kitItem=\u00a76- \u00a7f{0}
invalidBanner=\u00a74Invalid banner syntax. invalidBanner=\u00a74Invalid banner syntax.
commandCooldown=\u00a7cYou cannot type that command for {0}.

View File

@ -561,4 +561,5 @@ createdKit=\u00a76Created kit \u00a7c{0} \u00a76with \u00a7c{1} \u00a76entries a
spectator=spectator spectator=spectator
kitContains=\u00a76Kit \u00a7c{0} \u00a76contains: kitContains=\u00a76Kit \u00a7c{0} \u00a76contains:
kitItem=\u00a76- \u00a7f{0} kitItem=\u00a76- \u00a7f{0}
invalidBanner=\u00a74Invalid banner syntax. invalidBanner=\u00a74Invalid banner syntax.
commandCooldown=\u00a7cYou cannot type that command for {0}.

View File

@ -561,4 +561,5 @@ createdKit=\u00a76Created kit \u00a7c{0} \u00a76with \u00a7c{1} \u00a76entries a
spectator=spectator spectator=spectator
kitContains=\u00a76Kit \u00a7c{0} \u00a76contains: kitContains=\u00a76Kit \u00a7c{0} \u00a76contains:
kitItem=\u00a76- \u00a7f{0} kitItem=\u00a76- \u00a7f{0}
invalidBanner=\u00a74Invalid banner syntax. invalidBanner=\u00a74Invalid banner syntax.
commandCooldown=\u00a7cYou cannot type that command for {0}.