feat(build): rework structure

This commit is contained in:
ineanto 2023-04-03 11:03:05 +02:00
parent 6e0ef4a243
commit d17aadc09a
68 changed files with 171 additions and 194 deletions

View file

@ -0,0 +1,159 @@
package net.artelnatif.nicko;
import com.comphenix.protocol.ProtocolLibrary;
import com.comphenix.protocol.ProtocolManager;
import net.artelnatif.nicko.command.NickoCommand;
import net.artelnatif.nicko.config.Configuration;
import net.artelnatif.nicko.config.ConfigurationManager;
import net.artelnatif.nicko.event.PlayerJoinListener;
import net.artelnatif.nicko.event.PlayerQuitListener;
import net.artelnatif.nicko.gui.items.common.OptionUnavailable;
import net.artelnatif.nicko.gui.items.main.ExitGUI;
import net.artelnatif.nicko.i18n.Locale;
import net.artelnatif.nicko.i18n.LocaleFileManager;
import net.artelnatif.nicko.mojang.MojangAPI;
import net.artelnatif.nicko.placeholder.PlaceHolderHook;
import net.artelnatif.nicko.storage.PlayerDataStore;
import net.artelnatif.nicko.storage.name.PlayerNameStore;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.command.PluginCommand;
import org.bukkit.plugin.PluginDescriptionFile;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.plugin.java.JavaPluginLoader;
import xyz.xenondevs.invui.gui.structure.Structure;
import xyz.xenondevs.invui.item.builder.ItemBuilder;
import xyz.xenondevs.invui.item.impl.SimpleItem;
import java.io.File;
import java.io.IOException;
public class NickoBukkit extends JavaPlugin {
private static NickoBukkit plugin;
private final boolean unitTesting;
private MojangAPI mojangAPI;
private PlayerDataStore dataStore;
private ConfigurationManager configurationManager;
private Configuration configuration;
private LocaleFileManager localeFileManager;
private PlayerNameStore nameStore;
private ProtocolManager protocolManager;
public NickoBukkit() { this.unitTesting = false; }
/**
* Used by MockBukkit
*/
protected NickoBukkit(JavaPluginLoader loader, PluginDescriptionFile description, File dataFolder, File file) {
this(loader, description, dataFolder, file, null);
}
/**
* Used by MockBukkit
*/
protected NickoBukkit(JavaPluginLoader loader, PluginDescriptionFile description, File dataFolder, File file, Configuration configuration) {
super(loader, description, dataFolder, file);
unitTesting = true;
this.configuration = configuration;
getLogger().info("Unit Testing Mode enabled.");
}
@Override
public void onEnable() {
plugin = this;
configurationManager = new ConfigurationManager(getDataFolder());
configurationManager.saveDefaultConfig();
protocolManager = ProtocolLibrary.getProtocolManager();
mojangAPI = new MojangAPI();
dataStore = new PlayerDataStore(mojangAPI, getNickoConfig());
nameStore = new PlayerNameStore();
if (!dataStore.getStorage().isError()) {
getLogger().info("Loading persistence...");
if (!dataStore.getStorage().getProvider().init()) {
dataStore.getStorage().setError(true);
getLogger().severe("Failed to open persistence, data will NOT be saved!");
}
}
if (!unitTesting) {
localeFileManager = new LocaleFileManager();
if (configuration.isCustomLocale()) {
if (localeFileManager.dumpFromLocale(Locale.ENGLISH)) {
getLogger().info("Successfully loaded custom language file.");
} else {
getLogger().severe("Failed to load custom language file!");
}
}
final PluginCommand command = getCommand("nicko");
if (command != null) {
command.setExecutor(new NickoCommand());
}
Structure.addGlobalIngredient('#', new SimpleItem(new ItemBuilder(Material.BLACK_STAINED_GLASS_PANE).setDisplayName(" ")));
Structure.addGlobalIngredient('%', new SimpleItem(new ItemBuilder(Material.ORANGE_STAINED_GLASS_PANE).setDisplayName(" ")));
Structure.addGlobalIngredient('U', new OptionUnavailable());
Structure.addGlobalIngredient('E', new ExitGUI());
new PlaceHolderHook(this).hook();
getServer().getPluginManager().registerEvents(new PlayerJoinListener(), this);
getServer().getPluginManager().registerEvents(new PlayerQuitListener(), this);
getLogger().info("Nicko (Bukkit) has been enabled.");
}
}
@Override
public void onDisable() {
if (!getDataStore().getStorage().isError()) {
getLogger().info("Closing persistence...");
nameStore.clearStoredNames();
Bukkit.getOnlinePlayers().forEach(player -> dataStore.saveData(player));
if (!dataStore.getStorage().getProvider().close()) {
getLogger().severe("Failed to close persistence!");
}
}
getLogger().info("Nicko (Bukkit) has been disabled.");
}
public static NickoBukkit getInstance() {
return plugin;
}
public Configuration getNickoConfig() {
try {
if (configuration == null) { return configuration = configurationManager.load(); }
return configuration;
} catch (IOException e) {
getLogger().severe("Failed to load the configuration file!");
getLogger().severe("It may be have been generated with an older version of Nicko.");
getLogger().severe("Delete the configuration and restart the server please :)");
getLogger().severe("(" + e.getMessage() + ")");
return null;
}
}
public PlayerDataStore getDataStore() {
return dataStore;
}
public PlayerNameStore getNameStore() {
return nameStore;
}
public MojangAPI getMojangAPI() {
return mojangAPI;
}
public LocaleFileManager getLocaleFileManager() {
return localeFileManager;
}
public ProtocolManager getProtocolManager() { return protocolManager; }
}

View file

@ -0,0 +1,106 @@
package net.artelnatif.nicko.anvil;
import net.artelnatif.nicko.NickoBukkit;
import net.artelnatif.nicko.appearance.AppearanceManager;
import net.artelnatif.nicko.disguise.ActionResult;
import net.artelnatif.nicko.i18n.I18N;
import net.artelnatif.nicko.i18n.I18NDict;
import net.artelnatif.nicko.mojang.MojangUtils;
import net.wesjd.anvilgui.AnvilGUI;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import java.util.Collections;
import java.util.List;
public class AnvilManager {
private final Player player;
private final AppearanceManager appearanceManager;
public AnvilManager(Player player) {
this.player = player;
this.appearanceManager = AppearanceManager.get(player);
}
public void openNameThenSkinAnvil() {
getNameThenSkinAnvil().open(player);
}
public void openSkinAnvil() {
getSkinAnvil().open(player);
}
public void openNameAnvil() {
getNameAnvil().open(player);
}
public AnvilGUI.Builder getNameThenSkinAnvil() {
return new AnvilGUI.Builder()
.plugin(NickoBukkit.getInstance())
.itemLeft(getLeftItem(false))
.interactableSlots(AnvilGUI.Slot.OUTPUT)
.onComplete((completion) -> {
if (MojangUtils.isUsernameInvalid(completion.getText())) {
return Collections.singletonList(AnvilGUI.ResponseAction.replaceInputText("Invalid username!"));
} else {
appearanceManager.setName(completion.getText());
openSkinAnvil();
return Collections.singletonList(AnvilGUI.ResponseAction.close());
}
})
.text("New name...");
}
public AnvilGUI.Builder getNameAnvil() {
return new AnvilGUI.Builder()
.plugin(NickoBukkit.getInstance())
.itemLeft(getLeftItem(false))
.interactableSlots(AnvilGUI.Slot.OUTPUT)
.onComplete((completion) -> {
if (MojangUtils.isUsernameInvalid(completion.getText())) {
return Collections.singletonList(AnvilGUI.ResponseAction.replaceInputText("Invalid username!"));
} else {
appearanceManager.setName(completion.getText());
final ActionResult<Void> actionResult = appearanceManager.updatePlayer(false);
return sendResultAndClose(actionResult);
}
})
.text("New name...");
}
private AnvilGUI.Builder getSkinAnvil() {
return new AnvilGUI.Builder()
.plugin(NickoBukkit.getInstance())
.itemLeft(getLeftItem(true))
.interactableSlots(AnvilGUI.Slot.OUTPUT)
.onComplete((completion) -> {
if (MojangUtils.isUsernameInvalid(completion.getText())) {
return Collections.singletonList(AnvilGUI.ResponseAction.replaceInputText("Invalid username!"));
} else {
appearanceManager.setSkin(completion.getText());
final ActionResult<Void> actionResult = appearanceManager.updatePlayer(true);
return sendResultAndClose(actionResult);
}
})
.text("New skin...");
}
private List<AnvilGUI.ResponseAction> sendResultAndClose(ActionResult<Void> actionResult) {
if (!actionResult.isError()) {
player.sendMessage(I18N.translate(player, I18NDict.Event.Disguise.SUCCESS));
} else {
player.sendMessage(I18N.translate(player, I18NDict.Event.Disguise.FAIL, I18N.translateWithoutPrefix(player, actionResult.getErrorMessage())));
}
return Collections.singletonList(AnvilGUI.ResponseAction.close());
}
private ItemStack getLeftItem(boolean skin) {
final ItemStack item = new ItemStack(Material.PAPER);
final ItemMeta meta = item.getItemMeta();
meta.setDisplayName("§0New " + (skin ? "skin" : "name") + "...");
item.setItemMeta(meta);
return item;
}
}

View file

