Add support for Per Player Locale on Bukkit - Resolves #62

This commit is contained in:
Aikar
2017-11-24 13:05:20 -05:00
parent 1915ebf00e
commit adfb4b11a1
8 changed files with 148 additions and 12 deletions
@@ -26,6 +26,7 @@ package co.aikar.commands;
import co.aikar.commands.apachecommonslang.ApacheCommonsExceptionUtil;
import co.aikar.timings.lib.MCTiming;
import co.aikar.timings.lib.TimingManager;
import com.google.common.collect.Maps;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.Server;
@@ -33,10 +34,14 @@ import org.bukkit.command.Command;
import org.bukkit.command.CommandMap;
import org.bukkit.command.CommandSender;
import org.bukkit.command.SimpleCommandMap;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.server.PluginDisableEvent;
import org.bukkit.plugin.Plugin;
import org.bukkit.scheduler.BukkitTask;
import org.jetbrains.annotations.NotNull;
import java.lang.reflect.Field;
@@ -44,23 +49,29 @@ import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
@SuppressWarnings("WeakerAccess")
public class BukkitCommandManager extends CommandManager<CommandSender, ChatColor, BukkitMessageFormatter> {
public class BukkitCommandManager extends CommandManager<CommandSender, BukkitCommandIssuer, ChatColor, BukkitMessageFormatter> {
@SuppressWarnings("WeakerAccess")
protected final Plugin plugin;
private final CommandMap commandMap;
private final TimingManager timingManager;
private final BukkitTask localeTask;
protected Map<String, Command> knownCommands = new HashMap<>();
protected Map<String, BukkitRootCommand> registeredCommands = new HashMap<>();
protected BukkitCommandContexts contexts;
protected BukkitCommandCompletions completions;
MCTiming commandTiming;
protected BukkitLocales locales;
private boolean cantReadLocale = false;
protected Map<UUID, Locale> issuersLocale = Maps.newConcurrentMap();
@SuppressWarnings("JavaReflectionMemberAccess")
public BukkitCommandManager(Plugin plugin) {
@@ -74,6 +85,12 @@ public class BukkitCommandManager extends CommandManager<CommandSender, ChatColo
this.formatters.put(MessageType.HELP, new BukkitMessageFormatter(ChatColor.AQUA, ChatColor.GREEN, ChatColor.YELLOW));
Bukkit.getPluginManager().registerEvents(new ACFBukkitListener(plugin), plugin);
getLocales(); // auto load locales
this.localeTask = Bukkit.getScheduler().runTaskTimer(plugin, () -> {
if (cantReadLocale) {
return;
}
Bukkit.getOnlinePlayers().forEach(this::readPlayerLocale);
}, 5, 5);
}
@NotNull private CommandMap hookCommandMap() {
@@ -213,6 +230,49 @@ public class BukkitCommandManager extends CommandManager<CommandSender, ChatColo
this.registeredCommands.clear();
}
private Field getEntityField(Player player) throws NoSuchFieldException {
Class cls = player.getClass();
while (cls != Object.class) {
if (cls.getName().endsWith("CraftEntity")) {
Field field = cls.getDeclaredField("entity");
field.setAccessible(true);
return field;
}
cls = cls.getSuperclass();
}
return null;
}
private void readPlayerLocale(Player player) {
if (!player.isOnline() || cantReadLocale) {
return;
}
try {
Field entityField = getEntityField(player);
if (entityField == null) {
return;
}
Object nmsPlayer = entityField.get(player);
if (nmsPlayer != null) {
Field localeField = nmsPlayer.getClass().getField("locale");
Object localeString = localeField.get(nmsPlayer);
if (localeString != null && localeString instanceof String) {
String[] split = ACFPatterns.UNDERSCORE.split((String) localeString);
Locale locale = split.length > 1 ? new Locale(split[0], split[1]) : new Locale(split[0]);
Locale prev = issuersLocale.put(player.getUniqueId(), locale);
if (!Objects.equals(locale, prev)) {
this.notifyLocaleChange(getCommandIssuer(player), prev, locale);
}
}
}
} catch (Exception e) {
cantReadLocale = true;
this.localeTask.cancel();
this.log(LogLevel.INFO, "Can't read players locale, you will be unable to automatically detect players language. Only Bukkit 1.7+ is supported for this.", e);
}
}
private class ACFBukkitListener implements Listener {
private final Plugin plugin;
@@ -227,6 +287,17 @@ public class BukkitCommandManager extends CommandManager<CommandSender, ChatColo
}
unregisterCommands();
}
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
readPlayerLocale(player);
this.plugin.getServer().getScheduler().runTaskLater(this.plugin, () -> readPlayerLocale(player), 20);
}
@EventHandler
public void onPlayerJoin(PlayerQuitEvent event) {
issuersLocale.remove(event.getPlayer().getUniqueId());
}
}
public TimingManager getTimings() {
@@ -239,7 +310,7 @@ public class BukkitCommandManager extends CommandManager<CommandSender, ChatColo
}
@Override
public CommandIssuer getCommandIssuer(Object issuer) {
public BukkitCommandIssuer getCommandIssuer(Object issuer) {
if (!(issuer instanceof CommandSender)) {
throw new IllegalArgumentException(issuer.getClass().getName() + " is not a Command Issuer.");
}
@@ -273,4 +344,14 @@ public class BukkitCommandManager extends CommandManager<CommandSender, ChatColo
}
}
}
@Override
public Locale getIssuerLocale(CommandIssuer issuer) {
UUID uniqueId = ((Player) issuer.getIssuer()).getUniqueId();
Locale locale = issuersLocale.get(uniqueId);
if (locale != null) {
return locale;
}
return super.getIssuerLocale(issuer);
}
}
@@ -36,7 +36,7 @@ import java.util.HashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
public class BungeeCommandManager extends CommandManager<CommandSender, ChatColor, BungeeMessageFormatter> {
public class BungeeCommandManager extends CommandManager<CommandSender, BungeeCommandIssuer, ChatColor, BungeeMessageFormatter> {
protected final Plugin plugin;
protected Map<String, BungeeRootCommand> registeredCommands = new HashMap<>();
@@ -131,7 +131,7 @@ public class BungeeCommandManager extends CommandManager<CommandSender, ChatColo
}
@Override
public CommandIssuer getCommandIssuer(Object issuer) {
public BungeeCommandIssuer getCommandIssuer(Object issuer) {
if (!(issuer instanceof CommandSender)) {
throw new IllegalArgumentException(issuer.getClass().getName() + " is not a Command Issuer.");
}
@@ -37,6 +37,7 @@ final class ACFPatterns {
public static final Pattern PERCENTAGE = Pattern.compile("%", Pattern.LITERAL);
public static final Pattern NEWLINE = Pattern.compile("\n");
public static final Pattern DASH = Pattern.compile("-");
public static final Pattern UNDERSCORE = Pattern.compile("_");
public static final Pattern SPACE = Pattern.compile(" ");
public static final Pattern SEMICOLON = Pattern.compile(";");
public static final Pattern COLON = Pattern.compile(":");
@@ -50,7 +50,7 @@ public abstract class BaseCommand {
private String execSubcommand;
@SuppressWarnings("WeakerAccess")
private String[] origArgs;
CommandManager<?, ?, ?> manager = null;
CommandManager<?, ?, ?, ?> manager = null;
BaseCommand parentCommand;
Map<String, RootCommand> registeredCommands = new HashMap<>();
String description;
@@ -24,15 +24,22 @@
package co.aikar.commands;
import co.aikar.locales.MessageKeyProvider;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import org.jetbrains.annotations.NotNull;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.*;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
@SuppressWarnings("WeakerAccess")
public abstract class CommandManager <I, FT, F extends MessageFormatter<FT>> {
public abstract class CommandManager <I, AI extends CommandIssuer, FT, F extends MessageFormatter<FT>> {
/**
* This is a stack incase a command calls a command
@@ -48,11 +55,14 @@ public abstract class CommandManager <I, FT, F extends MessageFormatter<FT>> {
protected Map<String, RootCommand> rootCommands = new HashMap<>();
protected final CommandReplacements replacements = new CommandReplacements(this);
protected ExceptionHandler defaultExceptionHandler = null;
protected List<IssuerLocaleChangedCallback<AI>> localeChangedCallbacks = Lists.newArrayList();
protected Set<Locale> supportedLanguages = Sets.newHashSet(Locales.ENGLISH, Locales.GERMAN, Locales.SPANISH, Locales.CZECH);
protected Map<MessageType, F> formatters = new IdentityHashMap<>();
protected F defaultFormatter;
protected int defaultHelpPerPage = 10;
private Set<String> unstableAPIs = Sets.newHashSet();
public static CommandOperationContext getCurrentCommandOperationContext() {
@@ -165,7 +175,7 @@ public abstract class CommandManager <I, FT, F extends MessageFormatter<FT>> {
public abstract boolean isCommandIssuer(Class<?> type);
// TODO: Change this to I if we make a breaking change
public abstract CommandIssuer getCommandIssuer(Object issuer);
public abstract AI getCommandIssuer(Object issuer);
public abstract RootCommand createRootCommand(String cmd);
@@ -273,6 +283,20 @@ public abstract class CommandManager <I, FT, F extends MessageFormatter<FT>> {
return message;
}
public void onLocaleChange(IssuerLocaleChangedCallback<AI> onChange) {
localeChangedCallbacks.add(onChange);
}
public void notifyLocaleChange(AI issuer, Locale oldLocale, Locale newLocale) {
localeChangedCallbacks.forEach(cb -> {
try {
cb.onIssuerLocaleChange(issuer, oldLocale, newLocale);
} catch (Exception e) {
this.log(LogLevel.ERROR, "Error in notifyLocaleChange", e);
}
});
}
public Locale getIssuerLocale(CommandIssuer issuer) {
return getLocales().getDefaultLocale();
}
@@ -0,0 +1,30 @@
/*
* Copyright (c) 2016-2017 Daniel Ennis (Aikar) - MIT License
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package co.aikar.commands;
import java.util.Locale;
public interface IssuerLocaleChangedCallback <I extends CommandIssuer> {
void onIssuerLocaleChange(I issuer, Locale oldLocale, Locale newLocale);
}
@@ -72,7 +72,7 @@ public class Locales {
public static final Locale WELSH = new Locale("cy");
private final CommandManager<?, ?, ?> manager;
private final CommandManager<?, ?, ?, ?> manager;
private final LocaleManager<CommandIssuer> localeManager;
private final SetMultimap<String, Locale> loadedBundles = HashMultimap.create();
@@ -40,7 +40,7 @@ import java.util.List;
import java.util.Map;
@SuppressWarnings("WeakerAccess")
public class SpongeCommandManager extends CommandManager<CommandSource, TextColor, SpongeMessageFormatter> {
public class SpongeCommandManager extends CommandManager<CommandSource, SpongeCommandIssuer, TextColor, SpongeMessageFormatter> {
protected final PluginContainer plugin;
protected Map<String, SpongeRootCommand> registeredCommands = new HashMap<>();
@@ -126,7 +126,7 @@ public class SpongeCommandManager extends CommandManager<CommandSource, TextColo
}
@Override
public CommandIssuer getCommandIssuer(Object issuer) {
public SpongeCommandIssuer getCommandIssuer(Object issuer) {
if (!(issuer instanceof CommandSource)) {
throw new IllegalArgumentException(issuer.getClass().getName() + " is not a Command Issuer.");
}