@ -0,0 +1,185 @@
package net.artelnatif.nicko.appearance;
import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.wrappers.*;
import com.google.common.collect.Multimap;
import net.artelnatif.nicko.NickoBukkit;
import net.artelnatif.nicko.disguise.ActionResult;
import net.artelnatif.nicko.disguise.NickoProfile;
import net.artelnatif.nicko.i18n.I18NDict;
import net.artelnatif.nicko.mojang.MojangAPI;
import net.artelnatif.nicko.mojang.MojangSkin;
import net.artelnatif.nicko.storage.PlayerDataStore;
import net.artelnatif.nicko.storage.name.PlayerNameStore;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.entity.Player;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
public class AppearanceManager {
private final NickoProfile profile;
private final Player player;
private final NickoBukkit instance = NickoBukkit.getInstance();
private final PlayerDataStore dataStore = instance.getDataStore();
private final PlayerNameStore nameStore = instance.getNameStore();
private AppearanceManager(UUID uuid) {
this.player = Bukkit.getPlayer(uuid);
this.profile = dataStore.getData(uuid).orElse(NickoProfile.EMPTY_PROFILE.clone());
}
private AppearanceManager(String name) {
this.player = null;
this.profile = dataStore.getOfflineData(name).orElse(NickoProfile.EMPTY_PROFILE.clone());
}
public static AppearanceManager get(Player player) {
return new AppearanceManager(player.getUniqueId());
}
public static AppearanceManager get(String name) {
return new AppearanceManager(name);
}
public boolean hasData() {
return !profile.isEmpty();
}
public void setSkin(String skin) {
profile.setSkin(skin);
}
public String getSkin() {
return profile.getSkin();
}
public boolean needsASkinChange() {
return profile.getSkin() != null && !profile.getSkin().equals(player.getName());
}
public void setName(String name) {
profile.setName(name);
}
public String getName() {
return profile.getName();
}
public NickoProfile getProfile() {
return profile;
}
public void setNameAndSkin(String name, String skin) {
this.profile.setName(name);
this.profile.setSkin(skin);
updatePlayer(true);
}
public ActionResult<Void> reset() {
final String defaultName = nameStore.getStoredName(player);
this.profile.setName(defaultName);
this.profile.setSkin(defaultName);
final ActionResult<Void> actionResult = resetPlayer();
this.profile.setSkin(null);
this.profile.setName(null);
return actionResult;
}
public ActionResult<Void> resetPlayer() {
// TODO: 4/3/23 Reset player
return new ActionResult<>();
}
public ActionResult<Void> updatePlayer(boolean skinChange) {
final String displayName = profile.getName() == null ? player.getName() : profile.getName();
final WrappedGameProfile gameProfile = new WrappedGameProfile(player.getUniqueId(), displayName);
final ActionResult<Void> result = updateGameProfileSkin(gameProfile, skinChange);
if (!result.isError()) {
updateTabList(gameProfile, displayName);
}
return new ActionResult<>();
}
private ActionResult<Void> updateGameProfileSkin(WrappedGameProfile gameProfile, boolean skinChange) {
final boolean changeOnlyName = profile.getSkin() != null && !profile.getSkin().equalsIgnoreCase(player.getName());
if (skinChange || changeOnlyName) {
Optional<MojangSkin> skin;
try {
final MojangAPI mojang = NickoBukkit.getInstance().getMojangAPI();
final Optional<String> uuid = mojang.getUUID(profile.getSkin());
if (uuid.isPresent()) {
skin = mojang.getSkin(uuid.get());
if (skin.isPresent()) {
final MojangSkin skinResult = skin.get();
final Multimap<String, WrappedSignedProperty> properties = gameProfile.getProperties();
properties.removeAll("textures");
properties.put("textures", new WrappedSignedProperty("textures", skinResult.getValue(), skinResult.getSignature()));
Bukkit.broadcastMessage("Modified properties");
}
}
Bukkit.broadcastMessage("Respawning player");
respawnPlayer();
return new ActionResult<>();
} catch (ExecutionException e) {
return new ActionResult<>(I18NDict.Error.SKIN_FAIL_CACHE);
} catch (IOException e) {
return new ActionResult<>(I18NDict.Error.NAME_FAIL_MOJANG);
}
}
return new ActionResult<>();
}
private void respawnPlayer() {
final PacketContainer respawnOtherWorld = getRespawnPacket(Bukkit.getWorld("world_the_end"));
final PacketContainer respawn = getRespawnPacket(player.getWorld());
instance.getProtocolManager().sendServerPacket(player, respawnOtherWorld);
instance.getProtocolManager().sendServerPacket(player, respawn);
}
private PacketContainer getRespawnPacket(World world) {
final PacketContainer packet = new PacketContainer(PacketType.Play.Server.RESPAWN);
final EnumWrappers.NativeGameMode gamemode = EnumWrappers.NativeGameMode.fromBukkit(player.getGameMode());
packet.getWorldKeys().write(0, world);
packet.getLongs().write(0, world.getSeed());
packet.getGameModes().write(0, gamemode); // gamemode
packet.getGameModes().write(1, gamemode); // previous gamemode
packet.getBooleans().write(0, false);
packet.getBooleans().write(1, false);
return packet;
}
private void updateTabList(WrappedGameProfile gameProfile, String displayName) {
final PacketContainer infoAdd = new PacketContainer(PacketType.Play.Server.PLAYER_INFO);
infoAdd.getPlayerInfoActions().write(0, Set.of(
EnumWrappers.PlayerInfoAction.ADD_PLAYER,
EnumWrappers.PlayerInfoAction.UPDATE_GAME_MODE,
EnumWrappers.PlayerInfoAction.UPDATE_DISPLAY_NAME,
EnumWrappers.PlayerInfoAction.UPDATE_LISTED,
EnumWrappers.PlayerInfoAction.UPDATE_LATENCY
));
infoAdd.getPlayerInfoDataLists().write(1, List.of(new PlayerInfoData(
gameProfile,
0,
EnumWrappers.NativeGameMode.fromBukkit(player.getGameMode()),
WrappedChatComponent.fromText(displayName)
)));
final PacketContainer infoRemove = new PacketContainer(PacketType.Play.Server.PLAYER_INFO_REMOVE);
infoRemove.getUUIDLists().write(0, List.of(player.getUniqueId()));
instance.getProtocolManager().broadcastServerPacket(infoRemove);
instance.getProtocolManager().broadcastServerPacket(infoAdd);
}
}

View file

@ -0,0 +1,48 @@
package net.artelnatif.nicko.command;
import net.artelnatif.nicko.NickoBukkit;
import net.artelnatif.nicko.command.sub.NickoCheckSubCmd;
import net.artelnatif.nicko.command.sub.NickoDebugSubCmd;
import net.artelnatif.nicko.gui.MainGUI;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
public class NickoCommand implements CommandExecutor {
private String helpMessage = "§cNicko §8§o[{version}] §f- §2Help:\n" +
"§6/nicko §f- §7Open the GUI.\n" +
"§6/nicko help §f- §7Print this help message.\n";
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (sender instanceof Player) {
Player player = (Player) sender;
if (args.length >= 1) {
switch (args[0]) {
case "debug":
new NickoDebugSubCmd().execute(player, args);
break;
case "check":
new NickoCheckSubCmd().execute(player, args);
break;
default:
sendHelpMessage(sender);
break;
}
return false;
}
new MainGUI(player).open();
return false;
}
sender.sendMessage("This plugin can only be used in-game. Sorry!");
return false;
}
public void sendHelpMessage(CommandSender sender) {
helpMessage = helpMessage.replace("{version}", NickoBukkit.getInstance().getDescription().getVersion());
sender.sendMessage(helpMessage);
}
}

View file

@ -0,0 +1,42 @@
package net.artelnatif.nicko.command.sub;
import net.artelnatif.nicko.NickoBukkit;
import net.artelnatif.nicko.appearance.AppearanceManager;
import net.artelnatif.nicko.i18n.I18N;
import net.artelnatif.nicko.i18n.I18NDict;
import net.artelnatif.nicko.mojang.MojangUtils;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.util.StringJoiner;
public class NickoCheckSubCmd {
public void execute(Player player, String[] args) {
final String targetName = args[1];
final Player target = Bukkit.getPlayerExact(targetName);
AppearanceManager appearanceManager;
if (MojangUtils.isUsernameInvalid(targetName)) {
player.sendMessage(I18N.translate(player, I18NDict.Error.INVALID_USERNAME));
return;
}
if (target == null) {
appearanceManager = AppearanceManager.get(targetName);
} else {
appearanceManager = AppearanceManager.get(target);
}
final StringJoiner builder = new StringJoiner("\n");
builder.add("§c" + NickoBukkit.getInstance().getNickoConfig().getPrefix() + "§6Check for: §f§o" + targetName);
if (!appearanceManager.hasData()) {
builder.add("§cThis player has not data.");
} else {
builder.add("§7- §fNicked: §a✔");
builder.add("§7- §fName: §6" + appearanceManager.getName());
builder.add("§7- §fSkin: §6" + appearanceManager.getSkin());
}
player.sendMessage(builder.toString());
}
}

View file

@ -0,0 +1,48 @@
package net.artelnatif.nicko.command.sub;
import net.artelnatif.nicko.NickoBukkit;
import net.artelnatif.nicko.appearance.AppearanceManager;
import net.artelnatif.nicko.mojang.MojangUtils;
import org.bukkit.Bukkit;
import org.bukkit.Sound;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
public class NickoDebugSubCmd {
public void execute(CommandSender sender, String[] args) {
final String prefix = NickoBukkit.getInstance().getNickoConfig().getPrefix();
Player target;
String name, skin;
if (args.length == 3) {
target = (Player) sender;
name = args[1];
skin = args[2];
} else {
if (args.length < 3) {
sender.sendMessage(prefix + "§cMissing argument.");
return;
}
final String playerName = args[1];
name = args[2];
skin = args[3];
target = Bukkit.getPlayer(playerName);
if (target == null) {
sender.sendMessage(prefix + "§cSpecified player is offline.");
return;
}
}
final AppearanceManager appearanceManager = AppearanceManager.get(target.getPlayer());
if (MojangUtils.isUsernameInvalid(name) || MojangUtils.isUsernameInvalid(skin)) {
sender.sendMessage(prefix + "§cSpecified username is invalid.");
}
appearanceManager.setNameAndSkin(name, skin);
target.sendMessage(prefix + "§aWhoosh!");
target.playSound(target.getLocation(), Sound.ENTITY_ITEM_FRAME_PLACE, 1, 1);
}
}

View file

@ -0,0 +1,55 @@
package net.artelnatif.nicko.config;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* TODO: 4/2/23 Convert using Bukkit YAML API
* <a href="https://www.spigotmc.org/threads/tutorial-bukkit-custom-serialization.148781/">Link</a>
**/
public class Configuration {
@JsonProperty("sql")
private final DataSourceConfiguration sqlConfiguration;
@JsonProperty("redis")
private final DataSourceConfiguration redisConfiguration;
private final String prefix;
private final Boolean local;
private final Boolean customLocale;
public Configuration(DataSourceConfiguration sqlConfiguration, DataSourceConfiguration redisConfiguration, String prefix, Boolean local, Boolean customLocale) {
this.sqlConfiguration = sqlConfiguration;
this.redisConfiguration = redisConfiguration;
this.prefix = prefix;
this.local = local;
this.customLocale = customLocale;
}
public Configuration() {
this(
new DataSourceConfiguration("", 3306, "", ""),
new DataSourceConfiguration("", 6379, "", ""),
"",
false,
false
);
}
public DataSourceConfiguration getSqlConfiguration() {
return sqlConfiguration;
}
public DataSourceConfiguration getRedisConfiguration() {
return redisConfiguration;
}
public String getPrefix() {
return prefix;
}
public Boolean isLocal() {
return local;
}
public Boolean isCustomLocale() {
return customLocale;
}
}

View file

@ -0,0 +1,50 @@
package net.artelnatif.nicko.config;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.logging.Logger;
public class ConfigurationManager {
private final Logger logger = Logger.getLogger("ConfigurationManager");
private final ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
private final File file;
public ConfigurationManager(File directory) {
this.file = new File(directory, "config.yml");
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
public void save(Configuration configuration) throws IOException {
try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) {
mapper.writeValue(writer, configuration);
writer.flush();
}
}
public void saveDefaultConfig() {
if (!file.exists()) {
try {
final InputStream input = getClass().getResourceAsStream("/config.yml");
if (input != null) {
logger.info("Saved default configuration as config.yml");
Files.createDirectories(file.getParentFile().toPath());
Files.createFile(file.toPath());
Files.copy(input, file.toPath(), StandardCopyOption.REPLACE_EXISTING);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
public Configuration load() throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
return mapper.readValue(reader, Configuration.class);
}
}
}

View file

@ -0,0 +1,36 @@
package net.artelnatif.nicko.config;
public class DataSourceConfiguration {
public static final DataSourceConfiguration SQL_EMPTY = new DataSourceConfiguration("127.0.0.1", 3306, "root", "");
public static final DataSourceConfiguration REDIS_EMPTY = new DataSourceConfiguration("127.0.0.1", 6379, "", "");
private final String address;
private final Integer port;
private final String username;
private final String password;
public DataSourceConfiguration(String address, Integer port, String username, String password) {
this.address = address;
this.port = port;
this.username = username;
this.password = password;
}
public DataSourceConfiguration() { this("", 0, "", ""); }
public String getAddress() {
return address;
}
public Integer getPort() {
return port;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
}

View file

@ -0,0 +1,36 @@
package net.artelnatif.nicko.disguise;
import net.artelnatif.nicko.i18n.I18NDict;
public class ActionResult<R> {
private final I18NDict errorMessage;
private boolean error = false;
private R result;
public ActionResult() {
this.errorMessage = null;
}
public ActionResult(I18NDict errorMessage) {
this.errorMessage = errorMessage;
this.error = true;
this.result = null;
}
public ActionResult(R result) {
this.errorMessage = null;
this.result = result;
}
public R getResult() {
return result;
}
public boolean isError() {
return error;
}
public I18NDict getErrorMessage() {
return errorMessage;
}
}

View file

@ -0,0 +1,72 @@
package net.artelnatif.nicko.disguise;
import net.artelnatif.nicko.i18n.Locale;
public class NickoProfile implements Cloneable {
public static final NickoProfile EMPTY_PROFILE = new NickoProfile(null, null, Locale.ENGLISH, true);
private String name;
private String skin;
private Locale locale;
private boolean bungeecordTransfer;
public NickoProfile(String name, String skin, Locale locale, boolean bungeecordTransfer) {
this.name = name;
this.skin = skin;
this.locale = locale;
this.bungeecordTransfer = bungeecordTransfer;
}
public boolean isEmpty() {
return name == null && skin == null;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSkin() {
return skin;
}
public void setSkin(String skin) {
this.skin = skin;
}
public Locale getLocale() { return locale; }
public void setLocale(Locale locale) { this.locale = locale; }
public boolean isBungeecordTransfer() {
return bungeecordTransfer;
}
public void setBungeecordTransfer(boolean bungeecordTransfer) {
this.bungeecordTransfer = bungeecordTransfer;
}
@Override
public String toString() {
return "NickoProfile{" +
"name='" + name + '\'' +
", skin='" + skin + '\'' +
", locale=" + locale +
", bungeecordTransfer=" + bungeecordTransfer +
'}';
}
@Override
public NickoProfile clone() {
Object o;
try {
o = super.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
return (NickoProfile) o;
}
}

View file

@ -0,0 +1,42 @@
package net.artelnatif.nicko.event;
import net.artelnatif.nicko.NickoBukkit;
import net.artelnatif.nicko.appearance.AppearanceManager;
import net.artelnatif.nicko.disguise.ActionResult;
import net.artelnatif.nicko.disguise.NickoProfile;
import net.artelnatif.nicko.i18n.I18N;
import net.artelnatif.nicko.i18n.I18NDict;
import net.artelnatif.nicko.storage.PlayerDataStore;
import net.artelnatif.nicko.storage.name.PlayerNameStore;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
public class PlayerJoinListener implements Listener {
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
final Player player = event.getPlayer();
final NickoBukkit instance = NickoBukkit.getInstance();
final PlayerDataStore dataStore = instance.getDataStore();
final PlayerNameStore nameStore = instance.getNameStore();
nameStore.storeName(player);
// TODO: 2/20/23 BungeeCord transfer
dataStore.performProfileUpdate(player.getUniqueId(), NickoProfile.EMPTY_PROFILE);
Bukkit.getScheduler().runTaskLater(instance, () -> {
final AppearanceManager appearanceManager = AppearanceManager.get(player);
if (appearanceManager.hasData()) {
final ActionResult<Void> actionResult = appearanceManager.updatePlayer(appearanceManager.needsASkinChange());
if (!actionResult.isError()) {
player.sendMessage(I18N.translate(player, I18NDict.Event.PreviousSkin.SUCCESS));
} else {
player.sendMessage(I18N.translate(player, I18NDict.Event.PreviousSkin.FAIL, I18N.translateWithoutPrefix(player, actionResult.getErrorMessage())));
}
}
}, 20L);
}
}

View file

@ -0,0 +1,19 @@
package net.artelnatif.nicko.event;
import net.artelnatif.nicko.NickoBukkit;
import net.artelnatif.nicko.disguise.ActionResult;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerQuitEvent;
public class PlayerQuitListener implements Listener {
@EventHandler
public void onPlayerQuit(PlayerQuitEvent event) {
final Player player = event.getPlayer();
final ActionResult<Void> result = NickoBukkit.getInstance().getDataStore().saveData(player);
if (result.isError()) {
NickoBukkit.getInstance().getLogger().warning("Failed to save data for " + player.getName());
}
}
}

View file

@ -0,0 +1,35 @@
package net.artelnatif.nicko.gui;
import net.artelnatif.nicko.gui.items.admin.ManageCache;
import net.artelnatif.nicko.gui.items.common.GoBack;
import org.bukkit.entity.Player;
import xyz.xenondevs.invui.gui.Gui;
import xyz.xenondevs.invui.window.Window;
public class AdminGUI {
public static final String TITLE = "Nicko > Administration";
private final Player player;
private final Gui gui;
public AdminGUI(Player player) {
this.gui = Gui.normal()
.setStructure(
"# # # # # # # # #",
"# # # S U U # # #",
"B # # # # # # # #"
)
.addIngredient('S', new ManageCache())
.addIngredient('B', new GoBack(new MainGUI(player).getGUI()))
.build();
this.player = player;
}
public Gui getGUI() {
return gui;
}
public void open() {
Window.single().setGui(gui).setTitle(TITLE).open(player);
}
}

View file

@ -0,0 +1,46 @@
package net.artelnatif.nicko.gui;
import net.artelnatif.nicko.gui.items.main.AdminSubGUI;
import net.artelnatif.nicko.gui.items.main.ResetAppearance;
import net.artelnatif.nicko.gui.items.main.SettingsSubGUI;
import net.artelnatif.nicko.gui.items.skin.ChangeName;
import net.artelnatif.nicko.gui.items.skin.ChangeNameAndSkin;
import net.artelnatif.nicko.gui.items.skin.ChangeSkin;
import org.bukkit.entity.Player;
import xyz.xenondevs.invui.gui.Gui;
import xyz.xenondevs.invui.window.Window;
public class MainGUI {
private final Player player;
private final Gui gui;
public MainGUI(Player player) {
final String[] dynamicStructure = new String[]{
"# # # # # # # # #",
"# # # N B S # # #",
"E P A # # # # # R"};
if (!player.hasPermission("nicko.admin") || !player.isOp()) {
dynamicStructure[2] = dynamicStructure[2].replace("A", "#");
}
this.gui = Gui.normal()
.setStructure(dynamicStructure)
.addIngredient('R', new ResetAppearance())
.addIngredient('N', new ChangeName())
.addIngredient('B', new ChangeNameAndSkin())
.addIngredient('S', new ChangeSkin(player))
.addIngredient('P', new SettingsSubGUI())
.addIngredient('A', new AdminSubGUI())
.build();
this.player = player;
}
public Gui getGUI() {
return gui;
}
public void open() {
Window.single().setGui(gui).setTitle("Nicko - Home").open(player);
}
}

View file

@ -0,0 +1,38 @@
package net.artelnatif.nicko.gui;
import net.artelnatif.nicko.gui.items.common.GoBack;
import net.artelnatif.nicko.gui.items.settings.BungeeCordCycling;
import net.artelnatif.nicko.gui.items.settings.LanguageCycling;
import org.bukkit.entity.Player;
import xyz.xenondevs.invui.gui.Gui;
import xyz.xenondevs.invui.window.Window;
public class SettingsGUI {
public static final String TITLE = "Nicko > Settings";
private final Player player;
private final Gui gui;
public SettingsGUI(Player player) {
final String[] dynamicStructure = new String[]{
"# # # # # # # # #",
"# # # L T U # # #",
"B # # # # # # # #"
};
// TODO: 3/6/23 Replace when Redis is not enabled
dynamicStructure[1] = dynamicStructure[1].replace("T", "U");
this.gui = Gui.normal()
.setStructure(dynamicStructure)
.addIngredient('B', new GoBack(new MainGUI(player).getGUI()))
.addIngredient('L', new LanguageCycling().get(player))
.addIngredient('T', new BungeeCordCycling().get(player))
.build();
this.player = player;
}
public void open() {
Window.single().setGui(gui).setTitle(TITLE).open(player);
}
}

View file

@ -0,0 +1,36 @@
package net.artelnatif.nicko.gui.admin;
import net.artelnatif.nicko.gui.AdminGUI;
import net.artelnatif.nicko.gui.items.admin.cache.CacheDetailed;
import net.artelnatif.nicko.gui.items.admin.cache.CacheInvalidate;
import net.artelnatif.nicko.gui.items.admin.cache.CacheOverview;
import net.artelnatif.nicko.gui.items.common.GoBack;
import org.bukkit.entity.Player;
import xyz.xenondevs.invui.gui.Gui;
import xyz.xenondevs.invui.window.Window;
public class CacheManagementGUI {
public static final String TITLE = "Nicko > Admin... > Cache";
private final Player player;
private final Gui gui;
public CacheManagementGUI(Player player) {
this.gui = Gui.normal()
.setStructure("B # S A D")
.addIngredient('B', new GoBack(new AdminGUI(player).getGUI()))
.addIngredient('S', new CacheOverview())
.addIngredient('A', new CacheInvalidate())
.addIngredient('D', new CacheDetailed())
.build();
this.player = player;
}
public Gui getGUI() {
return gui;
}
public void open() {
Window.single().setGui(gui).setTitle(TITLE).open(player);
}
}

View file

@ -0,0 +1,61 @@
package net.artelnatif.nicko.gui.admin.cache;
import xyz.xenondevs.invui.gui.Gui;
import xyz.xenondevs.invui.gui.ScrollGui;
import xyz.xenondevs.invui.gui.structure.Markers;
import xyz.xenondevs.invui.item.Item;
import net.artelnatif.nicko.NickoBukkit;
import net.artelnatif.nicko.gui.items.admin.cache.SkinPlaceholder;
import net.artelnatif.nicko.gui.admin.CacheManagementGUI;
import net.artelnatif.nicko.gui.items.common.GoBack;
import net.artelnatif.nicko.gui.items.common.ScrollDown;
import net.artelnatif.nicko.gui.items.common.ScrollUp;
import net.artelnatif.nicko.mojang.MojangSkin;
import org.bukkit.entity.Player;
import xyz.xenondevs.invui.window.Window;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentMap;
import java.util.stream.Collectors;
public class CacheDetailedGUI {
public static final String TITLE = "... > Cache > Invalidate";
private final Player player;
private final Gui gui;
public CacheDetailedGUI(Player player) {
final ConcurrentMap<String, Optional<MojangSkin>> skins = NickoBukkit.getInstance().getMojangAPI().getCache().asMap();
final List<String> loadedSkins = skins.entrySet().stream()
.filter(entry -> entry.getValue().isPresent())
.map(Map.Entry::getKey)
.collect(Collectors.toList());
final List<Item> items = loadedSkins.stream()
.map(SkinPlaceholder::new)
.collect(Collectors.toList());
gui = ScrollGui.items(guiItemBuilder -> {
guiItemBuilder.setStructure(
"# # # # # # # # #",
"# x x x x x x U #",
"# x x x x x x # #",
"# x x x x x x # #",
"# x x x x x x D #",
"B # # # # # # # #");
guiItemBuilder.addIngredient('x', Markers.CONTENT_LIST_SLOT_HORIZONTAL);
guiItemBuilder.addIngredient('U', new ScrollUp());
guiItemBuilder.addIngredient('D', new ScrollDown());
guiItemBuilder.addIngredient('B', new GoBack(new CacheManagementGUI(player).getGUI()));
guiItemBuilder.setContent(items);
});
this.player = player;
}
public void open() {
Window.single().setGui(gui).setTitle(TITLE).open(player);
}
}

View file

@ -0,0 +1,33 @@
package net.artelnatif.nicko.gui.items.admin;
import xyz.xenondevs.invui.item.builder.ItemBuilder;
import xyz.xenondevs.invui.item.builder.SkullBuilder;
import xyz.xenondevs.invui.item.impl.AsyncItem;
import net.artelnatif.nicko.gui.admin.CacheManagementGUI;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.ClickType;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.jetbrains.annotations.NotNull;
public class ManageCache extends AsyncItem {
public ManageCache() {
super(new ItemBuilder(Material.PAINTING)
.setDisplayName("§fManage §6skin §fcache...")
.addLoreLines("§7Access the skin cache management panel."),
() -> {
final SkullBuilder builder = new SkullBuilder("Notch");
builder.setDisplayName("§fManage §6skin §fcache...");
builder.addLoreLines("§7Access the skin cache management panel.");
return builder;
});
}
@Override
public void handleClick(@NotNull ClickType clickType, @NotNull Player player, @NotNull InventoryClickEvent event) {
if (clickType.isLeftClick() || clickType.isRightClick()) {
event.getView().close();
new CacheManagementGUI(player).open();
}
}
}

View file

@ -0,0 +1,26 @@
package net.artelnatif.nicko.gui.items.admin.cache;
import net.artelnatif.nicko.gui.admin.cache.CacheDetailedGUI;
import org.bukkit.Material;
import org.bukkit.event.inventory.ClickType;
import xyz.xenondevs.invui.item.builder.ItemBuilder;
import xyz.xenondevs.invui.item.impl.SuppliedItem;
public class CacheDetailed extends SuppliedItem {
public CacheDetailed() {
super(() -> {
final ItemBuilder builder = new ItemBuilder(Material.PAPER);
builder.setDisplayName("§6Invalidate specific skin...");
builder.addLoreLines("§7Select a specific skin to invalidate.");
return builder;
}, (click) -> {
final ClickType clickType = click.getClickType();
if (clickType.isLeftClick() || clickType.isRightClick()) {
click.getEvent().getView().close();
new CacheDetailedGUI(click.getPlayer()).open();
return true;
}
return false;
});
}
}

View file

@ -0,0 +1,36 @@
package net.artelnatif.nicko.gui.items.admin.cache;
import net.artelnatif.nicko.NickoBukkit;
import net.artelnatif.nicko.i18n.I18N;
import net.artelnatif.nicko.i18n.I18NDict;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.ClickType;
import xyz.xenondevs.invui.item.builder.ItemBuilder;
import xyz.xenondevs.invui.item.impl.SuppliedItem;
public class CacheInvalidate extends SuppliedItem {
public CacheInvalidate() {
super(() -> {
final ItemBuilder builder = new ItemBuilder(Material.TNT);
builder.setDisplayName("§fInvalidate §6skin cache");
builder.addLoreLines(
"§c§oNOT RECOMMENDED",
"§7Invalidates every skin entry present in the cache.",
"§7Does not reset player disguises.",
"§7Could be useful if a skin has been updated",
"§7recently and the cache is now outdated.");
return builder;
}, (click) -> {
final ClickType clickType = click.getClickType();
if (clickType.isLeftClick() || clickType.isRightClick()) {
final Player player = click.getPlayer();
click.getEvent().getView().close();
player.sendMessage(I18N.translate(player, I18NDict.Event.Admin.CACHE_CLEAN));
NickoBukkit.getInstance().getMojangAPI().getCache().invalidateAll();
return true;
}
return false;
});
}
}

View file

@ -0,0 +1,28 @@
package net.artelnatif.nicko.gui.items.admin.cache;
import com.google.common.cache.CacheStats;
import com.google.common.cache.LoadingCache;
import net.artelnatif.nicko.NickoBukkit;
import net.artelnatif.nicko.mojang.MojangSkin;
import org.bukkit.Material;
import xyz.xenondevs.invui.item.builder.ItemBuilder;
import xyz.xenondevs.invui.item.impl.SuppliedItem;
import java.util.Optional;
public class CacheOverview extends SuppliedItem {
public CacheOverview() {
super(() -> {
final ItemBuilder builder = new ItemBuilder(Material.OAK_SIGN);
final LoadingCache<String, Optional<MojangSkin>> cache = NickoBukkit.getInstance().getMojangAPI().getCache();
final CacheStats stats = cache.stats();
builder.setDisplayName("§6Skin cache §foverview:");
builder.addLoreLines(
"Request Count: §2" + stats.requestCount(),
"Skin Cached: §2" + Math.round(cache.size()),
"§7§oCache is cleared every 24 hours.",
"§7§o(Click to refresh)");
return builder;
}, (event) -> true);
}
}

View file

@ -0,0 +1,21 @@
package net.artelnatif.nicko.gui.items.admin.cache;
import org.bukkit.Material;
import xyz.xenondevs.invui.item.builder.ItemBuilder;
import xyz.xenondevs.invui.item.builder.SkullBuilder;
import xyz.xenondevs.invui.item.impl.AsyncItem;
import java.util.UUID;
public class SkinPlaceholder extends AsyncItem {
public SkinPlaceholder(String name) {
super(new ItemBuilder(Material.PAINTING).setDisplayName("§7§oLoading..."), () -> {
final String stringUUID = name.replaceAll("(.{8})(.{4})(.{4})(.{4})(.+)", "$1-$2-$3-$4-$5");
final UUID uuid = UUID.fromString(stringUUID);
final SkullBuilder skull = new SkullBuilder(uuid);
skull.setDisplayName("§6Skin Entry");
skull.addLoreLines("§7Click to invalidate skin");
return skull;
});
}
}

View file

@ -0,0 +1,23 @@
package net.artelnatif.nicko.gui.items.common;
import org.bukkit.Material;
import xyz.xenondevs.invui.gui.Gui;
import xyz.xenondevs.invui.item.builder.ItemBuilder;
import xyz.xenondevs.invui.item.impl.SuppliedItem;
import xyz.xenondevs.invui.window.Window;
public class GoBack extends SuppliedItem {
public GoBack(Gui gui) {
super(() -> {
final ItemBuilder builder = new ItemBuilder(Material.ARROW);
builder.setDisplayName("Go back");
builder.addLoreLines("§7Return to the previous window.");
return builder;
}, click -> {
click.getEvent().getView().close();
// TODO: 4/1/23 Get title of parent GUI
Window.single().setGui(gui).setTitle("Nicko").open(click.getPlayer());
return true;
});
}
}

View file

@ -0,0 +1,16 @@
package net.artelnatif.nicko.gui.items.common;
import org.bukkit.Material;
import xyz.xenondevs.invui.item.builder.ItemBuilder;
import xyz.xenondevs.invui.item.impl.SuppliedItem;
public class OptionUnavailable extends SuppliedItem {
public OptionUnavailable() {
super(() -> {
final ItemBuilder builder = new ItemBuilder(Material.RED_TERRACOTTA);
builder.setDisplayName("§cFeature unavailable :(");
builder.addLoreLines("§7This button is disabled.");
return builder;
}, click -> true);
}
}

View file

@ -0,0 +1,25 @@
package net.artelnatif.nicko.gui.items.common;
import org.bukkit.Material;
import xyz.xenondevs.invui.gui.ScrollGui;
import xyz.xenondevs.invui.item.ItemProvider;
import xyz.xenondevs.invui.item.builder.ItemBuilder;
import xyz.xenondevs.invui.item.impl.controlitem.ScrollItem;
public class ScrollDown extends ScrollItem {
public ScrollDown() {
super(1);
}
@Override
public ItemProvider getItemProvider(ScrollGui gui) {
ItemBuilder builder = new ItemBuilder(Material.GREEN_STAINED_GLASS_PANE);
builder.setDisplayName("§7Scroll down");
if (!gui.canScroll(1))
builder.addLoreLines("§cYou can't scroll further down");
return builder;
}
}

View file

@ -0,0 +1,26 @@
package net.artelnatif.nicko.gui.items.common;
import org.bukkit.Material;
import xyz.xenondevs.invui.gui.ScrollGui;
import xyz.xenondevs.invui.item.ItemProvider;
import xyz.xenondevs.invui.item.builder.ItemBuilder;
import xyz.xenondevs.invui.item.impl.controlitem.ScrollItem;
public class ScrollUp extends ScrollItem {
public ScrollUp() {
super(-1);
}
@Override
public ItemProvider getItemProvider(ScrollGui gui) {
ItemBuilder builder = new ItemBuilder(Material.RED_STAINED_GLASS_PANE);
builder.setDisplayName("§7Scroll up");
if (!gui.canScroll(-1))
builder.addLoreLines("§cYou've reached the top");
return builder;
}
}

View file

@ -0,0 +1,30 @@
package net.artelnatif.nicko.gui.items.main;
import net.artelnatif.nicko.gui.AdminGUI;
import org.bukkit.Material;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.event.inventory.ClickType;
import org.bukkit.inventory.ItemFlag;
import xyz.xenondevs.invui.item.builder.ItemBuilder;
import xyz.xenondevs.invui.item.impl.SuppliedItem;
public class AdminSubGUI extends SuppliedItem {
public AdminSubGUI() {
super(() -> {
final ItemBuilder builder = new ItemBuilder(Material.COMMAND_BLOCK);
builder.addEnchantment(Enchantment.DAMAGE_ALL, 1, false);
builder.addItemFlags(ItemFlag.HIDE_ENCHANTS);
builder.setDisplayName("§cAdministration panel...");
builder.addLoreLines("§7Access the administration panel.");
return builder;
}, click -> {
final ClickType clickType = click.getClickType();
if (clickType.isLeftClick() || clickType.isRightClick()) {
click.getEvent().getView().close();
new AdminGUI(click.getPlayer()).open();
return true;
}
return false;
});
}
}

View file

@ -0,0 +1,17 @@
package net.artelnatif.nicko.gui.items.main;
import org.bukkit.Material;
import org.bukkit.event.inventory.ClickType;
import xyz.xenondevs.invui.item.builder.ItemBuilder;
import xyz.xenondevs.invui.item.impl.SimpleItem;
public class ExitGUI extends SimpleItem {
public ExitGUI() {
super(new ItemBuilder(Material.OAK_DOOR).setDisplayName("§fExit"), click -> {
final ClickType clickType = click.getClickType();
if (clickType.isLeftClick() || clickType.isRightClick()) {
click.getEvent().getView().close();
}
});
}
}

View file

@ -0,0 +1,42 @@
package net.artelnatif.nicko.gui.items.main;
import net.artelnatif.nicko.appearance.AppearanceManager;
import net.artelnatif.nicko.i18n.I18N;
import net.artelnatif.nicko.i18n.I18NDict;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.ClickType;
import xyz.xenondevs.invui.item.builder.ItemBuilder;
import xyz.xenondevs.invui.item.impl.SuppliedItem;
public class ResetAppearance extends SuppliedItem {
public ResetAppearance() {
super(() -> {
final ItemBuilder builder = new ItemBuilder(Material.TNT);
builder.setDisplayName("§fReset");
builder.addLoreLines("§7Get rid of your disguise.");
return builder;
}, (event) -> {
final Player player = event.getPlayer();
final ClickType clickType = event.getClickType();
if (clickType.isLeftClick() || clickType.isRightClick()) {
final AppearanceManager appearanceManager = AppearanceManager.get(player);
if (!appearanceManager.hasData()) {
player.sendMessage(I18N.translate(player, I18NDict.Event.Undisguise.NONE));
event.getEvent().getView().close();
return true;
}
if (!appearanceManager.reset().isError()) {
player.sendMessage(I18N.translate(player, I18NDict.Event.Undisguise.SUCCESS));
return true;
} else {
player.sendMessage(I18N.translate(player, I18NDict.Event.Undisguise.FAIL));
return false;
}
}
return false;
});
}
}

View file

@ -0,0 +1,26 @@
package net.artelnatif.nicko.gui.items.main;
import net.artelnatif.nicko.gui.SettingsGUI;
import org.bukkit.Material;
import org.bukkit.event.inventory.ClickType;
import xyz.xenondevs.invui.item.builder.ItemBuilder;
import xyz.xenondevs.invui.item.impl.SuppliedItem;
public class SettingsSubGUI extends SuppliedItem {
public SettingsSubGUI() {
super(() -> {
final ItemBuilder builder = new ItemBuilder(Material.COMPARATOR);
builder.setDisplayName("§fSettings...");
builder.addLoreLines("§7Adjust your preferences.");
return builder;
}, click -> {
final ClickType clickType = click.getClickType();
if (clickType.isLeftClick() || clickType.isRightClick()) {
click.getEvent().getView().close();
new SettingsGUI(click.getPlayer()).open();
return true;
}
return false;
});
}
}

View file

@ -0,0 +1,49 @@
package net.artelnatif.nicko.gui.items.settings;
import net.artelnatif.nicko.NickoBukkit;
import net.artelnatif.nicko.disguise.NickoProfile;
import org.bukkit.Material;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
import xyz.xenondevs.invui.item.ItemProvider;
import xyz.xenondevs.invui.item.builder.ItemBuilder;
import xyz.xenondevs.invui.item.impl.AbstractItem;
import xyz.xenondevs.invui.item.impl.CycleItem;
import xyz.xenondevs.invui.item.impl.SimpleItem;
import java.util.Optional;
public class BungeeCordCycling {
private final ItemProvider[] providers = new ItemProvider[]{
getItemProviderForValue(true),
getItemProviderForValue(false)
};
public AbstractItem get(Player player) {
final Optional<NickoProfile> profile = NickoBukkit.getInstance().getDataStore().getData(player.getUniqueId());
if (profile.isPresent()) {
final NickoProfile nickoProfile = profile.get();
int startingState = nickoProfile.isBungeecordTransfer() ? 0 : 1;
return CycleItem.withStateChangeHandler((observer, integer) -> {
nickoProfile.setBungeecordTransfer(integer != 1);
observer.playSound(player, Sound.UI_BUTTON_CLICK, 1f, 0.707107f); // 0.707107 ~= C
}, startingState, providers);
}
return new SimpleItem(ItemProvider.EMPTY);
}
private ItemProvider getItemProviderForValue(boolean enabled) {
final ItemBuilder builder = new ItemBuilder(Material.COMPASS);
builder.setDisplayName("§6BungeeCord transfer:");
if (enabled) {
builder.addLoreLines("§7> §cDisabled");
builder.addLoreLines("§6§l> §a§lEnabled");
} else {
builder.addLoreLines("§6§l> §c§lDisabled");
builder.addLoreLines("§7> §aEnabled");
}
builder.addLoreLines("§7§oCycle through the values by", "§7§oleft and right clicking.");
return builder;
}
}

View file

@ -0,0 +1,63 @@
package net.artelnatif.nicko.gui.items.settings;
import net.artelnatif.nicko.NickoBukkit;
import net.artelnatif.nicko.disguise.NickoProfile;
import net.artelnatif.nicko.i18n.Locale;
import org.bukkit.Material;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
import xyz.xenondevs.invui.item.ItemProvider;
import xyz.xenondevs.invui.item.builder.ItemBuilder;
import xyz.xenondevs.invui.item.impl.AbstractItem;
import xyz.xenondevs.invui.item.impl.CycleItem;
import xyz.xenondevs.invui.item.impl.SimpleItem;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
public class LanguageCycling {
private final ItemProvider[] providers = getItems();
public AbstractItem get(Player player) {
final Optional<NickoProfile> profile = NickoBukkit.getInstance().getDataStore().getData(player.getUniqueId());
if (profile.isPresent()) {
final NickoProfile nickoProfile = profile.get();
int localeOrdinal = nickoProfile.getLocale().ordinal();
return CycleItem.withStateChangeHandler((observer, integer) -> {
nickoProfile.setLocale(Locale.values()[integer]);
observer.playSound(player, Sound.UI_BUTTON_CLICK, 1f, 0.707107f); // 0.707107 ~= C
}, localeOrdinal, providers);
}
return new SimpleItem(ItemProvider.EMPTY);
}
private ItemProvider generateItem(Locale locale, List<Locale> locales) {
final ItemBuilder builder = new ItemBuilder(Material.OAK_SIGN);
builder.setDisplayName("§6Select your language:");
for (Locale value : locales) {
if (locale != value) {
builder.addLoreLines("§7> " + value.getName());
} else {
builder.addLoreLines("§6§l> §f" + value.getName());
}
}
builder.addLoreLines("§7§oCycle through the values by", "§7§oleft and right clicking.");
return builder;
}
private ItemProvider[] getItems() {
final NickoBukkit instance = NickoBukkit.getInstance();
final ArrayList<ItemProvider> items = new ArrayList<>();
final ArrayList<Locale> localesToGenerate = new ArrayList<>();
Collections.addAll(localesToGenerate, Locale.values());
if (!instance.getNickoConfig().isCustomLocale()) {
localesToGenerate.remove(Locale.CUSTOM);
}
localesToGenerate.forEach(locale -> items.add(generateItem(locale, localesToGenerate)));
return items.toArray(new ItemProvider[]{});
}
}

View file

@ -0,0 +1,26 @@
package net.artelnatif.nicko.gui.items.skin;
import net.artelnatif.nicko.anvil.AnvilManager;
import org.bukkit.Material;
import org.bukkit.event.inventory.ClickType;
import xyz.xenondevs.invui.item.builder.ItemBuilder;
import xyz.xenondevs.invui.item.impl.SuppliedItem;
public class ChangeName extends SuppliedItem {
public ChangeName() {
super(() -> {
final ItemBuilder builder = new ItemBuilder(Material.NAME_TAG);
builder.setDisplayName("§fChange §6name");
builder.addLoreLines("§7Only change your name.");
return builder;
}, click -> {
final ClickType clickType = click.getClickType();
if (clickType.isLeftClick() || clickType.isRightClick()) {
click.getEvent().getView().close();
final AnvilManager manager = new AnvilManager(click.getPlayer());
manager.openNameAnvil();
}
return true;
});
}
}

View file

@ -0,0 +1,26 @@
package net.artelnatif.nicko.gui.items.skin;
import net.artelnatif.nicko.anvil.AnvilManager;
import org.bukkit.Material;
import org.bukkit.event.inventory.ClickType;
import xyz.xenondevs.invui.item.builder.ItemBuilder;
import xyz.xenondevs.invui.item.impl.SuppliedItem;
public class ChangeNameAndSkin extends SuppliedItem {
public ChangeNameAndSkin() {
super(() -> {
final ItemBuilder builder = new ItemBuilder(Material.END_PORTAL_FRAME);
builder.setDisplayName("§6Skin §fand §6name §fchange");
builder.addLoreLines("§7Change both your skin and name.");
return builder;
}, click -> {
final ClickType clickType = click.getClickType();
if (clickType.isLeftClick() || clickType.isRightClick()) {
click.getEvent().getView().close();
final AnvilManager manager = new AnvilManager(click.getPlayer());
manager.openNameThenSkinAnvil();
}
return true;
});
}
}

View file

@ -0,0 +1,26 @@
package net.artelnatif.nicko.gui.items.skin;
import net.artelnatif.nicko.anvil.AnvilManager;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.ClickType;
import xyz.xenondevs.invui.item.builder.SkullBuilder;
import xyz.xenondevs.invui.item.impl.SuppliedItem;
public class ChangeSkin extends SuppliedItem {
public ChangeSkin(Player player) {
super(() -> {
final SkullBuilder builder = new SkullBuilder(player.getName());
builder.setDisplayName("§fChange §6skin");
builder.addLoreLines("§7Only change your skin.");
return builder;
}, click -> {
final ClickType clickType = click.getClickType();
if (clickType.isLeftClick() || clickType.isRightClick()) {
click.getEvent().getView().close();
final AnvilManager manager = new AnvilManager(click.getPlayer());
manager.openSkinAnvil();
}
return true;
});
}
}

View file

@ -0,0 +1,62 @@
package net.artelnatif.nicko.i18n;
import com.github.jsixface.YamlConfig;
import net.artelnatif.nicko.NickoBukkit;
import net.artelnatif.nicko.disguise.NickoProfile;
import org.bukkit.entity.Player;
import java.io.InputStream;
import java.text.MessageFormat;
import java.util.Optional;
public class I18N {
private final static MessageFormat formatter = new MessageFormat("");
private static Locale getLocale(Player player) {
final NickoBukkit instance = NickoBukkit.getInstance();
try {
final Optional<NickoProfile> profile = instance.getDataStore().getData(player.getUniqueId());
return !profile.isPresent() ? Locale.FALLBACK_LOCALE : profile.get().getLocale();
} catch (IllegalArgumentException exception) {
instance.getLogger().severe("Invalid locale provided by " + player.getName() + ", defaulting to " + Locale.FALLBACK_LOCALE.getCode() + ".");
return Locale.FALLBACK_LOCALE;
}
}
public static String translate(Player player, I18NDict key, Object... arguments) {
final NickoBukkit instance = NickoBukkit.getInstance();
final String translation = findTranslation(player, key);
try {
formatter.applyPattern(translation);
return instance.getNickoConfig().getPrefix() + formatter.format(arguments);
} catch (Exception e) {
return instance.getNickoConfig().getPrefix() + key.key();
}
}
public static String translateWithoutPrefix(Player player, I18NDict key, Object... arguments) {
final String translation = findTranslation(player, key);
try {
formatter.applyPattern(translation);
return formatter.format(arguments);
} catch (Exception e) {
return key.key();
}
}
private static String findTranslation(Player player, I18NDict key) {
final NickoBukkit instance = NickoBukkit.getInstance();
final Locale locale = getLocale(player);
String translation;
if (locale == Locale.CUSTOM) {
translation = instance.getLocaleFileManager().get(key.key());
} else {
final InputStream resource = instance.getResource(locale.getCode() + ".yml");
final YamlConfig yamlConfig = YamlConfig.load(resource);
translation = yamlConfig.getString(key.key());
}
return translation;
}
}

View file

@ -0,0 +1,44 @@
package net.artelnatif.nicko.i18n;
public class I18NDict {
private final String key;
public I18NDict(String key) { this.key = key; }
public static class Event {
public static class Admin {
public static final I18NDict CACHE_CLEAN = new I18NDict("event.admin.cache_clear");
}
public static class Disguise {
public static final I18NDict SUCCESS = new I18NDict("event.disguise.success");
public static final I18NDict FAIL = new I18NDict("event.disguise.fail");
}
public static class Undisguise {
public static final I18NDict SUCCESS = new I18NDict("event.undisguise.success");
public static final I18NDict FAIL = new I18NDict("event.undisguise.fail");
public static final I18NDict NONE = new I18NDict("event.undisguise.none");
}
public static class PreviousSkin {
public static final I18NDict SUCCESS = new I18NDict("event.previous_skin_applied.success");
public static final I18NDict FAIL = new I18NDict("event.previous_skin_applied.fail");
}
}
public static class Error {
public static final I18NDict PLAYER_OFFLINE = new I18NDict("error.player_offline");
public static final I18NDict SKIN_FAIL_MOJANG = new I18NDict("error.couldnt_get_skin_from_mojang");
public static final I18NDict SKIN_FAIL_CACHE = new I18NDict("error.couldnt_get_skin_from_cache");
public static final I18NDict NAME_FAIL_MOJANG = new I18NDict("error.couldnt_get_name_from_mojang");
public static final I18NDict INVALID_USERNAME = new I18NDict("error.invalid_username");
public static final I18NDict UNEXPECTED_ERROR = new I18NDict("error.generic");
public static final I18NDict SQL_ERROR = new I18NDict("error.sql");
public static final I18NDict JSON_ERROR = new I18NDict("error.json");
}
public String key() {
return key;
}
}

View file

@ -0,0 +1,34 @@
package net.artelnatif.nicko.i18n;
import java.io.Serializable;
public enum Locale implements Serializable {
ENGLISH("en", "English"),
FRENCH("fr", "Français"),
CUSTOM("cm", "Server Custom");
public static final Locale FALLBACK_LOCALE = ENGLISH;
private final String code;
private transient final String name;
Locale(String code, String name) {
this.code = code;
this.name = name;
}
public static Locale fromCode(String code) {
for (Locale value : values()) {
if (code.equals(value.code)) return value;
}
return ENGLISH;
}
public String getCode() {
return code;
}
public String getName() {
return name;
}
}

View file

@ -0,0 +1,42 @@
package net.artelnatif.nicko.i18n;
import com.github.jsixface.YamlConfig;
import net.artelnatif.nicko.NickoBukkit;
import xyz.xenondevs.invui.util.IOUtils;
import java.io.*;
import java.nio.file.Files;
public class LocaleFileManager {
private final File folder = new File(NickoBukkit.getInstance().getDataFolder() + "/lang/");
private final File file = new File(folder, "lang.yml");
public String get(String key) {
if (!file.exists()) return key;
try (BufferedInputStream inputStream = new BufferedInputStream(Files.newInputStream(file.toPath()))) {
final YamlConfig yamlConfig = YamlConfig.load(inputStream);
return yamlConfig.getString(key);
} catch (IOException e) {
return key;
}
}
public boolean dumpFromLocale(Locale locale) {
if (locale == Locale.CUSTOM) return true;
if (file.exists()) return true;
final InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(locale.getCode() + ".yml");
try {
if (folder.mkdirs()) {
if (file.createNewFile()) {
try (FileOutputStream outputStream = new FileOutputStream(file)) {
IOUtils.copy(inputStream, outputStream, 8192);
}
}
}
return true;
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
}

View file

@ -0,0 +1,116 @@
package net.artelnatif.nicko.mojang;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonParser;
import javax.annotation.Nonnull;
import javax.net.ssl.HttpsURLConnection;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
public class MojangAPI {
public static final String URL_NAME = "https://api.mojang.com/users/profiles/minecraft/{name}";
public static final String URL_SKIN = "https://sessionserver.mojang.com/session/minecraft/profile/{uuid}?unsigned=false";
private final Logger logger = Logger.getLogger("MojangAPI");
private final CacheLoader<String, Optional<MojangSkin>> loader = new CacheLoader<>() {
@Nonnull
public Optional<MojangSkin> load(@Nonnull String uuid) throws Exception {
return getSkinFromMojang(uuid);
}
};
private final LoadingCache<String, Optional<MojangSkin>> cache = CacheBuilder
.newBuilder()
.recordStats()
.expireAfterWrite(24, TimeUnit.HOURS)
.build(loader);
public Optional<MojangSkin> getSkin(String uuid) throws IOException, ExecutionException {
return cache.get(uuid);
}
public Optional<MojangSkin> getSkinWithoutCaching(String uuid) throws IOException {
return getSkinFromMojang(uuid);
}
public Optional<String> getUUID(String name) throws IOException {
final String parametrizedUrl = URL_NAME.replace("{name}", name);
final JsonObject object = getRequestToUrl(parametrizedUrl);
if (hasNoError(object)) {
return Optional.of(object.get("id").getAsString());
}
return Optional.empty();
}
private Optional<MojangSkin> getSkinFromMojang(String uuid) throws IOException {
final String parametrizedUrl = URL_SKIN.replace("{uuid}", uuid);
final JsonObject object = getRequestToUrl(parametrizedUrl);
if (hasNoError(object)) {
final MojangSkin skin = MojangSkin.buildFromJson(object);
return Optional.of(skin);
}
return Optional.empty();
}
private JsonObject getRequestToUrl(String parametrizedUrl) throws IOException {
final URL url = new URL(parametrizedUrl);
final HttpsURLConnection con = (HttpsURLConnection) url.openConnection();
con.setDoInput(true);
con.setRequestMethod("GET");
switch (con.getResponseCode()) {
case 400:
logger.warning("Failed to parse request: Invalid Name");
return getErrorObject();
case 429:
logger.warning("Failed to parse request: The connection is throttled.");
return getErrorObject();
case 200:
final BufferedReader input = new BufferedReader(new InputStreamReader(con.getInputStream()));
final StringBuilder builder = new StringBuilder();
String line;
while ((line = input.readLine()) != null) {
builder.append(line);
}
try {
final JsonElement jsonElt = JsonParser.parseString(builder.toString());
return jsonElt.getAsJsonObject();
} catch (JsonParseException | IllegalStateException exception) {
logger.warning("Failed to parse request (" + parametrizedUrl + ")!");
return getErrorObject();
}
default:
logger.warning("Unhandled response code from Mojang: " + con.getResponseCode());
return getErrorObject();
}
}
private JsonObject getErrorObject() {
final JsonObject errorObject = new JsonObject();
errorObject.addProperty("error", "An error occurred.");
return errorObject;
}
private boolean hasNoError(JsonObject object) {
return object.get("error") == null;
}
public LoadingCache<String, Optional<MojangSkin>> getCache() {
return cache;
}
}

View file

@ -0,0 +1,28 @@
package net.artelnatif.nicko.mojang;
import com.google.gson.JsonObject;
public class MojangSkin {
private final String value;
private final String signature;
public MojangSkin(String value, String signature) {
this.value = value;
this.signature = signature;
}
public static MojangSkin buildFromJson(JsonObject object) {
final JsonObject properties = object.get("properties").getAsJsonArray().get(0).getAsJsonObject();
final String value = properties.get("value").getAsString();
final String signature = properties.get("signature").getAsString();
return new MojangSkin(value, signature);
}
public String getValue() {
return value;
}
public String getSignature() {
return signature;
}
}

View file

@ -0,0 +1,26 @@
package net.artelnatif.nicko.mojang;
import java.util.UUID;
import java.util.regex.Pattern;
public class MojangUtils {
public static boolean isUsernameInvalid(String username) {
return !Pattern.matches("^\\w{3,16}$", username);
}
public static UUID fromTrimmed(String trimmedUUID) throws IllegalArgumentException {
if (trimmedUUID == null) throw new IllegalArgumentException();
StringBuilder builder = new StringBuilder(trimmedUUID.trim());
/* Backwards adding to avoid index adjustments */
try {
builder.insert(20, "-");
builder.insert(16, "-");
builder.insert(12, "-");
builder.insert(8, "-");
} catch (StringIndexOutOfBoundsException e) {
throw new IllegalArgumentException();
}
return UUID.fromString(builder.toString());
}
}

View file

@ -0,0 +1,70 @@
package net.artelnatif.nicko.placeholder;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
import net.artelnatif.nicko.NickoBukkit;
import net.artelnatif.nicko.disguise.NickoProfile;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Optional;
public class NickoExpansion extends PlaceholderExpansion {
private final NickoBukkit instance;
public NickoExpansion(NickoBukkit instance) {
this.instance = instance;
}
@Override
public @NotNull String getIdentifier() {
return "nicko";
}
@Override
public @NotNull String getAuthor() {
return "Aro";
}
@Override
public @NotNull String getVersion() {
return "1.0.0";
}
@Override
public boolean persist() {
return true;
}
@Override
public @Nullable String onPlaceholderRequest(Player player, @NotNull String params) {
if (player == null) return null;
String name, skin, locale;
boolean bungeecord;
name = skin = player.getName();
locale = "N/A";
bungeecord = true;
final Optional<NickoProfile> optionalProfile = instance.getDataStore().getData(player.getUniqueId());
if (optionalProfile.isPresent()) {
final NickoProfile profile = optionalProfile.get();
if (!profile.isEmpty()) {
name = profile.getName();
skin = profile.getSkin();
}
locale = profile.getLocale().getName();
bungeecord = profile.isBungeecordTransfer();
}
switch (params) {
case "name": return name;
case "skin": return skin;
case "locale": return locale;
case "bungeecord": return String.valueOf(bungeecord);
default: return null;
}
}
}

View file

@ -0,0 +1,19 @@
package net.artelnatif.nicko.placeholder;
import net.artelnatif.nicko.NickoBukkit;
import org.bukkit.Bukkit;
public class PlaceHolderHook {
private final NickoBukkit instance;
public PlaceHolderHook(NickoBukkit instance) {
this.instance = instance;
}
public void hook() {
if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null) {
instance.getLogger().info("Enabling PlaceHolderAPI support...");
new NickoExpansion(instance).register();
}
}
}

View file

@ -0,0 +1,92 @@
package net.artelnatif.nicko.storage;
import net.artelnatif.nicko.config.Configuration;
import net.artelnatif.nicko.disguise.ActionResult;
import net.artelnatif.nicko.disguise.NickoProfile;
import net.artelnatif.nicko.i18n.I18NDict;
import net.artelnatif.nicko.mojang.MojangAPI;
import net.artelnatif.nicko.mojang.MojangUtils;
import net.artelnatif.nicko.storage.cache.Cache;
import net.artelnatif.nicko.storage.cache.redis.RedisCache;
import net.artelnatif.nicko.storage.json.JSONStorage;
import net.artelnatif.nicko.storage.sql.SQLStorage;
import org.bukkit.entity.Player;
import java.io.IOException;
import java.util.HashMap;
import java.util.Optional;
import java.util.UUID;
public class PlayerDataStore {
private final Storage storage;
private final Cache cache;
private final MojangAPI mojangAPI;
private final HashMap<UUID, NickoProfile> profiles = new HashMap<>();
public PlayerDataStore(MojangAPI mojangAPI, Configuration configuration) {
this.mojangAPI = mojangAPI;
this.storage = configuration.isLocal() ? new JSONStorage() : new SQLStorage(configuration);
this.cache = new RedisCache(); // The only option for now!
}
public void performProfileUpdate(UUID uuid, NickoProfile profile) {
if (!profiles.containsKey(uuid)) {
profiles.put(uuid, profile);
return;
}
profiles.replace(uuid, profile);
}
public Optional<NickoProfile> getData(UUID uuid) {
if (storage.isError()) {
return Optional.empty();
}
if (profiles.containsKey(uuid)) {
return Optional.of(profiles.get(uuid));
} else if (storage.isStored(uuid)) {
final Optional<NickoProfile> retrievedProfile = storage.retrieve(uuid);
retrievedProfile.ifPresent(profile -> profiles.put(uuid, profile));
return retrievedProfile;
} else {
final NickoProfile newProfile = NickoProfile.EMPTY_PROFILE.clone();
profiles.put(uuid, newProfile);
return Optional.of(newProfile);
}
}
public Optional<NickoProfile> getOfflineData(String name) {
if (storage.isError()) {
return Optional.empty();
}
try {
final Optional<String> uuidTrimmed = mojangAPI.getUUID(name);
if (uuidTrimmed.isPresent()) {
final UUID uuid = MojangUtils.fromTrimmed(uuidTrimmed.get());
return getData(uuid);
}
return Optional.empty();
} catch (IOException e) {
return Optional.empty();
}
}
public ActionResult<Void> saveData(Player player) {
if (storage.isError()) { return new ActionResult<>(I18NDict.Error.UNEXPECTED_ERROR); }
if (!profiles.containsKey(player.getUniqueId())) { return new ActionResult<>(I18NDict.Error.UNEXPECTED_ERROR); }
final ActionResult<Void> store = storage.store(player.getUniqueId(), profiles.get(player.getUniqueId()));
profiles.remove(player.getUniqueId());
return store;
}
public Storage getStorage() {
return storage;
}
public Cache getCache() {
return cache;
}
}

View file

@ -0,0 +1,27 @@
package net.artelnatif.nicko.storage;
import net.artelnatif.nicko.disguise.ActionResult;
import net.artelnatif.nicko.disguise.NickoProfile;
import java.util.Optional;
import java.util.UUID;
public abstract class Storage {
private boolean error = false;
public abstract StorageProvider getProvider();
public abstract ActionResult<Void> store(UUID uuid, NickoProfile profile);
public abstract boolean isStored(UUID uuid);
public abstract Optional<NickoProfile> retrieve(UUID uuid);
public boolean isError() {
return error;
}
public void setError(boolean error) {
this.error = error;
}
}

View file

@ -0,0 +1,7 @@
package net.artelnatif.nicko.storage;
public interface StorageProvider {
boolean init();
boolean close();
}

View file

@ -0,0 +1,27 @@
package net.artelnatif.nicko.storage.cache;
import net.artelnatif.nicko.disguise.ActionResult;
import net.artelnatif.nicko.disguise.NickoProfile;
import java.util.Optional;
import java.util.UUID;
public abstract class Cache {
private boolean error = false;
public abstract CacheProvider getProvider();
public abstract ActionResult<Void> cache(UUID uuid, NickoProfile profile);
public abstract boolean isCached(UUID uuid);
public abstract Optional<NickoProfile> retrieve(UUID uuid);
public boolean isError() {
return error;
}
public void setError(boolean error) {
this.error = error;
}
}

View file

@ -0,0 +1,7 @@
package net.artelnatif.nicko.storage.cache;
public interface CacheProvider {
boolean init();
boolean close();
}

View file

@ -0,0 +1,31 @@
package net.artelnatif.nicko.storage.cache.redis;
import net.artelnatif.nicko.disguise.ActionResult;
import net.artelnatif.nicko.disguise.NickoProfile;
import net.artelnatif.nicko.storage.cache.Cache;
import net.artelnatif.nicko.storage.cache.CacheProvider;
import java.util.Optional;
import java.util.UUID;
public class RedisCache extends Cache {
@Override
public CacheProvider getProvider() {
return null;
}
@Override
public ActionResult<Void> cache(UUID uuid, NickoProfile profile) {
return null;
}
@Override
public boolean isCached(UUID uuid) {
return false;
}
@Override
public Optional<NickoProfile> retrieve(UUID uuid) {
return Optional.empty();
}
}

View file

@ -0,0 +1,26 @@
package net.artelnatif.nicko.storage.cache.redis;
import net.artelnatif.nicko.storage.cache.CacheProvider;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
public class RedisCacheProvider implements CacheProvider {
private JedisPool pool;
@Override
public boolean init() {
// TODO: 3/12/23 Get port from configuration
pool = new JedisPool("localhost", 6379);
return !pool.isClosed() && pool.getResource() != null;
}
@Override
public boolean close() {
pool.close();
return pool.isClosed();
}
public Jedis getJedis() {
return pool.getResource();
}
}

View file

@ -0,0 +1,83 @@
package net.artelnatif.nicko.storage.json;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import net.artelnatif.nicko.NickoBukkit;
import net.artelnatif.nicko.disguise.ActionResult;
import net.artelnatif.nicko.disguise.NickoProfile;
import net.artelnatif.nicko.i18n.I18NDict;
import net.artelnatif.nicko.storage.Storage;
import net.artelnatif.nicko.storage.StorageProvider;
import java.io.*;
import java.util.Optional;
import java.util.UUID;
import java.util.logging.Logger;
public class JSONStorage extends Storage {
private final Logger logger = Logger.getLogger("JSONStorage");
private final Gson gson = new GsonBuilder().serializeNulls().setPrettyPrinting().create();
private final File directory = new File(NickoBukkit.getInstance().getDataFolder() + "/players/");
private JSONStorageProvider provider;
@Override
public StorageProvider getProvider() {
if (provider == null) {
provider = new JSONStorageProvider(directory);
}
return provider;
}
@Override
public ActionResult<Void> store(UUID uuid, NickoProfile profile) {
final String profileToJson = gson.toJson(profile);
final File file = new File(directory, uuid.toString() + ".json");
try {
if (checkFileExists(file)) {
try (FileWriter fileWriter = new FileWriter(file)) {
try (BufferedWriter writer = new BufferedWriter(fileWriter)) {
writer.write(profileToJson);
}
} catch (IOException e) {
logger.warning("Could not write to file.");
return new ActionResult<>(I18NDict.Error.JSON_ERROR);
}
}
} catch (IOException e) {
logger.warning("Could not create file.");
return new ActionResult<>(I18NDict.Error.JSON_ERROR);
}
return new ActionResult<>();
}
@Override
public boolean isStored(UUID uuid) {
final File directory = new File(NickoBukkit.getInstance().getDataFolder() + "/players/");
final File file = new File(directory, uuid.toString() + ".json");
return file.exists();
}
@Override
public Optional<NickoProfile> retrieve(UUID uuid) {
final File directory = new File(NickoBukkit.getInstance().getDataFolder() + "/players/");
final File file = new File(directory, uuid.toString() + ".json");
try (FileReader fileReader = new FileReader(file)) {
try (BufferedReader reader = new BufferedReader(fileReader)) {
final NickoProfile value = gson.fromJson(reader, NickoProfile.class);
return Optional.of(value);
}
} catch (IOException e) {
return Optional.empty();
}
}
private boolean checkFileExists(File file) throws IOException {
if (!file.exists()) {
return file.createNewFile();
}
return true;
}
}

View file

@ -0,0 +1,22 @@
package net.artelnatif.nicko.storage.json;
import net.artelnatif.nicko.storage.StorageProvider;
import java.io.File;
public class JSONStorageProvider implements StorageProvider {
private final File directory;
public JSONStorageProvider(File directory) {
this.directory = directory;
}
@Override
public boolean init()
{
return directory.exists() || directory.mkdirs();
}
@Override
public boolean close() { return true; }
}

View file

@ -0,0 +1,28 @@
package net.artelnatif.nicko.storage.name;
import org.bukkit.entity.Player;
import java.util.HashMap;
import java.util.UUID;
public class PlayerNameStore {
private final HashMap<UUID, String> names = new HashMap<>();
public void storeName(Player player) {
if (!isNameStored(player)) {
names.put(player.getUniqueId(), player.getName());
}
}
public String getStoredName(Player player) {
return names.get(player.getUniqueId());
}
private boolean isNameStored(Player player) {
return names.containsKey(player.getUniqueId());
}
public void clearStoredNames() {
names.clear();
}
}

View file

@ -0,0 +1,133 @@
package net.artelnatif.nicko.storage.sql;
import net.artelnatif.nicko.config.Configuration;
import net.artelnatif.nicko.disguise.ActionResult;
import net.artelnatif.nicko.disguise.NickoProfile;
import net.artelnatif.nicko.i18n.I18NDict;
import net.artelnatif.nicko.i18n.Locale;
import net.artelnatif.nicko.storage.Storage;
import java.io.ByteArrayInputStream;
import java.nio.ByteBuffer;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Optional;
import java.util.UUID;
import java.util.logging.Logger;
public class SQLStorage extends Storage {
private final Logger logger = Logger.getLogger("SQLStorage");
private final Configuration configuration;
private SQLStorageProvider provider;
public SQLStorage(Configuration configuration) {
this.configuration = configuration;
}
@Override
public SQLStorageProvider getProvider() {
if (provider == null) {
provider = new SQLStorageProvider(configuration);
}
return provider;
}
@Override
public ActionResult<Void> store(UUID uuid, NickoProfile profile) {
final Connection connection = getProvider().getConnection();
if (connection == null) return new ActionResult<>(I18NDict.Error.SQL_ERROR);
try {
final PreparedStatement statement = isStored(uuid) ?
getUpdateStatement(connection, uuid, profile) : getInsertStatement(connection, uuid, profile);
statement.executeUpdate();
return new ActionResult<>();
} catch (SQLException e) {
logger.warning("Couldn't send SQL Request: " + e.getMessage());
return new ActionResult<>(I18NDict.Error.SQL_ERROR);
}
}
@Override
public boolean isStored(UUID uuid) {
final Connection connection = getProvider().getConnection();
if (connection == null) return false;
try {
final String sql = "SELECT * FROM nicko.DATA WHERE uuid = ?";
final PreparedStatement statement = connection.prepareStatement(sql);
statement.setBinaryStream(1, uuidToBin(uuid));
final ResultSet resultSet = statement.executeQuery();
return resultSet.next();
} catch (SQLException e) {
logger.warning("Couldn't check if data is present: " + e.getMessage());
return false;
}
}
@Override
public Optional<NickoProfile> retrieve(UUID uuid) {
final Connection connection = getProvider().getConnection();
if (connection == null) return Optional.empty();
try {
final String sql = "SELECT * FROM nicko.DATA WHERE uuid = ?";
final PreparedStatement statement = connection.prepareStatement(sql);
statement.setBinaryStream(1, uuidToBin(uuid));
final ResultSet resultSet = statement.executeQuery();
String name = "";
String skin = "";
String locale = "";
boolean bungeecord = false;
while (resultSet.next()) {
name = resultSet.getString("name");
skin = resultSet.getString("skin");
locale = resultSet.getString("locale");
bungeecord = resultSet.getBoolean("bungeecord");
}
final NickoProfile profile = new NickoProfile(name, skin, Locale.fromCode(locale), bungeecord);
return Optional.of(profile);
} catch (SQLException e) {
logger.warning("Couldn't fetch profile: " + e.getMessage());
return Optional.empty();
}
}
private PreparedStatement getInsertStatement(Connection connection, UUID uuid, NickoProfile profile) throws SQLException {
final String sql = "INSERT IGNORE INTO nicko.DATA (`uuid`, `name`, `skin`, `locale`, `bungeecord`) VALUES (?, ?, ?, ?, ?)";
final PreparedStatement statement = connection.prepareStatement(sql);
statement.setBinaryStream(1, uuidToBin(uuid));
statement.setString(2, profile.getName() == null ? null : profile.getName());
statement.setString(3, profile.getSkin() == null ? null : profile.getSkin());
statement.setString(4, profile.getLocale().getCode());
statement.setBoolean(5, profile.isBungeecordTransfer());
return statement;
}
private PreparedStatement getUpdateStatement(Connection connection, UUID uuid, NickoProfile profile) throws SQLException {
final String sql = "UPDATE nicko.DATA SET name = ?, skin = ?, locale = ?, bungeecord = ? WHERE uuid = ?";
final PreparedStatement statement = connection.prepareStatement(sql);
statement.setString(1, profile.getName() == null ? null : profile.getName());
statement.setString(2, profile.getSkin() == null ? null : profile.getSkin());
statement.setString(3, profile.getLocale().getCode());
statement.setBoolean(4, profile.isBungeecordTransfer());
statement.setBinaryStream(5, uuidToBin(uuid));
return statement;
}
private ByteArrayInputStream uuidToBin(UUID uuid) {
byte[] bytes = new byte[16];
ByteBuffer.wrap(bytes)
.putLong(uuid.getMostSignificantBits())
.putLong(uuid.getLeastSignificantBits());
return new ByteArrayInputStream(bytes);
}
}

View file

@ -0,0 +1,89 @@
package net.artelnatif.nicko.storage.sql;
import net.artelnatif.nicko.config.Configuration;
import net.artelnatif.nicko.config.DataSourceConfiguration;
import net.artelnatif.nicko.storage.StorageProvider;
import org.mariadb.jdbc.MariaDbDataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.logging.Logger;
public class SQLStorageProvider implements StorageProvider {
private final Logger logger = Logger.getLogger("SQLStorageProvider");
private final Configuration configuration;
private Connection connection;
private final String schemaName = "nicko";
public SQLStorageProvider(Configuration configuration) {
this.configuration = configuration;
}
@Override
public boolean init() {
try {
final MariaDbDataSource dataSource = new MariaDbDataSource();
final DataSourceConfiguration dataSourceConfiguration = configuration.getSqlConfiguration();
dataSource.setUrl("jdbc:mariadb://" + dataSourceConfiguration.getAddress() + ":" + dataSourceConfiguration.getPort());
dataSource.setUser(dataSourceConfiguration.getUsername());
dataSource.setPassword(dataSourceConfiguration.getPassword());
connection = dataSource.getConnection();
final boolean initialized = connection != null && !connection.isClosed();
if (!initialized) return false;
createDatabase();
createTable();
return true;
} catch (SQLException e) {
logger.severe("Couldn't establish a connection to the MySQL database: " + e.getMessage());
return false;
}
}
@Override
public boolean close() {
if (connection == null) { return true; }
try {
connection.close();
return connection.isClosed();
} catch (SQLException e) {
return false;
}
}
private void createTable() throws SQLException {
final Connection connection = getConnection();
String query = "CREATE TABLE IF NOT EXISTS %s.DATA " +
"(uuid binary(16) NOT NULL," +
"name varchar(16)," +
"skin varchar(16)," +
"locale char(2) NOT NULL," +
"bungeecord boolean NOT NULL," +
"PRIMARY KEY (UUID))";
query = query.replace("%s", schemaName);
final PreparedStatement statement = connection.prepareStatement(query);
statement.executeUpdate();
statement.close();
}
private void createDatabase() throws SQLException {
final Connection connection = getConnection();
String query = "CREATE DATABASE IF NOT EXISTS %s";
query = query.replace("%s", schemaName);
final PreparedStatement statement = connection.prepareStatement(query);
statement.executeUpdate();
statement.close();
}
public Connection getConnection() {
return connection;
}
}

View file

@ -0,0 +1,59 @@
# Nicko ${project.version} - Config:
###########
# STORAGE #
###########
# Indicates wherever the data will be stored
# locally through a .json file or a (My)SQL database.
# Accepted values: false (Disabled), true (Enabled)
local: true
# This configuration section manages SQL.
sql:
# SQL database's address
# Accepted values: valid IP address (e.g. localhost, 127.0.0.1)
address: "localhost"
# SQL database's port
# Accepted values: valid integer (e.g. 3306, 25565)
port: 3306
# SQL database's username.
# Accepted values: any string
username: "username"
# SQL database's password.
# Accepted values: any string
password: "password"
# This configuration section manages Redis (BungeeCord support).
# It is used to transfer data between multiple servers.
redis:
# Indicates wherever the data will be stored through
# Redis to transfer whenever a player switches server.
# Accepted values: false (Disabled), true (Enabled)
enabled: false
# Redis server's address
# Accepted values: valid IP address (e.g. localhost, 127.0.0.1)
address: "localhost"
# Redis server's port
# Accepted values: valid integer (e.g. 3306, 25565)
port: 6379
# Redis server's username
# Accepted values: any string
username: "username"
# Redis server's password
# Accepted values: any string
password: "password"
###########
# DISPLAY #
###########
# Nicko's messages prefix.
# Accepted values: any string
prefix: "§6Nicko §8§l| §r"
# Nicko will copy the English locale as "lang.yml"
# and will use the translations in that file when "Server Custom"
# is selected as the player's locale.
# Accepted values: false (Disabled), true (Enabled)
customLocale: false

22
src/main/resources/en.yml Normal file
View file

@ -0,0 +1,22 @@
error:
couldnt_get_name_from_mojang: "Failed to get username from Mojang"
couldnt_get_skin_from_cache: "Failed to get skin from cache"
couldnt_get_skin_from_mojang: "Failed to get skin from Mojang"
generic: "Unknown error"
invalid_username: "§cThe specified username is not a valid Minecraft username."
player_offline: "§c{0} §fis offline, please try again."
sql: "SQL Error"
json: "JSON Error"
event:
admin:
cache_clear: "§aSkin cache cleaned."
disguise:
fail: "§cUnable to apply your disguise. §7§o({0})"
success: "§aDisguise applied!"
previous_skin_applied:
fail: "§cFailed to apply your previous disguise back. §7§o({0})"
success: "§aYour previous active disguise has been applied back."
undisguise:
fail: "§cUnable to remove your disguise. It will be set back to default on your next login. Sorry!"
none: "§cYou do not have an active disguise."
success: "§aDisguise removed."

22
src/main/resources/fr.yml Normal file
View file

@ -0,0 +1,22 @@
error:
couldnt_get_name_from_mojang: "Impossible de récupérer le nom d''utilisateur depuis Mojang"
couldnt_get_skin_from_cache: "Impossible de récupérer le skin depuis le cache"
couldnt_get_skin_from_mojang: "Impossible de récupérer le skin depuis Mojang"
generic: "Erreur inconnue"
invalid_username: "§cLe pseudo spécifié n''est pas un pseudo Minecraft valide."
player_offline: "§c{0} §fest hors-ligne, veuillez réessayer."
sql: "Erreur SQL"
json: "Erreur JSON"
event:
admin:
cache_clear: "§aCache des skins nettoyé."
disguise:
fail: "§cImpossible d''appliquer votre déguisement. §7§o({0})"
success: "§aDéguisement appliqué !"
previous_skin_applied:
fail: "§cImpossible d''appliquer votre déguisement précédent. §7§o({0})"
success: "§aVotre précédent déguisement a été réappliqué."
undisguise:
fail: "§cImpossible de retier votre déguisement. Il sera remis par défaut à votre prochaine reconnexion. Désolé !"
none: "§cVous n''avez pas de déguisement."
success: "§aDéguisement retiré."

View file

@ -0,0 +1,18 @@
name: Nicko
main: net.artelnatif.nicko.NickoBukkit
version: 1.0-SNAPSHOT
author: Ineanto
api-version: 1.13
softdepend: [ PlaceholderAPI ]
depend: [ ProtocolLib ]
commands:
nicko:
description: "Opens Nicko's GUI."
permission: nicko.admin
permissions:
nicko.*:
default: op
children:
- nicko.use
nicko.use:
default: op