feat: reworked project structure, 1.20 to 1.21.1 mappings and more

This commit is contained in:
ineanto 2024-09-22 16:39:13 +02:00
parent b7be5ad54e
commit fd9676f6f5
Signed by: ineanto
GPG key ID: E511F9CAA2F9CE84
115 changed files with 1399 additions and 248 deletions

View file

@ -0,0 +1,208 @@
package xyz.ineanto.nicko;
import com.comphenix.protocol.utility.MinecraftVersion;
import org.bstats.bukkit.Metrics;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.command.PluginCommand;
import org.bukkit.plugin.java.JavaPlugin;
import xyz.ineanto.nicko.appearance.random.RandomNameFetcher;
import xyz.ineanto.nicko.command.NickoCommand;
import xyz.ineanto.nicko.config.Configuration;
import xyz.ineanto.nicko.config.ConfigurationManager;
import xyz.ineanto.nicko.event.PlayerJoinListener;
import xyz.ineanto.nicko.event.PlayerQuitListener;
import xyz.ineanto.nicko.language.CustomLanguage;
import xyz.ineanto.nicko.language.Language;
import xyz.ineanto.nicko.mapping.MappingManager;
import xyz.ineanto.nicko.migration.ConfigurationMigrator;
import xyz.ineanto.nicko.migration.CustomLocaleMigrator;
import xyz.ineanto.nicko.mojang.MojangAPI;
import xyz.ineanto.nicko.placeholder.NickoExpansion;
import xyz.ineanto.nicko.storage.PlayerDataStore;
import xyz.ineanto.nicko.storage.json.JSONStorage;
import xyz.ineanto.nicko.storage.map.MapCache;
import xyz.ineanto.nicko.storage.name.PlayerNameStore;
import xyz.xenondevs.invui.gui.structure.Structure;
import xyz.xenondevs.invui.item.builder.ItemBuilder;
import xyz.xenondevs.invui.item.impl.SimpleItem;
import java.io.IOException;
public class Nicko extends JavaPlugin {
private static Nicko plugin;
private final boolean unitTesting;
private MojangAPI mojangAPI;
private PlayerDataStore dataStore;
private ConfigurationManager configurationManager;
private Configuration configuration;
private CustomLanguage customLanguage;
private PlayerNameStore nameStore;
private RandomNameFetcher nameFetcher;
private MappingManager mappingManager;
private Metrics metrics;
public Nicko() {
this.unitTesting = false;
}
/**
* Used by MockBukkit
*/
protected Nicko(Configuration configuration) {
this.unitTesting = true;
this.configuration = configuration;
getLogger().info("Unit Testing Mode enabled.");
}
@Override
public void onEnable() {
plugin = this;
configurationManager = new ConfigurationManager(getDataFolder());
configurationManager.saveDefaultConfig();
dataStore = new PlayerDataStore(mojangAPI, getNickoConfig());
mappingManager = new MappingManager();
if (!MinecraftVersion.TRAILS_AND_TAILS.atOrAbove() || mappingManager.getMappingForServer().isEmpty()) {
getLogger().severe("This version (" + MinecraftVersion.getCurrentVersion().getVersion() + ") is not supported by Nicko!");
getLogger().severe("As of version 1.0.7, Nicko only supports the latest two majors Minecraft versions. (Currently 1.20.X-1.21.X)");
dataStore.getStorage().setError(true);
Bukkit.getPluginManager().disablePlugin(this);
}
if (!Bukkit.getOnlineMode()) {
getLogger().warning("Nicko has not been tested using offline mode!");
getLogger().warning("Issues regarding Nicko being used in offline mode will be ignored for now.");
}
try {
Class.forName("io.papermc.paper.threadedregions.RegionizedServerInitEvent");
getLogger().warning("Nicko has not been tested against Folia and might not work at all!");
getLogger().warning("Issues regarding Nicko on Folia will be ignored for now.");
} catch (ClassNotFoundException ignored) { }
getLogger().info("Loading persistence...");
if (!dataStore.getStorage().getProvider().init()) {
getLogger().severe("Couldn't connect to distant persistence, falling back on local persistence.");
final JSONStorage storage = new JSONStorage();
storage.getProvider().init();
dataStore.setStorage(storage);
}
getLogger().info("Loading cache...");
if (!dataStore.getCache().getProvider().init()) {
getLogger().severe("Couldn't connect to distant cache, falling back on local cache.");
final MapCache cache = new MapCache();
cache.getProvider().init();
dataStore.setCache(cache);
}
if (!unitTesting) {
nameStore = new PlayerNameStore();
mojangAPI = new MojangAPI();
nameFetcher = new RandomNameFetcher(this);
new ConfigurationMigrator(this).migrate();
if (configuration.isCustomLocale()) {
try {
CustomLanguage.dumpIntoFile(Language.ENGLISH);
customLanguage = new CustomLanguage();
new CustomLocaleMigrator(this, customLanguage).migrate();
getLogger().info("Successfully loaded the custom locale.");
} catch (IOException e) {
getLogger().severe("Failed to load the custom locale!");
}
}
final PluginCommand command = getCommand("xyz/ineanto/nicko");
if (command != null) {
command.setExecutor(new NickoCommand());
}
Structure.addGlobalIngredient('#', new SimpleItem(new ItemBuilder(Material.AIR)));
Structure.addGlobalIngredient('%', new SimpleItem(new ItemBuilder(Material.BLACK_STAINED_GLASS_PANE).setDisplayName(" ")));
if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null) {
getLogger().info("Enabling PlaceHolderAPI support...");
new NickoExpansion(this).register();
}
getServer().getPluginManager().registerEvents(new PlayerJoinListener(), this);
getServer().getPluginManager().registerEvents(new PlayerQuitListener(), this);
metrics = new Metrics(this, 20483);
}
getLogger().info("Nicko has been enabled.");
}
@Override
public void onDisable() {
if (!getDataStore().getStorage().isError()) {
Bukkit.getOnlinePlayers().forEach(player -> dataStore.saveData(player));
if (!dataStore.getStorage().getProvider().close()) {
getLogger().severe("Failed to close persistence!");
} else {
getLogger().info("Persistence closed.");
}
}
if (!unitTesting) {
nameStore.clearStoredNames();
metrics.shutdown();
}
getLogger().info("Nicko (Bukkit) has been disabled.");
}
public static Nicko getInstance() {
return plugin;
}
public Configuration getNickoConfig() {
try {
if (configuration == null) {
configuration = configurationManager.load();
getLogger().info("Configuration file loaded.");
}
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 RandomNameFetcher getNameFetcher() {
return nameFetcher;
}
public PlayerDataStore getDataStore() {
return dataStore;
}
public ConfigurationManager getConfigurationManager() {
return configurationManager;
}
public PlayerNameStore getNameStore() {
return nameStore;
}
public MojangAPI getMojangAPI() {
return mojangAPI;
}
public CustomLanguage getCustomLocale() {
return customLanguage;
}
public MappingManager getMappingManager() {
return mappingManager;
}
}

View file

@ -0,0 +1,146 @@
package xyz.ineanto.nicko.anvil;
import net.kyori.adventure.text.Component;
import net.wesjd.anvilgui.AnvilGUI;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import xyz.ineanto.nicko.Nicko;
import xyz.ineanto.nicko.appearance.ActionResult;
import xyz.ineanto.nicko.appearance.AppearanceManager;
import xyz.ineanto.nicko.event.custom.PlayerDisguiseEvent;
import xyz.ineanto.nicko.language.PlayerLanguage;
import xyz.ineanto.nicko.language.LanguageKey;
import xyz.ineanto.nicko.mojang.MojangUtils;
import xyz.ineanto.nicko.profile.NickoProfile;
import xyz.ineanto.nicko.storage.PlayerDataStore;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
public class AnvilManager {
private final Player player;
private final AppearanceManager appearanceManager;
private final PlayerDataStore dataStore = Nicko.getInstance().getDataStore();
private final NickoProfile profile;
private final PlayerLanguage playerLanguage;
public AnvilManager(Player player) {
this.player = player;
this.playerLanguage = new PlayerLanguage(player);
final Optional<NickoProfile> optionalProfile = dataStore.getData(player.getUniqueId());
this.profile = optionalProfile.orElse(NickoProfile.EMPTY_PROFILE.clone());
this.appearanceManager = new AppearanceManager(player);
}
public void openNameThenSkinAnvil() {
getNameThenSkinAnvil().open(player);
}
public void openSkinAnvil() {
getSkinAnvil().open(player);
}
public void openNameAnvil() {
getNameAnvil().open(player);
}
private AnvilGUI.Builder getNameThenSkinAnvil() {
return new AnvilGUI.Builder()
.plugin(Nicko.getInstance())
.itemLeft(getLeftItem(false))
.interactableSlots(AnvilGUI.Slot.OUTPUT)
.onClick((slot, snapshot) -> {
if (slot == AnvilGUI.Slot.OUTPUT) {
if (MojangUtils.isUsernameInvalid(snapshot.getText())) {
return Collections.singletonList(AnvilGUI.ResponseAction.replaceInputText("Invalid username!"));
} else {
profile.setName(snapshot.getText());
openSkinAnvil();
return Collections.singletonList(AnvilGUI.ResponseAction.close());
}
}
return Collections.emptyList();
})
.text("New name...");
}
private AnvilGUI.Builder getNameAnvil() {
return new AnvilGUI.Builder()
.plugin(Nicko.getInstance())
.itemLeft(getLeftItem(false))
.interactableSlots(AnvilGUI.Slot.OUTPUT)
.onClick((slot, snapshot) -> {
if (slot == AnvilGUI.Slot.OUTPUT) {
if (MojangUtils.isUsernameInvalid(snapshot.getText())) {
return Collections.singletonList(AnvilGUI.ResponseAction.replaceInputText("Invalid username!"));
} else {
profile.setName(snapshot.getText());
dataStore.updateCache(player.getUniqueId(), profile);
return sendResultAndClose(false);
}
}
return Collections.emptyList();
})
.text("New name...");
}
private AnvilGUI.Builder getSkinAnvil() {
return new AnvilGUI.Builder()
.plugin(Nicko.getInstance())
.itemLeft(getLeftItem(true))
.interactableSlots(AnvilGUI.Slot.OUTPUT)
.onClick((slot, snapshot) -> {
if (slot == AnvilGUI.Slot.OUTPUT) {
if (MojangUtils.isUsernameInvalid(snapshot.getText())) {
return Collections.singletonList(AnvilGUI.ResponseAction.replaceInputText("Invalid username!"));
} else {
profile.setSkin(snapshot.getText());
dataStore.updateCache(player.getUniqueId(), profile);
return sendResultAndClose(true);
}
}
return Collections.emptyList();
})
.text("New skin...");
}
private List<AnvilGUI.ResponseAction> sendResultAndClose(boolean skinChange) {
final PlayerDisguiseEvent event = new PlayerDisguiseEvent(player, profile.getSkin(), player.getName());
Bukkit.getPluginManager().callEvent(event);
if (event.isCancelled()) { return Collections.singletonList(AnvilGUI.ResponseAction.close()); }
final ActionResult actionResult = appearanceManager.updatePlayer(skinChange, false);
if (!actionResult.isError()) {
player.sendMessage(playerLanguage.translate(LanguageKey.Event.Appearance.Set.OK, true));
} else {
player.sendMessage(
playerLanguage.translate(
LanguageKey.Event.Appearance.Set.ERROR,
true,
playerLanguage.translate(actionResult.getErrorKey(), false)
));
}
return Collections.singletonList(AnvilGUI.ResponseAction.close());
}
private ItemStack getLeftItem(boolean skin) {
final ItemStack item = new ItemStack(Material.PAPER);
final ItemMeta meta = item.getItemMeta();
if (meta != null) {
if (skin) {
meta.displayName(Component.text(playerLanguage.translate(LanguageKey.GUI.NEW_SKIN, false)));
} else {
meta.displayName(Component.text(playerLanguage.translate(LanguageKey.GUI.NEW_NAME, false)));
}
}
item.setItemMeta(meta);
return item;
}
}

View file

@ -0,0 +1,35 @@
package xyz.ineanto.nicko.appearance;
public class ActionResult {
private final String errorKey;
private boolean error = false;
public static ActionResult ok() {
return new ActionResult();
}
public static ActionResult error() {
return new ActionResult(null);
}
public static ActionResult error(String errorMessage) {
return new ActionResult(errorMessage);
}
private ActionResult() {
this.errorKey = null;
}
private ActionResult(String errorMessage) {
this.errorKey = errorMessage;
this.error = true;
}
public boolean isError() {
return error;
}
public String getErrorKey() {
return errorKey;
}
}

View file

@ -0,0 +1,204 @@
package xyz.ineanto.nicko.appearance;
import com.comphenix.protocol.utility.MinecraftVersion;
import com.comphenix.protocol.wrappers.*;
import com.google.common.collect.Multimap;
import it.unimi.dsi.fastutil.ints.IntList;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.event.player.PlayerTeleportEvent;
import xyz.ineanto.nicko.Nicko;
import xyz.ineanto.nicko.language.LanguageKey;
import xyz.ineanto.nicko.mapping.Mapping;
import xyz.ineanto.nicko.mojang.MojangAPI;
import xyz.ineanto.nicko.mojang.MojangSkin;
import xyz.ineanto.nicko.profile.NickoProfile;
import xyz.ineanto.nicko.storage.PlayerDataStore;
import xyz.ineanto.nicko.storage.name.PlayerNameStore;
import xyz.ineanto.nicko.wrapper.*;
import java.io.IOException;
import java.util.EnumSet;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
public class AppearanceManager {
private final Nicko instance = Nicko.getInstance();
private final PlayerDataStore dataStore = instance.getDataStore();
private final PlayerNameStore nameStore = instance.getNameStore();
private final Player player;
public AppearanceManager(Player player) {
this.player = player;
}
public ActionResult reset() {
final NickoProfile profile = getNickoProfile();
final String defaultName = nameStore.getStoredName(player);
profile.setName(defaultName);
profile.setSkin(defaultName);
dataStore.getCache().cache(player.getUniqueId(), profile);
final ActionResult result = updatePlayer(true, true);
if (!result.isError()) {
profile.setName(null);
profile.setSkin(null);
dataStore.getCache().cache(player.getUniqueId(), profile);
}
return result;
}
public ActionResult updatePlayer(boolean skinChange, boolean reset) {
final NickoProfile profile = getNickoProfile();
final String displayName = profile.getName() == null ? player.getName() : profile.getName();
final WrappedGameProfile gameProfile = WrappedGameProfile.fromPlayer(player).withName(displayName);
final ActionResult result = updateGameProfileSkin(gameProfile, skinChange, reset);
if (!result.isError()) {
updateMetadata();
updateTabList(gameProfile, displayName);
respawnPlayerViaMappings();
respawnEntityForOthers();
}
return result;
}
public ActionResult updateForOthers(boolean skinChange, boolean reset) {
final NickoProfile profile = getNickoProfile();
final String displayName = profile.getName() == null ? player.getName() : profile.getName();
final WrappedGameProfile gameProfile = WrappedGameProfile.fromPlayer(player).withName(displayName);
final ActionResult result = updateGameProfileSkin(gameProfile, skinChange, reset);
if (!result.isError()) {
updateMetadata();
updateTabList(gameProfile, displayName);
respawnEntityForOthers();
}
return result;
}
private NickoProfile getNickoProfile() {
final Optional<NickoProfile> optionalProfile = dataStore.getData(player.getUniqueId());
return optionalProfile.orElse(NickoProfile.EMPTY_PROFILE.clone());
}
public void respawnEntityForOthers() {
final NickoProfile nickoProfile = getNickoProfile();
if (!nickoProfile.hasData()) return;
final WrapperPlayServerEntityDestroy destroy = new WrapperPlayServerEntityDestroy();
final WrapperPlayServerSpawnEntity spawn = new WrapperPlayServerSpawnEntity();
destroy.setEntityIds(IntList.of(player.getEntityId()));
spawn.setEntityId(player.getEntityId());
spawn.setLocation(player.getLocation());
spawn.setPlayerId(player.getUniqueId());
Bukkit.getOnlinePlayers().stream().filter(receiver -> receiver.getUniqueId() != player.getUniqueId()).forEach(receiver -> {
destroy.sendPacket(receiver);
spawn.sendPacket(receiver);
});
}
private ActionResult updateGameProfileSkin(WrappedGameProfile gameProfile, boolean skinChange, boolean reset) {
final NickoProfile profile = getNickoProfile();
if (skinChange) {
Optional<MojangSkin> skin;
try {
final MojangAPI mojangAPI = Nicko.getInstance().getMojangAPI();
final Optional<String> uuid = mojangAPI.getUUID(profile.getSkin());
if (uuid.isPresent()) {
skin = reset ? mojangAPI.getSkinWithoutCaching(uuid.get()) : mojangAPI.getSkin(uuid.get());
if (skin.isPresent()) {
final MojangSkin skinResult = skin.get();
final Multimap<String, WrappedSignedProperty> properties = gameProfile.getProperties();
properties.get("textures").clear();
properties.put("textures", new WrappedSignedProperty("textures", skinResult.value(), skinResult.signature()));
} else {
reset();
return ActionResult.error(LanguageKey.Error.MOJANG_SKIN);
}
} else {
reset();
return ActionResult.error(LanguageKey.Error.MOJANG_NAME);
}
return ActionResult.ok();
} catch (ExecutionException e) {
return ActionResult.error(LanguageKey.Error.CACHE);
} catch (IOException e) {
reset();
return ActionResult.error(LanguageKey.Error.MOJANG_NAME);
} catch (InterruptedException e) {
return ActionResult.error("Unknown error");
}
}
return ActionResult.ok();
}
private void updateMetadata() {
final WrappedDataWatcher entityWatcher = WrappedDataWatcher.getEntityWatcher(player);
entityWatcher.setObject(17, (byte) 0x7f, true);
}
private void respawnPlayerViaMappings() {
final Optional<Mapping> mapping = instance.getMappingManager().getMappingForServer();
if (mapping.isEmpty()) return;
mapping.get().respawn(player);
}
@Deprecated
private void respawnPlayerViaProtocolLib() {
final World world = player.getWorld();
final boolean wasFlying = player.isFlying();
final boolean wasAllowedToFly = player.getAllowFlight();
final int foodLevel = player.getFoodLevel();
final WrapperPlayServerRespawn respawn = new WrapperPlayServerRespawn();
//respawn.setDimension(world);
respawn.setSeed(world.getSeed());
respawn.setGameMode(player.getGameMode());
respawn.setPreviousGameMode(player.getGameMode());
respawn.setCopyMetadata(true);
//respawn.sendPacket(player);
player.teleport(player.getLocation(), PlayerTeleportEvent.TeleportCause.PLUGIN);
player.setAllowFlight(wasAllowedToFly);
player.setFlying(wasFlying);
player.updateInventory();
player.sendHealthUpdate();
player.setFoodLevel(foodLevel);
}
@SuppressWarnings("deprecation")
private void updateTabList(WrappedGameProfile gameProfile, String displayName) {
final WrapperPlayerServerPlayerInfo add = new WrapperPlayerServerPlayerInfo();
if (MinecraftVersion.FEATURE_PREVIEW_UPDATE.atOrAbove()) {
final WrapperPlayerServerPlayerInfoRemove remove = new WrapperPlayerServerPlayerInfoRemove();
final EnumSet<EnumWrappers.PlayerInfoAction> actions = EnumSet.of(
EnumWrappers.PlayerInfoAction.ADD_PLAYER,
EnumWrappers.PlayerInfoAction.INITIALIZE_CHAT,
EnumWrappers.PlayerInfoAction.UPDATE_LISTED,
EnumWrappers.PlayerInfoAction.UPDATE_DISPLAY_NAME,
EnumWrappers.PlayerInfoAction.UPDATE_GAME_MODE,
EnumWrappers.PlayerInfoAction.UPDATE_LATENCY);
remove.setUUIDs(List.of(player.getUniqueId()));
remove.broadcastPacket();
add.setActions(actions);
} else {
final WrapperPlayerServerPlayerInfo remove = new WrapperPlayerServerPlayerInfo();
remove.setActions(EnumSet.of(EnumWrappers.PlayerInfoAction.REMOVE_PLAYER));
add.setActions(EnumSet.of(EnumWrappers.PlayerInfoAction.ADD_PLAYER));
remove.broadcastPacket();
}
add.setData(List.of(new PlayerInfoData(
player.getUniqueId(),
player.getPing(),
true,
EnumWrappers.NativeGameMode.fromBukkit(player.getGameMode()),
gameProfile,
WrappedChatComponent.fromText(displayName),
WrappedRemoteChatSessionData.fromPlayer(player)
)));
add.broadcastPacket();
}
}

View file

@ -0,0 +1,36 @@
package xyz.ineanto.nicko.appearance.random;
import xyz.ineanto.nicko.Nicko;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
public class RandomNameFetcher {
private final Nicko instance;
public RandomNameFetcher(Nicko instance) {
this.instance = instance;
}
public String getRandomUsername() {
final InputStream resource = instance.getResource("names.txt");
final List<List<String>> records = new ArrayList<>();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(resource))) {
String line;
while ((line = reader.readLine()) != null) {
final String[] values = line.split("\n");
records.add(Arrays.asList(values));
}
return records.get(new Random().nextInt(records.size() - 1)).getFirst();
} catch (IOException e) {
instance.getLogger().severe("Unable to fetch random names.");
return "Ineanto";
}
}
}

View file

@ -0,0 +1,28 @@
package xyz.ineanto.nicko.command;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import xyz.ineanto.nicko.gui.HomeGUI;
import xyz.ineanto.nicko.language.PlayerLanguage;
import xyz.ineanto.nicko.language.LanguageKey;
public class NickoCommand implements CommandExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
if (sender instanceof Player player) {
if (player.isOp() || player.hasPermission("nicko.use") || player.hasPermission("nicko.*")) {
new HomeGUI(player).open();
} else {
final PlayerLanguage playerLanguage = new PlayerLanguage(player);
player.sendMessage(playerLanguage.translate(LanguageKey.Error.PERMISSION, true));
}
return false;
}
sender.sendMessage("This plugin can only be used in-game. Sorry!");
return false;
}
}

View file

@ -0,0 +1,54 @@
package xyz.ineanto.nicko.config;
import com.fasterxml.jackson.annotation.JsonProperty;
import xyz.ineanto.nicko.version.Version;
public class Configuration {
public static final Version VERSION = new Version(1, 0, 9);
public static final Configuration DEFAULT = new Configuration(VERSION.toString(),
DefaultDataSources.SQL_EMPTY,
DefaultDataSources.REDIS_EMPTY,
false);
private final transient Version versionObject;
@JsonProperty("version")
private final String version;
@JsonProperty("sql")
private final SQLDataSourceConfiguration sqlConfiguration;
@JsonProperty("redis")
private final DataSourceConfiguration redisConfiguration;
@JsonProperty("customLocale")
private final Boolean customLocale;
public Configuration(@JsonProperty("version") String version,
@JsonProperty("sql") SQLDataSourceConfiguration sqlConfiguration,
@JsonProperty("redis") DataSourceConfiguration redisConfiguration,
@JsonProperty("customLocale") Boolean customLocale) {
this.version = version;
this.versionObject = Version.fromString(version);
this.sqlConfiguration = sqlConfiguration;
this.redisConfiguration = redisConfiguration;
this.customLocale = customLocale;
}
public String getVersion() {
return version;
}
public Version getVersionObject() {
return versionObject;
}
public SQLDataSourceConfiguration getSqlConfiguration() {
return sqlConfiguration;
}
public DataSourceConfiguration getRedisConfiguration() {
return redisConfiguration;
}
public Boolean isCustomLocale() {
return customLocale;
}
}

View file

@ -0,0 +1,64 @@
package xyz.ineanto.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.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
public class ConfigurationManager {
private final ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
private final File file;
private final File backupFile;
public ConfigurationManager(File directory) {
final String date = Instant.now()
.atZone(ZoneId.systemDefault())
.format(DateTimeFormatter.ofPattern("dd-MM-yyyy"));
this.file = new File(directory, "config.yml");
this.backupFile = new File(directory, "config.old-" + date + ".yml");
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);
}
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) {
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);
}
}
public File getFile() {
return file;
}
public File getBackupFile() {
return backupFile;
}
}

View file

@ -0,0 +1,50 @@
package xyz.ineanto.nicko.config;
public class DataSourceConfiguration {
private final boolean enabled;
private final String address;
private final Integer port;
private final String username;
private final String password;
public DataSourceConfiguration(boolean enabled, String address, Integer port, String username, String password) {
this.enabled = enabled;
this.address = address;
this.port = port;
this.username = username;
this.password = password;
}
public DataSourceConfiguration() { this(false, "", 0, "", ""); }
public boolean isEnabled() {
return enabled;
}
public String getAddress() {
return address;
}
public Integer getPort() {
return port;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
@Override
public String toString() {
return "DataSourceConfiguration{" +
"enabled=" + enabled +
", address='" + address + '\'' +
", port=" + port +
", username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
}

View file

@ -0,0 +1,8 @@
package xyz.ineanto.nicko.config;
public class DefaultDataSources {
public static final DataSourceConfiguration REDIS_EMPTY = new DataSourceConfiguration(false, "127.0.0.1", 6379, "", "");
public static final SQLDataSourceConfiguration MARIADB_EMPTY = new SQLDataSourceConfiguration(false, "127.0.0.1", 3306, "root", "", true);
public static final SQLDataSourceConfiguration SQL_EMPTY = new SQLDataSourceConfiguration(false, "127.0.0.1", 3306, "root", "", false);
}

View file

@ -0,0 +1,16 @@
package xyz.ineanto.nicko.config;
public class SQLDataSourceConfiguration extends DataSourceConfiguration {
private final boolean mariadb;
public SQLDataSourceConfiguration() { this(false, "", 0, "", "", true); }
public SQLDataSourceConfiguration(boolean enabled, String address, Integer port, String username, String password, boolean mariadb) {
super(enabled, address, port, username, password);
this.mariadb = mariadb;
}
public boolean isMariadb() {
return mariadb;
}
}

View file

@ -0,0 +1,89 @@
package xyz.ineanto.nicko.event;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import xyz.ineanto.nicko.Nicko;
import xyz.ineanto.nicko.appearance.ActionResult;
import xyz.ineanto.nicko.appearance.AppearanceManager;
import xyz.ineanto.nicko.gui.PlayerCheckGUI;
import xyz.ineanto.nicko.gui.PlayerCheckGUIData;
import xyz.ineanto.nicko.language.PlayerLanguage;
import xyz.ineanto.nicko.language.LanguageKey;
import xyz.ineanto.nicko.profile.NickoProfile;
import xyz.ineanto.nicko.storage.PlayerDataStore;
import xyz.ineanto.nicko.storage.name.PlayerNameStore;
import xyz.xenondevs.invui.window.Window;
import xyz.xenondevs.invui.window.WindowManager;
import java.util.ArrayList;
import java.util.Optional;
import java.util.UUID;
import java.util.logging.Logger;
public class PlayerJoinListener implements Listener {
private final Logger logger = Logger.getLogger("PlayerJoinListener");
@EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
public void onPlayerJoin(PlayerJoinEvent event) {
final Player player = event.getPlayer();
final Nicko instance = Nicko.getInstance();
final PlayerLanguage playerLanguage = new PlayerLanguage(player);
final PlayerNameStore nameStore = instance.getNameStore();
final PlayerDataStore dataStore = instance.getDataStore();
nameStore.storeName(player);
final Optional<NickoProfile> optionalProfile = dataStore.getData(player.getUniqueId());
optionalProfile.ifPresentOrElse(profile -> {
// Random Skin on connection feature
if (profile.isRandomSkin()) {
final String name = instance.getNameFetcher().getRandomUsername();
final String skin = instance.getNameFetcher().getRandomUsername();
profile.setName(name);
profile.setSkin(skin);
dataStore.updateCache(player.getUniqueId(), profile);
}
if (profile.hasData()) {
final AppearanceManager appearanceManager = new AppearanceManager(player);
final boolean needsASkinChange = profile.getSkin() != null && !profile.getSkin().equals(player.getName());
final ActionResult actionResult = appearanceManager.updatePlayer(needsASkinChange, false);
if (!actionResult.isError()) {
player.sendMessage(playerLanguage.translateWithWhoosh(LanguageKey.Event.Appearance.Restore.OK));
} else {
player.sendMessage(
playerLanguage.translateWithOops(LanguageKey.Event.Appearance.Restore.ERROR,
playerLanguage.translate(actionResult.getErrorKey(), false)
));
}
}
}, () -> instance.getLogger().warning("Failed to load data for " + player.getName()));
for (Player online : Bukkit.getOnlinePlayers().stream().filter(op -> op.getUniqueId() != player.getUniqueId()).toList()) {
final Optional<NickoProfile> optionalOnlinePlayerProfile = dataStore.getData(online.getUniqueId());
optionalOnlinePlayerProfile.ifPresent(profile -> {
final AppearanceManager appearanceManager = new AppearanceManager(online);
final boolean needsASkinChange = profile.getSkin() != null && !profile.getSkin().equals(online.getName());
final ActionResult actionResult = appearanceManager.updateForOthers(needsASkinChange, false);
if (actionResult.isError()) {
logger.warning("Something wrong happened while updating players to joining player (" + actionResult.getErrorKey() + ")");
}
});
}
@SuppressWarnings("unchecked") final ArrayList<UUID> viewers = (ArrayList<UUID>) PlayerCheckGUIData.VIEWERS.clone();
viewers.forEach(uuid -> {
final Player windowWatcher = Bukkit.getPlayer(uuid);
final Window openWindow = WindowManager.getInstance().getOpenWindow(windowWatcher);
if (openWindow != null) {
final PlayerCheckGUI gui = new PlayerCheckGUI(windowWatcher, Bukkit.getOnlinePlayers());
openWindow.close();
gui.open();
}
});
}
}

View file

@ -0,0 +1,44 @@
package xyz.ineanto.nicko.event;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerQuitEvent;
import xyz.ineanto.nicko.Nicko;
import xyz.ineanto.nicko.appearance.ActionResult;
import xyz.ineanto.nicko.gui.PlayerCheckGUI;
import xyz.ineanto.nicko.gui.PlayerCheckGUIData;
import xyz.xenondevs.invui.window.Window;
import xyz.xenondevs.invui.window.WindowManager;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
public class PlayerQuitListener implements Listener {
@EventHandler
public void onPlayerQuit(PlayerQuitEvent event) {
final Player player = event.getPlayer();
final ActionResult result = Nicko.getInstance().getDataStore().saveData(player);
if (result.isError()) {
Nicko.getInstance().getLogger().warning("Failed to save data for " + player.getName());
}
// This is a dirty way to do it but could be worse tbh
@SuppressWarnings("unchecked") final ArrayList<UUID> viewers = (ArrayList<UUID>) PlayerCheckGUIData.VIEWERS.clone();
viewers.forEach(uuid -> {
final Player windowWatcher = Bukkit.getPlayer(uuid);
final Window openWindow = WindowManager.getInstance().getOpenWindow(windowWatcher);
if (openWindow != null) {
final List<? extends Player> playersWithoutOffline = Bukkit.getOnlinePlayers()
.stream()
.filter(online -> online.getUniqueId() != player.getUniqueId()).collect(Collectors.toList());
final PlayerCheckGUI gui = new PlayerCheckGUI(windowWatcher, playersWithoutOffline);
openWindow.close();
gui.open();
}
});
}
}

View file

@ -0,0 +1,47 @@
package xyz.ineanto.nicko.event.custom;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull;
public class PlayerDisguiseEvent extends Event implements Cancellable {
private static final HandlerList HANDLERS_LIST = new HandlerList();
private boolean isCancelled;
private final Player player;
private final String skin, name;
public PlayerDisguiseEvent(Player player, String skin, String name) {
this.player = player;
this.skin = skin;
this.name = name;
}
@Override
public boolean isCancelled() {
return isCancelled;
}
@Override
public void setCancelled(boolean isCancelled) {
this.isCancelled = isCancelled;
}
@Override
public @NotNull HandlerList getHandlers() {
return HANDLERS_LIST;
}
public Player getPlayer() {
return player;
}
public String getSkin() {
return skin;
}
public String getName() {
return name;
}
}

View file

@ -0,0 +1,52 @@
package xyz.ineanto.nicko.gui;
import org.bukkit.entity.Player;
import xyz.ineanto.nicko.gui.items.ItemDefaults;
import xyz.ineanto.nicko.gui.items.admin.ManageCacheItem;
import xyz.ineanto.nicko.gui.items.admin.ManagePlayerItem;
import xyz.ineanto.nicko.gui.items.common.GoBackItem;
import xyz.ineanto.nicko.language.PlayerLanguage;
import xyz.ineanto.nicko.language.LanguageKey;
import xyz.xenondevs.invui.gui.Gui;
import xyz.xenondevs.invui.item.impl.SimpleItem;
import xyz.xenondevs.invui.window.Window;
public class AdminGUI {
private final Player player;
private final Gui gui;
private final String title;
public AdminGUI(Player player) {
final PlayerLanguage playerLanguage = new PlayerLanguage(player);
this.title = playerLanguage.translate(LanguageKey.GUI.Titles.ADMIN, false);
final HomeGUI parent = new HomeGUI(player);
final GoBackItem backItem = new GoBackItem(player);
final ManagePlayerItem managePlayerItem = new ManagePlayerItem(playerLanguage, player);
this.gui = Gui.normal()
.setStructure(
"# # # # # # # # #",
"# # # S C U # # #",
"B # # # # # # # #"
)
.addIngredient('S', new ManageCacheItem(playerLanguage))
.addIngredient('C', managePlayerItem.get())
.addIngredient('U', new SimpleItem(ItemDefaults.getUnavailableItem(playerLanguage)))
.addIngredient('B', backItem.get(parent.getGUI(), parent.getTitle()))
.build();
this.player = player;
}
public Gui getGUI() {
return gui;
}
public String getTitle() {
return title;
}
public void open() {
Window.single().setGui(gui).setTitle(title).open(player);
}
}

View file

@ -0,0 +1,54 @@
package xyz.ineanto.nicko.gui;
import org.bukkit.entity.Player;
import xyz.ineanto.nicko.gui.items.admin.cache.CacheStatisticsItem;
import xyz.ineanto.nicko.gui.items.admin.cache.InvalidateCacheItem;
import xyz.ineanto.nicko.gui.items.admin.cache.InvalidateSkinItem;
import xyz.ineanto.nicko.gui.items.common.GoBackItem;
import xyz.ineanto.nicko.language.PlayerLanguage;
import xyz.ineanto.nicko.language.LanguageKey;
import xyz.xenondevs.invui.gui.Gui;
import xyz.xenondevs.invui.window.Window;
public class CacheManagementGUI {
private final Player player;
private final Gui gui;
private final String title;
public CacheManagementGUI(Player player) {
final PlayerLanguage playerLanguage = new PlayerLanguage(player);
this.title = playerLanguage.translate(LanguageKey.GUI.Titles.CACHE, false);
final AdminGUI parent = new AdminGUI(player);
final GoBackItem backItem = new GoBackItem(player);
final CacheStatisticsItem cacheStatisticsItem = new CacheStatisticsItem(player);
final InvalidateCacheItem invalidateCacheItem = new InvalidateCacheItem(player);
final InvalidateSkinItem invalidateSkinItem = new InvalidateSkinItem(player);
this.gui = Gui.normal()
.setStructure(
"# # # # # # # # #",
"# # # S C E # # #",
"B # # # # # # # #"
)
.addIngredient('B', backItem.get(parent.getGUI(), parent.getTitle()))
.addIngredient('S', cacheStatisticsItem.get())
.addIngredient('C', invalidateCacheItem.get())
.addIngredient('E', invalidateSkinItem.get())
.build();
this.player = player;
}
public Gui getGUI() {
return gui;
}
public String getTitle() {
return title;
}
public void open() {
Window.single().setGui(gui).setTitle(title).open(player);
}
}

View file

@ -0,0 +1,42 @@
package xyz.ineanto.nicko.gui;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import xyz.ineanto.nicko.gui.items.common.choice.CancelItem;
import xyz.ineanto.nicko.gui.items.common.choice.ChoiceCallback;
import xyz.ineanto.nicko.gui.items.common.choice.ConfirmItem;
import xyz.ineanto.nicko.language.PlayerLanguage;
import xyz.ineanto.nicko.language.LanguageKey;
import xyz.xenondevs.invui.gui.Gui;
import xyz.xenondevs.invui.item.builder.ItemBuilder;
import xyz.xenondevs.invui.item.impl.SimpleItem;
import xyz.xenondevs.invui.window.Window;
public class ChoiceGUI {
private final Player player;
private final Gui gui;
private final String title;
public ChoiceGUI(Player player, ChoiceCallback callback) {
final PlayerLanguage playerLanguage = new PlayerLanguage(player);
final ConfirmItem confirmItem = new ConfirmItem(player, callback);
final CancelItem cancelItem = new CancelItem(player, callback);
this.title = playerLanguage.translate(LanguageKey.GUI.Titles.CONFIRM, false);
this.gui = Gui.normal()
.setStructure(
"@ @ @ @ % & & & &",
"@ @ @ @ I & & & &",
"@ @ @ @ % & & & &"
)
.addIngredient('@', confirmItem.get())
.addIngredient('&', cancelItem.get())
.addIngredient('I', new SimpleItem(playerLanguage.translateItem(new ItemBuilder(Material.PAPER), LanguageKey.GUI.Choice.CHOOSE)))
.build();
this.player = player;
}
public void open() {
Window.single().setGui(gui).setTitle(title).open(player);
}
}

View file

@ -0,0 +1,65 @@
package xyz.ineanto.nicko.gui;
import org.bukkit.entity.Player;
import xyz.ineanto.nicko.gui.items.appearance.ChangeBothItem;
import xyz.ineanto.nicko.gui.items.appearance.ChangeNameItem;
import xyz.ineanto.nicko.gui.items.appearance.ChangeSkinItem;
import xyz.ineanto.nicko.gui.items.home.*;
import xyz.ineanto.nicko.language.LanguageKey;
import xyz.ineanto.nicko.language.PlayerLanguage;
import xyz.xenondevs.invui.gui.Gui;
import xyz.xenondevs.invui.window.Window;
public class HomeGUI {
private final Player player;
private final Gui gui;
private final String title;
public HomeGUI(Player player) {
final String[] dynamicStructure = new String[]{
"# # # # D # # # #",
"A # # N B S # # #",
"E P # # # # # # R"};
if (!player.isOp() || !player.hasPermission("nicko.admin")) {
dynamicStructure[2] = dynamicStructure[2].replace("A", "#");
}
final PlayerLanguage playerLanguage = new PlayerLanguage(player);
this.title = playerLanguage.translate(LanguageKey.GUI.Titles.HOME, false);
final ExitItem exitItem = new ExitItem(player);
final ResetItem resetItem = new ResetItem(player);
final ChangeNameItem changeNameItem = new ChangeNameItem(player);
final ChangeBothItem changeBothItem = new ChangeBothItem(player);
final ChangeSkinItem changeSkinItem = new ChangeSkinItem(player);
final SettingsAccessItem settingsAccessItem = new SettingsAccessItem(player);
final AdminAccessItem adminAccessItem = new AdminAccessItem(player);
final RandomSkinItem randomSkinItem = new RandomSkinItem(player);
this.gui = Gui.normal()
.setStructure(dynamicStructure)
.addIngredient('E', exitItem.get())
.addIngredient('R', resetItem.get())
.addIngredient('N', changeNameItem.get())
.addIngredient('B', changeBothItem.get())
.addIngredient('S', changeSkinItem.get())
.addIngredient('P', settingsAccessItem.get())
.addIngredient('A', adminAccessItem.get())
.addIngredient('D', randomSkinItem.get())
.build();
this.player = player;
}
public Gui getGUI() {
return gui;
}
public String getTitle() {
return title;
}
public void open() {
Window.single().setGui(gui).setTitle(title).open(player);
}
}

View file

@ -0,0 +1,69 @@
package xyz.ineanto.nicko.gui;
import xyz.ineanto.nicko.gui.items.common.GoBackItem;
import xyz.ineanto.nicko.gui.items.common.ScrollUpItem;
import xyz.ineanto.nicko.language.PlayerLanguage;
import xyz.ineanto.nicko.language.LanguageKey;
import xyz.ineanto.nicko.mojang.MojangSkin;
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 xyz.ineanto.nicko.Nicko;
import xyz.ineanto.nicko.gui.items.admin.cache.CacheEntryItem;
import xyz.ineanto.nicko.gui.items.common.ScrollDownItem;
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 InvalidateSkinGUI {
private final Player player;
private final Gui gui;
private final String title;
public InvalidateSkinGUI(Player player) {
final PlayerLanguage playerLanguage = new PlayerLanguage(player);
this.title = playerLanguage.translate(LanguageKey.GUI.Titles.INVALIDATE_SKIN, false);
final ConcurrentMap<String, Optional<MojangSkin>> skins = Nicko.getInstance().getMojangAPI().getSkinCache().asMap();
final List<String> loadedSkins = skins.entrySet().stream()
.filter(entry -> entry.getValue().isPresent())
.map(Map.Entry::getKey)
.toList();
final List<Item> items = loadedSkins.stream()
.map(uuid -> new CacheEntryItem(playerLanguage, uuid))
.collect(Collectors.toList());
final CacheManagementGUI parent = new CacheManagementGUI(player);
final ScrollUpItem scrollUpItem = new ScrollUpItem(playerLanguage);
final ScrollDownItem scrollDownItem = new ScrollDownItem(playerLanguage);
final GoBackItem backItem = new GoBackItem(player);
gui = ScrollGui.items(guiItemBuilder -> {
guiItemBuilder.setStructure(
"x x 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 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', scrollUpItem);
guiItemBuilder.addIngredient('D', scrollDownItem);
guiItemBuilder.addIngredient('B', backItem.get(parent.getGUI(), parent.getTitle()));
guiItemBuilder.setContent(items);
});
this.player = player;
}
public void open() {
Window.single().setGui(gui).setTitle(title).open(player);
}
}

View file

@ -0,0 +1,68 @@
package xyz.ineanto.nicko.gui;
import org.bukkit.Bukkit;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import xyz.ineanto.nicko.gui.items.admin.check.PlayerInformationItem;
import xyz.ineanto.nicko.gui.items.common.GoBackItem;
import xyz.ineanto.nicko.gui.items.common.ScrollDownItem;
import xyz.ineanto.nicko.gui.items.common.ScrollUpItem;
import xyz.ineanto.nicko.language.PlayerLanguage;
import xyz.ineanto.nicko.language.LanguageKey;
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 xyz.xenondevs.invui.window.Window;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
public class PlayerCheckGUI {
private final Player player;
private final Gui gui;
private final String title;
public PlayerCheckGUI(Player player, Collection<? extends Player> players) {
final PlayerLanguage playerLanguage = new PlayerLanguage(player);
this.title = playerLanguage.translate(LanguageKey.GUI.Titles.CHECK, false);
final List<Item> items = players.stream()
.map(Entity::getUniqueId)
.map(Bukkit::getPlayer)
.filter(Objects::nonNull)
.map(mappedPlayer -> new PlayerInformationItem(playerLanguage, mappedPlayer))
.collect(Collectors.toList());
final AdminGUI parent = new AdminGUI(player);
final GoBackItem backItem = new GoBackItem(player);
final ScrollUpItem scrollUpItem = new ScrollUpItem(playerLanguage);
final ScrollDownItem scrollDownItem = new ScrollDownItem(playerLanguage);
gui = ScrollGui.items(guiItemBuilder -> {
guiItemBuilder.setStructure(
"x x 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 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', scrollUpItem);
guiItemBuilder.addIngredient('D', scrollDownItem);
guiItemBuilder.addIngredient('B', backItem.get(parent.getGUI(), parent.getTitle()));
guiItemBuilder.setContent(items);
});
this.player = player;
}
public void open() {
final Window.Builder.Normal.Single window = Window.single().setGui(gui).setTitle(title);
window.addOpenHandler(() -> PlayerCheckGUIData.VIEWERS.add(player.getUniqueId()));
window.addCloseHandler(() -> PlayerCheckGUIData.VIEWERS.remove(player.getUniqueId()));
window.open(player);
}
}

View file

@ -0,0 +1,8 @@
package xyz.ineanto.nicko.gui;
import java.util.ArrayList;
import java.util.UUID;
public class PlayerCheckGUIData {
public static final ArrayList<UUID> VIEWERS = new ArrayList<>();
}

View file

@ -0,0 +1,44 @@
package xyz.ineanto.nicko.gui;
import org.bukkit.entity.Player;
import xyz.ineanto.nicko.gui.items.common.GoBackItem;
import xyz.ineanto.nicko.gui.items.settings.LanguageCyclingItem;
import xyz.ineanto.nicko.gui.items.settings.RandomSkinCyclingItem;
import xyz.ineanto.nicko.language.PlayerLanguage;
import xyz.ineanto.nicko.language.LanguageKey;
import xyz.xenondevs.invui.gui.Gui;
import xyz.xenondevs.invui.window.Window;
public class SettingsGUI {
private final Player player;
private final Gui gui;
private final String title;
public SettingsGUI(Player player) {
final String[] dynamicStructure = new String[]{
"# # # # # # # # #",
"# # # L # R # # #",
"B # # # # # # # #"
};
final PlayerLanguage playerLanguage = new PlayerLanguage(player);
this.title = playerLanguage.translate(LanguageKey.GUI.Titles.SETTINGS, false);
final HomeGUI parent = new HomeGUI(player);
final LanguageCyclingItem languageItem = new LanguageCyclingItem(player);
final RandomSkinCyclingItem skinItem = new RandomSkinCyclingItem(player);
final GoBackItem backItem = new GoBackItem(player);
this.gui = Gui.normal()
.setStructure(dynamicStructure)
.addIngredient('B', backItem.get(parent.getGUI(), parent.getTitle()))
.addIngredient('L', languageItem.get())
.addIngredient('R', skinItem.get())
.build();
this.player = player;
}
public void open() {
Window.single().setGui(gui).setTitle(title).open(player);
}
}

View file

@ -0,0 +1,22 @@
package xyz.ineanto.nicko.gui.items;
import org.bukkit.Material;
import xyz.ineanto.nicko.language.PlayerLanguage;
import xyz.ineanto.nicko.language.LanguageKey;
import xyz.xenondevs.invui.item.builder.AbstractItemBuilder;
import xyz.xenondevs.invui.item.builder.ItemBuilder;
import xyz.xenondevs.invui.item.builder.SkullBuilder;
public class ItemDefaults {
public static AbstractItemBuilder<?> getErrorSkullItem(PlayerLanguage playerLanguage, String key, Object... args) {
// "Missing Value" (Valve's signature missing texture) Texture Value
final SkullBuilder.HeadTexture headTexture = new SkullBuilder.HeadTexture("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNjNmZTU5YjJhMWQyYmYzMjcwNDA2OGVmYzg2MGM3NWY5MjEyYzIzMTBiNDNkMDdjNGJiYTRiNGViMjM0ZTY4NCJ9fX0=");
final SkullBuilder builder = new SkullBuilder(headTexture);
return playerLanguage.translateItem(builder, key, args);
}
public static AbstractItemBuilder<?> getUnavailableItem(PlayerLanguage playerLanguage) {
final ItemBuilder builder = new ItemBuilder(Material.RED_TERRACOTTA);
return playerLanguage.translateItem(builder, LanguageKey.GUI.UNAVAILABLE);
}
}

View file

@ -0,0 +1,45 @@
package xyz.ineanto.nicko.gui.items.admin;
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;
import xyz.ineanto.nicko.Nicko;
import xyz.ineanto.nicko.gui.CacheManagementGUI;
import xyz.ineanto.nicko.gui.items.ItemDefaults;
import xyz.ineanto.nicko.language.PlayerLanguage;
import xyz.ineanto.nicko.language.LanguageKey;
import xyz.xenondevs.invui.item.builder.ItemBuilder;
import xyz.xenondevs.invui.item.builder.SkullBuilder;
import xyz.xenondevs.invui.item.impl.AsyncItem;
import xyz.xenondevs.invui.item.impl.SuppliedItem;
import xyz.xenondevs.invui.util.MojangApiUtils;
import java.io.IOException;
public class ManageCacheItem extends AsyncItem {
public ManageCacheItem(PlayerLanguage playerLanguage) {
super(new SuppliedItem(() -> {
final ItemBuilder builder = new ItemBuilder(Material.PAINTING);
return playerLanguage.translateItem(builder, LanguageKey.GUI.LOADING);
}, (click -> true)).getItemProvider(),
() -> {
try {
final SkullBuilder builder = new SkullBuilder("Notch");
return playerLanguage.translateItem(builder, LanguageKey.GUI.Admin.MANAGE_CACHE);
} catch (MojangApiUtils.MojangApiException | IOException e) {
Nicko.getInstance().getLogger().warning("Unable to get Head texture for Notch! (GUI/ManageCache)");
return ItemDefaults.getErrorSkullItem(playerLanguage, LanguageKey.GUI.Admin.MANAGE_CACHE);
}
});
}
@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,30 @@
package xyz.ineanto.nicko.gui.items.admin;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import xyz.ineanto.nicko.gui.PlayerCheckGUI;
import xyz.ineanto.nicko.language.PlayerLanguage;
import xyz.ineanto.nicko.language.LanguageKey;
import xyz.xenondevs.invui.item.builder.ItemBuilder;
import xyz.xenondevs.invui.item.impl.SuppliedItem;
public class ManagePlayerItem {
private final Player player;
private final PlayerLanguage playerLanguage;
public ManagePlayerItem(PlayerLanguage playerLanguage, Player player) {
this.playerLanguage = playerLanguage;
this.player = player;
}
public SuppliedItem get() {
return new SuppliedItem(() -> {
final ItemBuilder builder = new ItemBuilder(Material.WRITABLE_BOOK);
return playerLanguage.translateItem(builder, LanguageKey.GUI.Admin.MANAGE_PLAYER);
}, click -> {
new PlayerCheckGUI(player, Bukkit.getOnlinePlayers()).open();
return true;
});
}
}

View file

@ -0,0 +1,69 @@
package xyz.ineanto.nicko.gui.items.admin.cache;
import net.kyori.adventure.text.Component;
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;
import xyz.ineanto.nicko.Nicko;
import xyz.ineanto.nicko.gui.ChoiceGUI;
import xyz.ineanto.nicko.gui.InvalidateSkinGUI;
import xyz.ineanto.nicko.gui.items.ItemDefaults;
import xyz.ineanto.nicko.gui.items.common.choice.ChoiceCallback;
import xyz.ineanto.nicko.language.LanguageKey;
import xyz.ineanto.nicko.language.PlayerLanguage;
import xyz.ineanto.nicko.mojang.MojangAPI;
import xyz.xenondevs.invui.item.builder.ItemBuilder;
import xyz.xenondevs.invui.item.builder.SkullBuilder;
import xyz.xenondevs.invui.item.impl.AsyncItem;
import xyz.xenondevs.invui.util.MojangApiUtils;
import java.io.IOException;
import java.util.UUID;
public class CacheEntryItem extends AsyncItem {
private final String name;
private final String uuid;
private final MojangAPI mojangAPI = Nicko.getInstance().getMojangAPI();
public CacheEntryItem(PlayerLanguage playerLanguage, String uuid) {
super(new ItemBuilder(Material.PAINTING)
.setDisplayName(
Component.text(playerLanguage.translate(LanguageKey.GUI.LOADING, false)).content()
),
() -> {
final String dashedUuid = uuid.replaceAll("(.{8})(.{4})(.{4})(.{4})(.+)", "$1-$2-$3-$4-$5");
final UUID uuidObject = UUID.fromString(dashedUuid);
try {
final SkullBuilder skull = new SkullBuilder(uuidObject);
return playerLanguage.translateItem(skull, LanguageKey.GUI.Admin.Cache.ENTRY, Nicko.getInstance().getMojangAPI().getUUIDName(uuid));
} catch (MojangApiUtils.MojangApiException | IOException e) {
Nicko.getInstance().getLogger().warning("Unable to get Head texture for specified UUID (" + uuid + ")! (GUI/Cache/Entry)");
return ItemDefaults.getErrorSkullItem(playerLanguage, LanguageKey.GUI.Admin.Cache.ENTRY, Nicko.getInstance().getMojangAPI().getUUIDName(uuid));
}
});
this.uuid = uuid;
this.name = mojangAPI.getUUIDName(uuid);
}
@Override
public void handleClick(@NotNull ClickType click, @NotNull Player player, @NotNull InventoryClickEvent event) {
if (click.isLeftClick() || click.isRightClick()) {
event.getView().close();
new ChoiceGUI(player, new ChoiceCallback() {
@Override
public void onConfirm() {
final PlayerLanguage playerLanguage = new PlayerLanguage(player);
player.sendMessage(playerLanguage.translateWithWhoosh(LanguageKey.Event.Admin.Cache.INVALIDATE_ENTRY, name));
mojangAPI.eraseFromCache(uuid);
}
@Override
public void onCancel() {
new InvalidateSkinGUI(player).open();
}
}).open();
}
}
}

View file

@ -0,0 +1,35 @@
package xyz.ineanto.nicko.gui.items.admin.cache;
import com.google.common.cache.CacheStats;
import com.google.common.cache.LoadingCache;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import xyz.ineanto.nicko.Nicko;
import xyz.ineanto.nicko.language.PlayerLanguage;
import xyz.ineanto.nicko.language.LanguageKey;
import xyz.ineanto.nicko.mojang.MojangSkin;
import xyz.xenondevs.invui.item.builder.ItemBuilder;
import xyz.xenondevs.invui.item.impl.SuppliedItem;
import java.util.Optional;
public class CacheStatisticsItem {
private final PlayerLanguage playerLanguage;
public CacheStatisticsItem(Player player) {
this.playerLanguage = new PlayerLanguage(player);
}
public SuppliedItem get() {
return new SuppliedItem(() -> {
final ItemBuilder builder = new ItemBuilder(Material.BOOK);
final LoadingCache<String, Optional<MojangSkin>> cache = Nicko.getInstance().getMojangAPI().getSkinCache();
final CacheStats stats = cache.stats();
return playerLanguage.translateItem(builder, LanguageKey.GUI.Admin.Cache.STATISTICS,
stats.requestCount(),
Math.round(cache.size())
);
}, (event) -> true);
}
}

View file

@ -0,0 +1,37 @@
package xyz.ineanto.nicko.gui.items.admin.cache;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.ClickType;
import xyz.ineanto.nicko.Nicko;
import xyz.ineanto.nicko.language.PlayerLanguage;
import xyz.ineanto.nicko.language.LanguageKey;
import xyz.xenondevs.invui.item.builder.ItemBuilder;
import xyz.xenondevs.invui.item.impl.SuppliedItem;
public class InvalidateCacheItem {
private final PlayerLanguage playerLanguage;
public InvalidateCacheItem(Player player) {
this.playerLanguage = new PlayerLanguage(player);
}
public SuppliedItem get() {
return new SuppliedItem(() -> {
final ItemBuilder builder = new ItemBuilder(Material.TNT);
return playerLanguage.translateItem(builder, LanguageKey.GUI.Admin.Cache.INVALIDATE_CACHE);
}, (click) -> {
final ClickType clickType = click.getClickType();
if (clickType.isLeftClick() || clickType.isRightClick()) {
click.getEvent().getView().close();
final Player player = click.getPlayer();
final PlayerLanguage playerLanguage = new PlayerLanguage(player);
player.sendMessage(playerLanguage.translateWithWhoosh(LanguageKey.Event.Admin.Cache.INVALIDATE_CACHE));
Nicko.getInstance().getMojangAPI().getSkinCache().invalidateAll();
return true;
}
return false;
});
}
}

View file

@ -0,0 +1,33 @@
package xyz.ineanto.nicko.gui.items.admin.cache;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.ClickType;
import xyz.ineanto.nicko.gui.InvalidateSkinGUI;
import xyz.ineanto.nicko.language.PlayerLanguage;
import xyz.ineanto.nicko.language.LanguageKey;
import xyz.xenondevs.invui.item.builder.ItemBuilder;
import xyz.xenondevs.invui.item.impl.SuppliedItem;
public class InvalidateSkinItem {
private final PlayerLanguage playerLanguage;
public InvalidateSkinItem(Player player) {
this.playerLanguage = new PlayerLanguage(player);
}
public SuppliedItem get() {
return new SuppliedItem(() -> {
final ItemBuilder builder = new ItemBuilder(Material.PAPER);
return playerLanguage.translateItem(builder, LanguageKey.GUI.Admin.Cache.INVALIDATE_SKIN);
}, (click) -> {
final ClickType clickType = click.getClickType();
if (clickType.isLeftClick() || clickType.isRightClick()) {
click.getEvent().getView().close();
new InvalidateSkinGUI(click.getPlayer()).open();
return true;
}
return false;
});
}
}

View file

@ -0,0 +1,93 @@
package xyz.ineanto.nicko.gui.items.admin.check;
import org.bukkit.Bukkit;
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;
import xyz.ineanto.nicko.Nicko;
import xyz.ineanto.nicko.appearance.AppearanceManager;
import xyz.ineanto.nicko.gui.ChoiceGUI;
import xyz.ineanto.nicko.gui.PlayerCheckGUI;
import xyz.ineanto.nicko.gui.items.ItemDefaults;
import xyz.ineanto.nicko.gui.items.common.choice.ChoiceCallback;
import xyz.ineanto.nicko.language.PlayerLanguage;
import xyz.ineanto.nicko.language.LanguageKey;
import xyz.ineanto.nicko.profile.NickoProfile;
import xyz.xenondevs.invui.item.builder.AbstractItemBuilder;
import xyz.xenondevs.invui.item.builder.ItemBuilder;
import xyz.xenondevs.invui.item.builder.SkullBuilder;
import xyz.xenondevs.invui.item.impl.AsyncItem;
import xyz.xenondevs.invui.item.impl.SuppliedItem;
import xyz.xenondevs.invui.util.MojangApiUtils;
import java.io.IOException;
import java.util.Optional;
public class PlayerInformationItem extends AsyncItem {
private final Player target;
private final NickoProfile profile;
private final PlayerLanguage playerLanguage;
public PlayerInformationItem(PlayerLanguage playerLanguage, Player target) {
super(new SuppliedItem(() -> {
final ItemBuilder builder = new ItemBuilder(Material.PAINTING);
return playerLanguage.translateItem(builder, LanguageKey.GUI.LOADING);
}, (click -> true)).getItemProvider(), () -> {
try {
final SkullBuilder skull = new SkullBuilder(target.getUniqueId());
final Optional<NickoProfile> optionalProfile = Nicko.getInstance().getDataStore().getData(target.getUniqueId());
if (optionalProfile.isPresent()) {
final NickoProfile profile = optionalProfile.get();
final AbstractItemBuilder<?> headItem = playerLanguage.translateItem(skull, LanguageKey.GUI.Admin.CHECK,
target.getName(),
(profile.hasData() ? "<green>✔</green>" : "<red>❌</red>"),
(profile.getName() == null ? "<grey>N/A<grey>" : profile.getName()),
(profile.getSkin() == null ? "<grey>N/A</grey>" : profile.getSkin()));
if (!profile.hasData()) {
// Remove the last 2 lines of the lore.
headItem.removeLoreLine(headItem.getLore().size() - 1);
headItem.removeLoreLine(headItem.getLore().size() - 1);
}
return headItem;
}
} catch (MojangApiUtils.MojangApiException | IOException e) {
Nicko.getInstance().getLogger().severe("Unable to get head for specified UUID ( " + target.getUniqueId() + ")! (GUI/PlayerCheck)");
}
return ItemDefaults.getErrorSkullItem(playerLanguage, LanguageKey.GUI.Admin.CHECK,
"§c§l?!?", "§7N/A", "§7N/A", "§7N/A"
);
});
this.playerLanguage = playerLanguage;
this.target = target;
this.profile = Nicko.getInstance().getDataStore().getData(target.getUniqueId()).orElse(NickoProfile.EMPTY_PROFILE);
}
@Override
public void handleClick(@NotNull ClickType click, @NotNull Player player, @NotNull InventoryClickEvent event) {
if (click.isLeftClick() || click.isRightClick()) {
if (profile.hasData()) {
event.getView().close();
new ChoiceGUI(player, new ChoiceCallback() {
@Override
public void onConfirm() {
final AppearanceManager appearanceManager = new AppearanceManager(target);
appearanceManager.reset();
player.sendMessage(playerLanguage.translate(LanguageKey.Event.Admin.Check.REMOVE_SKIN, true, target.getName()));
}
@Override
public void onCancel() {
new PlayerCheckGUI(player, Bukkit.getOnlinePlayers()).open();
}
}).open();
}
}
}
}

View file

@ -0,0 +1,33 @@
package xyz.ineanto.nicko.gui.items.appearance;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.ClickType;
import xyz.ineanto.nicko.anvil.AnvilManager;
import xyz.ineanto.nicko.language.PlayerLanguage;
import xyz.ineanto.nicko.language.LanguageKey;
import xyz.xenondevs.invui.item.builder.ItemBuilder;
import xyz.xenondevs.invui.item.impl.SuppliedItem;
public class ChangeBothItem {
private final PlayerLanguage playerLanguage;
public ChangeBothItem(Player player) {
this.playerLanguage = new PlayerLanguage(player);
}
public SuppliedItem get() {
return new SuppliedItem(() -> {
final ItemBuilder builder = new ItemBuilder(Material.TOTEM_OF_UNDYING);
return playerLanguage.translateItem(builder, LanguageKey.GUI.Home.CHANGE_BOTH);
}, 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,33 @@
package xyz.ineanto.nicko.gui.items.appearance;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.ClickType;
import xyz.ineanto.nicko.anvil.AnvilManager;
import xyz.ineanto.nicko.language.PlayerLanguage;
import xyz.ineanto.nicko.language.LanguageKey;
import xyz.xenondevs.invui.item.builder.ItemBuilder;
import xyz.xenondevs.invui.item.impl.SuppliedItem;
public class ChangeNameItem {
private final PlayerLanguage playerLanguage;
public ChangeNameItem(Player player) {
this.playerLanguage = new PlayerLanguage(player);
}
public SuppliedItem get() {
return new SuppliedItem(() -> {
final ItemBuilder builder = new ItemBuilder(Material.NAME_TAG);
return playerLanguage.translateItem(builder, LanguageKey.GUI.Home.CHANGE_NAME);
}, 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,44 @@
package xyz.ineanto.nicko.gui.items.appearance;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.ClickType;
import xyz.ineanto.nicko.Nicko;
import xyz.ineanto.nicko.anvil.AnvilManager;
import xyz.ineanto.nicko.gui.items.ItemDefaults;
import xyz.ineanto.nicko.language.PlayerLanguage;
import xyz.ineanto.nicko.language.LanguageKey;
import xyz.xenondevs.invui.item.builder.SkullBuilder;
import xyz.xenondevs.invui.item.impl.SuppliedItem;
import xyz.xenondevs.invui.util.MojangApiUtils;
import java.io.IOException;
public class ChangeSkinItem {
private final PlayerLanguage playerLanguage;
private final Player player;
public ChangeSkinItem(Player player) {
this.playerLanguage = new PlayerLanguage(player);
this.player = player;
}
public SuppliedItem get() {
return new SuppliedItem(() -> {
try {
final SkullBuilder builder = new SkullBuilder(player.getName());
return playerLanguage.translateItem(builder, LanguageKey.GUI.Home.CHANGE_SKIN);
} catch (MojangApiUtils.MojangApiException | IOException e) {
Nicko.getInstance().getLogger().warning("Unable to get Head texture for specified player (" + player.getName() + ")! (GUI/Home)");
return ItemDefaults.getErrorSkullItem(playerLanguage, LanguageKey.GUI.Home.CHANGE_SKIN);
}
}, 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,29 @@
package xyz.ineanto.nicko.gui.items.common;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import xyz.ineanto.nicko.language.PlayerLanguage;
import xyz.ineanto.nicko.language.LanguageKey;
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 GoBackItem {
private final PlayerLanguage playerLanguage;
public GoBackItem(Player player) {
this.playerLanguage = new PlayerLanguage(player);
}
public SuppliedItem get(Gui gui, String parentTitle) {
return new SuppliedItem(() -> {
final ItemBuilder builder = new ItemBuilder(Material.ARROW);
return playerLanguage.translateItem(builder, LanguageKey.GUI.GO_BACK);
}, click -> {
click.getEvent().getView().close();
Window.single().setGui(gui).setTitle(parentTitle).open(click.getPlayer());
return true;
});
}
}

View file

@ -0,0 +1,39 @@
package xyz.ineanto.nicko.gui.items.common;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import org.bukkit.Material;
import xyz.ineanto.nicko.language.PlayerLanguage;
import xyz.ineanto.nicko.language.LanguageKey;
import xyz.ineanto.nicko.language.Translation;
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 ScrollDownItem extends ScrollItem {
final PlayerLanguage playerLanguage;
public ScrollDownItem(PlayerLanguage playerLanguage) {
super(1);
this.playerLanguage = playerLanguage;
}
@Override
public ItemProvider getItemProvider(ScrollGui gui) {
final ItemBuilder builder = new ItemBuilder(Material.GREEN_STAINED_GLASS_PANE);
final Translation translation = playerLanguage.translateAndReplace(LanguageKey.GUI.SCROLL_DOWN);
builder.setDisplayName(Component.text(translation.name()).content());
if (!gui.canScroll(1)) {
// Lore serialization
translation.lore().replaceAll(s -> {
final Component deserializedLoreLine = MiniMessage.miniMessage().deserialize(s);
return LegacyComponentSerializer.legacySection().serialize(deserializedLoreLine);
});
translation.lore().forEach(builder::addLoreLines);
}
return builder;
}
}

View file

@ -0,0 +1,40 @@
package xyz.ineanto.nicko.gui.items.common;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import org.bukkit.Material;
import xyz.ineanto.nicko.language.PlayerLanguage;
import xyz.ineanto.nicko.language.LanguageKey;
import xyz.ineanto.nicko.language.Translation;
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 ScrollUpItem extends ScrollItem {
final PlayerLanguage playerLanguage;
public ScrollUpItem(PlayerLanguage playerLanguage) {
super(-1);
this.playerLanguage = playerLanguage;
}
@Override
public ItemProvider getItemProvider(ScrollGui gui) {
final ItemBuilder builder = new ItemBuilder(Material.RED_STAINED_GLASS_PANE);
final Translation translation = playerLanguage.translateAndReplace(LanguageKey.GUI.SCROLL_UP);
builder.setDisplayName(Component.text(translation.name()).content());
if (!gui.canScroll(-1)) {
// Lore serialization
translation.lore().replaceAll(s -> {
final Component deserializedLoreLine = MiniMessage.miniMessage().deserialize(s);
return LegacyComponentSerializer.legacySection().serialize(deserializedLoreLine);
});
translation.lore().forEach(builder::addLoreLines);
}
return builder;
}
}

View file

@ -0,0 +1,29 @@
package xyz.ineanto.nicko.gui.items.common.choice;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import xyz.ineanto.nicko.language.PlayerLanguage;
import xyz.ineanto.nicko.language.LanguageKey;
import xyz.xenondevs.invui.item.builder.ItemBuilder;
import xyz.xenondevs.invui.item.impl.SuppliedItem;
public class CancelItem {
private final PlayerLanguage playerLanguage;
private final ChoiceCallback callback;
public CancelItem(Player player, ChoiceCallback callback) {
this.playerLanguage = new PlayerLanguage(player);
this.callback = callback;
}
public SuppliedItem get() {
return new SuppliedItem(() -> {
final ItemBuilder builder = new ItemBuilder(Material.RED_STAINED_GLASS_PANE);
return playerLanguage.translateItem(builder, LanguageKey.GUI.Choice.CANCEL);
}, click -> {
click.getEvent().getView().close();
callback.onCancel();
return true;
});
}
}

View file

@ -0,0 +1,7 @@
package xyz.ineanto.nicko.gui.items.common.choice;
public interface ChoiceCallback {
void onConfirm();
void onCancel();
}

View file

@ -0,0 +1,29 @@
package xyz.ineanto.nicko.gui.items.common.choice;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import xyz.ineanto.nicko.language.PlayerLanguage;
import xyz.ineanto.nicko.language.LanguageKey;
import xyz.xenondevs.invui.item.builder.ItemBuilder;
import xyz.xenondevs.invui.item.impl.SuppliedItem;
public class ConfirmItem {
private final PlayerLanguage playerLanguage;
private final ChoiceCallback callback;
public ConfirmItem(Player player, ChoiceCallback callback) {
this.playerLanguage = new PlayerLanguage(player);
this.callback = callback;
}
public SuppliedItem get() {
return new SuppliedItem(() -> {
final ItemBuilder builder = new ItemBuilder(Material.GREEN_STAINED_GLASS_PANE);
return playerLanguage.translateItem(builder, LanguageKey.GUI.Choice.CONFIRM);
}, click -> {
click.getEvent().getView().close();
callback.onConfirm();
return true;
});
}
}

View file

@ -0,0 +1,33 @@
package xyz.ineanto.nicko.gui.items.home;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.ClickType;
import xyz.ineanto.nicko.gui.AdminGUI;
import xyz.ineanto.nicko.language.PlayerLanguage;
import xyz.ineanto.nicko.language.LanguageKey;
import xyz.xenondevs.invui.item.builder.ItemBuilder;
import xyz.xenondevs.invui.item.impl.SuppliedItem;
public class AdminAccessItem {
private final PlayerLanguage playerLanguage;
public AdminAccessItem(Player player) {
this.playerLanguage = new PlayerLanguage(player);
}
public SuppliedItem get() {
return new SuppliedItem(() -> {
final ItemBuilder builder = new ItemBuilder(Material.COMMAND_BLOCK_MINECART);
return playerLanguage.translateItem(builder, LanguageKey.GUI.Home.ADMIN);
}, 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,31 @@
package xyz.ineanto.nicko.gui.items.home;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.ClickType;
import xyz.ineanto.nicko.language.PlayerLanguage;
import xyz.ineanto.nicko.language.LanguageKey;
import xyz.xenondevs.invui.item.builder.ItemBuilder;
import xyz.xenondevs.invui.item.impl.SuppliedItem;
public class ExitItem {
private final PlayerLanguage playerLanguage;
public ExitItem(Player player) {
this.playerLanguage = new PlayerLanguage(player);
}
public SuppliedItem get() {
return new SuppliedItem(() -> {
final ItemBuilder builder = new ItemBuilder(Material.OAK_DOOR);
return playerLanguage.translateItem(builder, LanguageKey.GUI.EXIT);
}, click -> {
click.getEvent().getView().close();
final ClickType clickType = click.getClickType();
if (clickType.isLeftClick() || clickType.isRightClick()) {
click.getEvent().getView().close();
}
return true;
});
}
}

View file

@ -0,0 +1,60 @@
package xyz.ineanto.nicko.gui.items.home;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.ClickType;
import xyz.ineanto.nicko.Nicko;
import xyz.ineanto.nicko.appearance.ActionResult;
import xyz.ineanto.nicko.appearance.AppearanceManager;
import xyz.ineanto.nicko.language.PlayerLanguage;
import xyz.ineanto.nicko.language.LanguageKey;
import xyz.ineanto.nicko.profile.NickoProfile;
import xyz.xenondevs.invui.item.builder.SkullBuilder;
import xyz.xenondevs.invui.item.impl.SuppliedItem;
import java.util.Optional;
public class RandomSkinItem {
private final PlayerLanguage playerLanguage;
private final Nicko instance;
public RandomSkinItem(Player player) {
this.instance = Nicko.getInstance();
this.playerLanguage = new PlayerLanguage(player);
}
public SuppliedItem get() {
return new SuppliedItem(() -> {
final SkullBuilder.HeadTexture texture = new SkullBuilder.HeadTexture("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYzgzMTEzOGMyMDYxMWQzMDJjNDIzZmEzMjM3MWE3NDNkMTc0MzdhMTg5NzNjMzUxOTczNDQ3MGE3YWJiNCJ9fX0=");
final SkullBuilder builder = new SkullBuilder(texture);
return playerLanguage.translateItem(builder, LanguageKey.GUI.Home.RANDOM_SKIN);
}, (event) -> {
final Player player = event.getPlayer();
final ClickType clickType = event.getClickType();
if (clickType.isLeftClick() || clickType.isRightClick()) {
final Optional<NickoProfile> optionalProfile = NickoProfile.get(player);
optionalProfile.ifPresent(profile -> {
final String name = instance.getNameFetcher().getRandomUsername();
final String skin = instance.getNameFetcher().getRandomUsername();
profile.setName(name);
profile.setSkin(skin);
instance.getDataStore().updateCache(player.getUniqueId(), profile);
final AppearanceManager appearanceManager = new AppearanceManager(player);
final ActionResult result = appearanceManager.updatePlayer(true, false);
if (!result.isError()) {
player.sendMessage(playerLanguage.translate(LanguageKey.Event.Appearance.Set.OK, true));
} else {
player.sendMessage(playerLanguage.translate(
LanguageKey.Event.Appearance.Set.ERROR,
true,
playerLanguage.translate(result.getErrorKey(), false)
)
);
}
});
return true;
}
return false;
});
}
}

View file

@ -0,0 +1,50 @@
package xyz.ineanto.nicko.gui.items.home;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.ClickType;
import xyz.ineanto.nicko.appearance.AppearanceManager;
import xyz.ineanto.nicko.language.PlayerLanguage;
import xyz.ineanto.nicko.language.LanguageKey;
import xyz.ineanto.nicko.profile.NickoProfile;
import xyz.xenondevs.invui.item.builder.ItemBuilder;
import xyz.xenondevs.invui.item.impl.SuppliedItem;
import java.util.Optional;
public class ResetItem {
private final PlayerLanguage playerLanguage;
public ResetItem(Player player) {
this.playerLanguage = new PlayerLanguage(player);
}
public SuppliedItem get() {
return new SuppliedItem(() -> {
final ItemBuilder builder = new ItemBuilder(Material.TNT);
return playerLanguage.translateItem(builder, LanguageKey.GUI.Home.RESET);
}, (event) -> {
final Player player = event.getPlayer();
final ClickType clickType = event.getClickType();
if (clickType.isLeftClick() || clickType.isRightClick()) {
final Optional<NickoProfile> optionalProfile = NickoProfile.get(player);
optionalProfile.ifPresent(profile -> {
if (!profile.hasData()) {
player.sendMessage(playerLanguage.translateWithOops(LanguageKey.Event.Appearance.Remove.MISSING));
event.getEvent().getView().close();
return;
}
final AppearanceManager appearanceManager = new AppearanceManager(player);
if (!appearanceManager.reset().isError()) {
player.sendMessage(playerLanguage.translateWithWhoosh(LanguageKey.Event.Appearance.Remove.OK));
} else {
player.sendMessage(playerLanguage.translateWithOops(LanguageKey.Event.Appearance.Remove.ERROR));
}
});
return true;
}
return false;
});
}
}

View file

@ -0,0 +1,33 @@
package xyz.ineanto.nicko.gui.items.home;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.ClickType;
import xyz.ineanto.nicko.gui.SettingsGUI;
import xyz.ineanto.nicko.language.PlayerLanguage;
import xyz.ineanto.nicko.language.LanguageKey;
import xyz.xenondevs.invui.item.builder.ItemBuilder;
import xyz.xenondevs.invui.item.impl.SuppliedItem;
public class SettingsAccessItem {
private final PlayerLanguage playerLanguage;
public SettingsAccessItem(Player player) {
this.playerLanguage = new PlayerLanguage(player);
}
public SuppliedItem get() {
return new SuppliedItem(() -> {
final ItemBuilder builder = new ItemBuilder(Material.COMPARATOR);
return playerLanguage.translateItem(builder, LanguageKey.GUI.Home.SETTINGS);
}, 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,94 @@
package xyz.ineanto.nicko.gui.items.settings;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import org.bukkit.Material;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
import xyz.ineanto.nicko.Nicko;
import xyz.ineanto.nicko.gui.SettingsGUI;
import xyz.ineanto.nicko.language.Language;
import xyz.ineanto.nicko.language.PlayerLanguage;
import xyz.ineanto.nicko.language.LanguageKey;
import xyz.ineanto.nicko.language.Translation;
import xyz.ineanto.nicko.profile.NickoProfile;
import xyz.ineanto.nicko.storage.PlayerDataStore;
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 LanguageCyclingItem {
private final Player player;
private final ItemProvider[] providers;
private final PlayerLanguage playerLanguage;
public LanguageCyclingItem(Player player) {
this.player = player;
this.playerLanguage = new PlayerLanguage(player);
this.providers = getItems();
}
public AbstractItem get() {
final PlayerDataStore dataStore = Nicko.getInstance().getDataStore();
final Optional<NickoProfile> profile = dataStore.getData(player.getUniqueId());
if (profile.isPresent()) {
final NickoProfile nickoProfile = profile.get();
int localeOrdinal = nickoProfile.getLocale().ordinal();
return CycleItem.withStateChangeHandler((observer, integer) -> {
observer.playSound(player, Sound.UI_BUTTON_CLICK, 1f, 0.707107f); // 0.707107 ~= C
nickoProfile.setLocale(Language.values()[integer]);
player.getOpenInventory().close();
if (dataStore.updateCache(player.getUniqueId(), nickoProfile).isError()) {
player.sendMessage(playerLanguage.translate(LanguageKey.Event.Settings.ERROR, true));
} else {
new SettingsGUI(player).open();
}
}, localeOrdinal, providers);
}
return new SimpleItem(ItemProvider.EMPTY);
}
private ItemProvider generateItem(Language language, List<Language> languages) {
final ItemBuilder builder = new ItemBuilder(Material.OAK_SIGN);
final Translation translation = playerLanguage.translateAndReplace(LanguageKey.GUI.Settings.LANGUAGE);
final Translation cyclingChoicesTranslation = playerLanguage.translateAndReplace(LanguageKey.GUI.Settings.CYCLING_CHOICES);
builder.setDisplayName(Component.text(translation.name()).content());
for (Language value : languages) {
if (language != value) {
builder.addLoreLines("§7> " + value.getName());
} else {
builder.addLoreLines("§6§l> §f" + value.getName());
}
}
cyclingChoicesTranslation.lore().replaceAll(s -> {
final Component deserializedLoreLine = MiniMessage.miniMessage().deserialize(s);
return LegacyComponentSerializer.legacySection().serialize(deserializedLoreLine);
});
cyclingChoicesTranslation.lore().forEach(builder::addLoreLines);
return builder;
}
private ItemProvider[] getItems() {
final Nicko instance = Nicko.getInstance();
final ArrayList<ItemProvider> items = new ArrayList<>();
final ArrayList<Language> localesToGenerate = new ArrayList<>();
Collections.addAll(localesToGenerate, Language.values());
if (!instance.getNickoConfig().isCustomLocale()) {
localesToGenerate.remove(Language.CUSTOM);
}
localesToGenerate.forEach(locale -> items.add(generateItem(locale, localesToGenerate)));
return items.toArray(new ItemProvider[]{});
}
}

View file

@ -0,0 +1,75 @@
package xyz.ineanto.nicko.gui.items.settings;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
import xyz.ineanto.nicko.Nicko;
import xyz.ineanto.nicko.language.PlayerLanguage;
import xyz.ineanto.nicko.language.LanguageKey;
import xyz.ineanto.nicko.language.Translation;
import xyz.ineanto.nicko.profile.NickoProfile;
import xyz.ineanto.nicko.storage.PlayerDataStore;
import xyz.xenondevs.invui.item.ItemProvider;
import xyz.xenondevs.invui.item.builder.SkullBuilder;
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 RandomSkinCyclingItem {
private final Player player;
private final ItemProvider[] providers;
private final PlayerLanguage playerLanguage;
public RandomSkinCyclingItem(Player player) {
this.player = player;
this.playerLanguage = new PlayerLanguage(player);
this.providers = new ItemProvider[]{
getItemProviderForValue(true),
getItemProviderForValue(false)
};
}
public AbstractItem get() {
final PlayerDataStore dataStore = Nicko.getInstance().getDataStore();
final Optional<NickoProfile> profile = dataStore.getData(player.getUniqueId());
if (profile.isPresent()) {
final NickoProfile nickoProfile = profile.get();
int localeOrdinal = nickoProfile.isRandomSkin() ? 0 : 1;
return CycleItem.withStateChangeHandler((observer, integer) -> {
observer.playSound(player, Sound.UI_BUTTON_CLICK, 1f, 0.707107f); // 0.707107 ~= C
nickoProfile.setRandomSkin(integer != 1);
if (dataStore.updateCache(player.getUniqueId(), nickoProfile).isError()) {
player.sendMessage(playerLanguage.translate(LanguageKey.Event.Settings.ERROR, true));
player.getOpenInventory().close();
}
}, localeOrdinal, providers);
}
return new SimpleItem(ItemProvider.EMPTY);
}
private ItemProvider getItemProviderForValue(boolean enabled) {
final SkullBuilder.HeadTexture texture = new SkullBuilder.HeadTexture("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYzgzMTEzOGMyMDYxMWQzMDJjNDIzZmEzMjM3MWE3NDNkMTc0MzdhMTg5NzNjMzUxOTczNDQ3MGE3YWJiNCJ9fX0=");
final SkullBuilder builder = new SkullBuilder(texture);
final Translation randomSkinTranslation = playerLanguage.translateAndReplace(LanguageKey.GUI.Settings.RANDOM_SKIN);
final Translation toggleableTranslation = playerLanguage.translateAndReplace(LanguageKey.GUI.Settings.TOGGLEABLE_BUTTON,
(enabled ? "§7>§c" : "§6§l>§c§l"),
(enabled ? "§6§l>§a§l" : "§7>§a")
);
final Translation cyclingChoicesTranslation = playerLanguage.translateAndReplace(LanguageKey.GUI.Settings.CYCLING_CHOICES);
builder.setDisplayName(randomSkinTranslation.name());
toggleableTranslation.lore().forEach(builder::addLoreLines);
cyclingChoicesTranslation.lore().replaceAll(s -> {
final Component deserializedLoreLine = MiniMessage.miniMessage().deserialize(s);
return LegacyComponentSerializer.legacySection().serialize(deserializedLoreLine);
});
cyclingChoicesTranslation.lore().forEach(builder::addLoreLines);
return builder;
}
}

View file

@ -0,0 +1,61 @@
package xyz.ineanto.nicko.language;
import com.github.jsixface.YamlConfig;
import xyz.ineanto.nicko.Nicko;
import xyz.ineanto.nicko.version.Version;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
public class CustomLanguage {
private static final File directory = new File(Nicko.getInstance().getDataFolder(), "/locale/");
private static final File file = new File(directory, "locale.yml");
private final String version;
private final Version versionObject;
private final YamlConfig yamlFile;
public CustomLanguage() throws IOException {
this.yamlFile = new YamlConfig(new FileInputStream(file));
this.version = yamlFile.getString("version");
this.versionObject = Version.fromString(version);
}
public static void dumpIntoFile(Language language) throws IOException {
if (language == Language.CUSTOM) return;
if (file.exists()) return;
if (!directory.exists()) directory.mkdirs();
final String localeFileName = language.getCode() + ".yml";
try {
final InputStream resource = Nicko.getInstance().getResource(localeFileName);
Files.copy(resource, file.toPath());
resource.close();
} catch (IOException e) {
Nicko.getInstance().getLogger().severe("Unable to dump Locale: " + language.getCode() + "!");
}
}
public String getVersion() {
return version;
}
public Version getVersionObject() {
return versionObject;
}
public YamlConfig getYamlFile() {
return yamlFile;
}
public File getDirectory() {
return directory;
}
public File getFile() {
return file;
}
}

View file

@ -0,0 +1,36 @@
package xyz.ineanto.nicko.language;
import xyz.ineanto.nicko.version.Version;
import java.io.Serializable;
public enum Language implements Serializable {
ENGLISH("en", "English"),
FRENCH("fr", "Français"),
CUSTOM("cm", "Server Custom");
public static final Version VERSION = new Version(1, 1, 4);
private final String code;
private transient final String name;
Language(String code, String name) {
this.code = code;
this.name = name;
}
public static Language fromCode(String code) {
for (Language 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,142 @@
package xyz.ineanto.nicko.language;
public class LanguageKey {
public static final String PREFIX = "prefix";
public static final String WHOOSH = "whoosh";
public static final String OOPS = "oops";
public static class Error {
public static final String ERROR_KEY = "error.";
public static final String PERMISSION = ERROR_KEY + "permission";
public static final String CACHE = ERROR_KEY + "cache";
public static final String MOJANG_NAME = ERROR_KEY + "mojang_name";
public static final String MOJANG_SKIN = ERROR_KEY + "mojang_skin";
}
public static class Event {
private static final String EVENT_KEY = "event.";
public static class Admin {
private static final String ADMIN_KEY = EVENT_KEY + "admin.";
public static class Cache {
private static final String CACHE_KEY = ADMIN_KEY + "cache.";
public static final String INVALIDATE_CACHE = CACHE_KEY + "invalidate_cache";
public static final String INVALIDATE_ENTRY = CACHE_KEY + "invalidate_entry";
}
public static class Check {
private static final String CHECK_KEY = ADMIN_KEY + "check.";
public static final String REMOVE_SKIN = CHECK_KEY + "remove_skin";
}
}
public static class Settings {
private static final String SETTINGS_KEY = EVENT_KEY + "settings.";
public static final String ERROR = SETTINGS_KEY + "error";
}
public static class Appearance {
private static final String APPEARANCE_KEY = EVENT_KEY + "appearance.";
public static class Set {
public static final String SET_KEY = APPEARANCE_KEY + "set.";
public static final String OK = SET_KEY + "ok";
public static final String ERROR = SET_KEY + "error";
}
public static class Remove {
private static final String REMOVE_KEY = APPEARANCE_KEY + "remove.";
public static final String OK = REMOVE_KEY + "ok";
public static final String MISSING = REMOVE_KEY + "missing";
public static final String ERROR = REMOVE_KEY + "error";
}
public static class Restore {
private static final String RESTORE_KEY = APPEARANCE_KEY + "restore.";
public static final String OK = RESTORE_KEY + "ok";
public static final String ERROR = RESTORE_KEY + "error";
}
}
}
public static class GUI {
private static final String GUI_KEY = "gui.";
public static final String EXIT = GUI_KEY + "exit";
public static final String GO_BACK = GUI_KEY + "go_back";
public static final String UNAVAILABLE = GUI_KEY + "unavailable";
public static final String LOADING = GUI_KEY + "loading";
public static final String SCROLL_UP = GUI_KEY + "scroll_up";
public static final String SCROLL_DOWN = GUI_KEY + "scroll_down";
public static final String NEW_SKIN = GUI_KEY + "new_skin";
public static final String NEW_NAME = GUI_KEY + "new_name";
public static class Titles {
public static final String TITLE_KEY = GUI_KEY + "title.";
public static final String HOME = TITLE_KEY + "home";
public static final String SETTINGS = TITLE_KEY + "settings";
public static final String ADMIN = TITLE_KEY + "admin";
public static final String CHECK = TITLE_KEY + "check";
public static final String CONFIRM = TITLE_KEY + "confirm";
public static final String CACHE = TITLE_KEY + "cache";
public static final String INVALIDATE_SKIN = TITLE_KEY + "invalidate_skin";
}
public static class Choice {
private static final String CHOICE_KEY = GUI_KEY + "choice.";
public static final String CONFIRM = CHOICE_KEY + "confirm";
public static final String CHOOSE = CHOICE_KEY + "choose";
public static final String CANCEL = CHOICE_KEY + "cancel";
}
public static class Home {
private static final String HOME_KEY = GUI_KEY + "home.";
public static final String ADMIN = HOME_KEY + "admin";
public static final String CHANGE_NAME = HOME_KEY + "change_name";
public static final String CHANGE_SKIN = HOME_KEY + "change_skin";
public static final String CHANGE_BOTH = HOME_KEY + "change_both";
public static final String RESET = HOME_KEY + "reset";
public static final String RANDOM_SKIN = HOME_KEY + "random_skin";
public static final String SETTINGS = HOME_KEY + "settings";
}
public static class Settings {
private static final String SETTINGS_KEY = GUI_KEY + "settings.";
public static final String CYCLING_CHOICES = SETTINGS_KEY + "cycling_choices";
public static final String TOGGLEABLE_BUTTON = SETTINGS_KEY + "toggleable_button";
public static final String LANGUAGE = SETTINGS_KEY + "language";
public static final String RANDOM_SKIN = SETTINGS_KEY + "random_skin";
}
public static class Admin {
private static final String ADMIN_KEY = GUI_KEY + "admin.";
public static final String MANAGE_CACHE = ADMIN_KEY + "manage_cache";
public static final String MANAGE_PLAYER = ADMIN_KEY + "manage_player";
public static final String CHECK = ADMIN_KEY + "check";
public static class Cache {
private static final String CACHE_KEY = ADMIN_KEY + "cache.";
public static final String STATISTICS = CACHE_KEY + "statistics";
public static final String INVALIDATE_CACHE = CACHE_KEY + "invalidate_cache";
public static final String INVALIDATE_SKIN = CACHE_KEY + "invalidate_skin";
public static final String ENTRY = CACHE_KEY + "entry";
}
}
}
}

View file

@ -0,0 +1,181 @@
package xyz.ineanto.nicko.language;
import com.github.jsixface.YamlConfig;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import org.bukkit.entity.Player;
import xyz.ineanto.nicko.Nicko;
import xyz.ineanto.nicko.profile.NickoProfile;
import xyz.xenondevs.invui.item.builder.AbstractItemBuilder;
import java.io.InputStream;
import java.text.MessageFormat;
import java.util.*;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class PlayerLanguage {
private final MessageFormat formatter = new MessageFormat("");
private final Logger logger = Logger.getLogger("I18N");
private final Nicko instance = Nicko.getInstance();
private final Pattern replacementPattern = Pattern.compile("(?ms)\\{\\d+}");
private final YamlConfig yamlConfig;
private final Language playerLanguage;
public PlayerLanguage(Player player) {
final Optional<NickoProfile> optionalProfile = NickoProfile.get(player);
this.playerLanguage = optionalProfile.map(NickoProfile::getLocale).orElse(Language.ENGLISH);
this.yamlConfig = getYamlConfig();
}
public PlayerLanguage(Language language) {
this.playerLanguage = language;
this.yamlConfig = getYamlConfig();
}
public AbstractItemBuilder<?> translateItem(AbstractItemBuilder<?> item, String key, Object... args) {
final Translation translation = translateAndReplace(key, args);
// Name serialization
final Component deserializedName = MiniMessage.miniMessage().deserialize(translation.name());
final String serializedName = LegacyComponentSerializer.legacySection().serialize(deserializedName);
// Lore serialization
translation.lore().replaceAll(s -> {
final Component deserializedLoreLine = MiniMessage.miniMessage().deserialize(s);
return LegacyComponentSerializer.legacySection().serialize(deserializedLoreLine);
});
item.setDisplayName(serializedName);
translation.lore().forEach(item::addLoreLines);
return item;
}
public Translation translateAndReplace(String key, Object... args) {
final String nameKey = key + ".name";
final String loreKey = key + ".lore";
final String name = readString(nameKey);
final ArrayList<String> lore = readList(loreKey);
if (name == null && lore == null) {
Nicko.getInstance().getLogger().warning(nameKey + " doesn't exists! Is your language file outdated?");
return new Translation(nameKey, new ArrayList<>(List.of(loreKey)));
}
// Add all elements to a list
final ArrayList<String> toTranslate = new ArrayList<>();
if (name != null) {
toTranslate.add(name);
}
if (lore != null && !lore.isEmpty()) {
toTranslate.addAll(lore);
}
// Set starting index to 0
int lineIndex = 0;
int replacementIndex = 0;
// While iterator next value exists/isn't null
final Iterator<String> iterator = toTranslate.iterator();
while (iterator.hasNext() && iterator.next() != null) {
// Get the current line
final String currentLine = toTranslate.get(lineIndex);
// If the line doesn't contain {i}, skip it
final Matcher matcher = replacementPattern.matcher(currentLine);
if (!matcher.find()) {
lineIndex++;
continue;
}
// If it does, replace the content with the args at position replacementIndex
if (replacementIndex < args.length && args[replacementIndex] != null) {
// Replace it with the corresponding varargs index
toTranslate.set(lineIndex, currentLine.replace("{" + replacementIndex + "}", args[replacementIndex].toString()));
replacementIndex++;
}
// Increment the index
lineIndex++;
}
if (name == null && !lore.isEmpty()) {
// Empty name, valid lore
return new Translation(null, toTranslate);
} else if (name != null && (lore == null || lore.isEmpty())) {
// Valid name, empty lore
return new Translation(toTranslate.getFirst(), new ArrayList<>(Collections.emptyList()));
} else {
// Valid name, valid lore
return new Translation(toTranslate.getFirst(), new ArrayList<>(toTranslate.subList(1, toTranslate.size())));
}
}
public String translate(String key, boolean prefix, Object... arguments) {
final String translation = readStringWithMiniMessage(key);
try {
formatter.applyPattern(translation);
return (prefix ? getPrefix() + " " : "") + formatter.format(arguments);
} catch (Exception e) {
return (prefix ? getPrefix() + " " : "") + key;
}
}
public String translateWithWhoosh(String key, Object... arguments) {
final String translation = readStringWithMiniMessage(key);
try {
formatter.applyPattern(translation);
return getWhoosh() + " " + formatter.format(arguments);
} catch (Exception e) {
return getWhoosh() + " " + key;
}
}
public String translateWithOops(String key, Object... arguments) {
final String translation = readStringWithMiniMessage(key);
try {
formatter.applyPattern(translation);
return getOops() + " " + formatter.format(arguments);
} catch (Exception e) {
return getOops() + " " + key;
}
}
private String readString(String key) {
return yamlConfig.getString(key);
}
private String readStringWithMiniMessage(String key) {
return LegacyComponentSerializer.legacySection().serialize(MiniMessage.miniMessage().deserialize(readString(key)));
}
private ArrayList<String> readList(String key) {
return yamlConfig.getStringList(key);
}
private String getPrefix() {
return readStringWithMiniMessage(LanguageKey.PREFIX);
}
private String getWhoosh() {
return readStringWithMiniMessage(LanguageKey.WHOOSH);
}
private String getOops() {
return readStringWithMiniMessage(LanguageKey.OOPS);
}
private YamlConfig getYamlConfig() {
if (playerLanguage == Language.CUSTOM) {
return instance.getCustomLocale().getYamlFile();
} else {
final InputStream resource = instance.getResource(playerLanguage.getCode() + ".yml");
return new YamlConfig(resource);
}
}
}

View file

@ -0,0 +1,5 @@
package xyz.ineanto.nicko.language;
import java.util.ArrayList;
public record Translation(String name, ArrayList<String> lore) {}

View file

@ -0,0 +1,27 @@
package xyz.ineanto.nicko.mapping;
import org.bukkit.Bukkit;
import xyz.ineanto.nicko.mapping.v1_20.Mapping1_20;
import xyz.ineanto.nicko.mapping.v1_20_2.Mapping1_20_2;
import xyz.ineanto.nicko.mapping.v1_20_4.Mapping1_20_4;
import xyz.ineanto.nicko.mapping.v1_20_6.Mapping1_20_6;
import xyz.ineanto.nicko.mapping.v1_21.Mapping1_21;
import java.util.List;
import java.util.Optional;
public class MappingManager {
private final List<Mapping> mappings = List.of(
new Mapping1_20(),
new Mapping1_20_2(),
new Mapping1_20_4(),
new Mapping1_20_6(),
new Mapping1_21()
);
public Optional<Mapping> getMappingForServer() {
return mappings.stream()
.filter(mapping -> mapping.supportedVersions().contains(Bukkit.getMinecraftVersion()))
.findFirst();
}
}

View file

@ -0,0 +1,41 @@
package xyz.ineanto.nicko.migration;
import xyz.ineanto.nicko.Nicko;
import xyz.ineanto.nicko.config.Configuration;
import xyz.ineanto.nicko.config.ConfigurationManager;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
public class ConfigurationMigrator implements Migrator {
private final Nicko instance;
public ConfigurationMigrator(Nicko instance) {
this.instance = instance;
}
@Override
public void migrate() {
final Configuration configuration = instance.getNickoConfig();
final ConfigurationManager configurationManager = instance.getConfigurationManager();
// Migrate configuration (1.0.8-RC1)
if (configuration.getVersion() == null
|| configuration.getVersion().isEmpty()
|| configuration.getVersionObject().compareTo(Configuration.VERSION) != 0) {
instance.getLogger().info("Migrating configuration file to match the current version...");
try {
Files.copy(configurationManager.getFile().toPath(), configurationManager.getBackupFile().toPath(), StandardCopyOption.REPLACE_EXISTING);
if (configurationManager.getFile().delete()) {
configurationManager.saveDefaultConfig();
instance.getLogger().info("Successfully migrated your configuration file!");
} else {
instance.getLogger().severe("Failed to migrate your configuration!");
}
} catch (IOException e) {
instance.getLogger().severe("Failed to migrate your configuration!");
}
}
}
}

View file

@ -0,0 +1,47 @@
package xyz.ineanto.nicko.migration;
import xyz.ineanto.nicko.Nicko;
import xyz.ineanto.nicko.language.CustomLanguage;
import xyz.ineanto.nicko.language.Language;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
public class CustomLocaleMigrator implements Migrator {
private final Nicko instance;
private final CustomLanguage customLanguage;
public CustomLocaleMigrator(Nicko instance, CustomLanguage customLanguage) {
this.instance = instance;
this.customLanguage = customLanguage;
}
@Override
public void migrate() {
// Migrate custom locale (1.1.0-RC1)
if (customLanguage.getVersionObject() == null
|| customLanguage.getVersion().isEmpty()
|| customLanguage.getVersionObject().compareTo(Language.VERSION) != 0) {
instance.getLogger().info("Migrating the custom locale (" + customLanguage.getVersion() + ") to match the current version (" + Language.VERSION + ")...");
final String date = Instant.now().atZone(ZoneId.systemDefault()).format(DateTimeFormatter.ofPattern("dd-MM-yyyy"));
final File backupFile = new File(customLanguage.getDirectory(), "locale-" + date + ".yml");
try {
Files.copy(customLanguage.getFile().toPath(), backupFile.toPath());
if (customLanguage.getFile().delete()) {
CustomLanguage.dumpIntoFile(Language.ENGLISH);
instance.getLogger().info("Successfully migrated the custom locale.");
} else {
instance.getLogger().severe("Failed to migrate the custom locale!");
}
} catch (IOException e) {
instance.getLogger().severe("Failed to migrate the custom locale!");
}
}
}
}

View file

@ -0,0 +1,5 @@
package xyz.ineanto.nicko.migration;
public interface Migrator {
void migrate();
}

View file

@ -0,0 +1,155 @@
package xyz.ineanto.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.URI;
import java.net.URL;
import java.util.HashMap;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
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 HashMap<String, String> uuidToName = new HashMap<>();
private final ExecutorService worker = Executors.newFixedThreadPool(6);
private final CacheLoader<String, Optional<MojangSkin>> skinLoader = new CacheLoader<>() {
@Nonnull
public Optional<MojangSkin> load(@Nonnull String uuid) throws Exception {
return getSkinFromMojang(uuid);
}
};
private final LoadingCache<String, Optional<MojangSkin>> skinCache = CacheBuilder
.newBuilder()
.recordStats()
.expireAfterWrite(24, TimeUnit.HOURS)
.build(skinLoader);
private final CacheLoader<String, Optional<String>> uuidLoader = new CacheLoader<>() {
@Nonnull
public Optional<String> load(@Nonnull String name) throws Exception {
return getUUIDFromMojang(name);
}
};
private final LoadingCache<String, Optional<String>> uuidCache = CacheBuilder
.newBuilder()
.expireAfterWrite(2, TimeUnit.DAYS)
.build(uuidLoader);
public Optional<MojangSkin> getSkin(String uuid) throws IOException, ExecutionException {
return skinCache.get(uuid);
}
public Optional<MojangSkin> getSkinWithoutCaching(String uuid) throws IOException, ExecutionException, InterruptedException {
return getSkinFromMojang(uuid);
}
public Optional<String> getUUID(String name) throws IOException, ExecutionException {
return uuidCache.get(name);
}
private Optional<String> getUUIDFromMojang(String name) throws ExecutionException, InterruptedException {
final String parametrizedUrl = URL_NAME.replace("{name}", name);
final JsonObject object = getRequestToUrl(parametrizedUrl);
if (hasNoError(object)) {
final JsonElement idObject = object.get("id");
final String uuid = idObject.getAsString();
final Optional<String> uuidOptional = Optional.of(uuid);
uuidCache.put(name, uuidOptional);
uuidToName.put(uuid, name);
return uuidOptional;
}
return Optional.empty();
}
public void eraseFromCache(String uuid) {
skinCache.invalidate(uuid);
uuidToName.remove(uuid);
uuidCache.invalidate(uuid);
}
private Optional<MojangSkin> getSkinFromMojang(String uuid) throws ExecutionException, InterruptedException {
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();
}
public String getUUIDName(String uuid) {
return uuidToName.get(uuid);
}
private JsonObject getRequestToUrl(String parametrizedUrl) throws ExecutionException, InterruptedException {
return worker.submit(() -> {
final URL url = URI.create(parametrizedUrl).toURL();
final HttpsURLConnection con = (HttpsURLConnection) url.openConnection();
con.setDoInput(true);
con.setRequestMethod("GET");
switch (con.getResponseCode()) {
case 404:
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();
}
}).get();
}
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>> getSkinCache() {
return skinCache;
}
}

View file

@ -0,0 +1,12 @@
package xyz.ineanto.nicko.mojang;
import com.google.gson.JsonObject;
public record MojangSkin(String value, String 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);
}
}

View file

@ -0,0 +1,26 @@
package xyz.ineanto.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,75 @@
package xyz.ineanto.nicko.placeholder;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.ineanto.nicko.Nicko;
import xyz.ineanto.nicko.profile.NickoProfile;
import java.util.Optional;
public class NickoExpansion extends PlaceholderExpansion {
private final Nicko instance;
public NickoExpansion(Nicko instance) {
this.instance = instance;
}
@Override
public @NotNull String getIdentifier() {
return "xyz/ineanto/nicko";
}
@Override
public @NotNull String getAuthor() {
return "Ineanto";
}
@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 randomSkin;
name = skin = player.getName();
locale = "N/A";
randomSkin = false;
final Optional<NickoProfile> optionalProfile = instance.getDataStore().getData(player.getUniqueId());
if (optionalProfile.isPresent()) {
final NickoProfile profile = optionalProfile.get();
if (profile.hasData()) {
if (profile.getName() != null) {
name = profile.getName();
}
if (profile.getSkin() != null) {
skin = profile.getSkin();
}
}
locale = profile.getLocale().getName();
randomSkin = profile.isRandomSkin();
}
return switch (params) {
case "name" -> name;
case "skin" -> skin;
case "locale" -> locale;
case "random_skin" -> String.valueOf(randomSkin);
default -> null;
};
}
}

View file

@ -0,0 +1,91 @@
package xyz.ineanto.nicko.profile;
import org.bukkit.entity.Player;
import xyz.ineanto.nicko.Nicko;
import xyz.ineanto.nicko.language.Language;
import xyz.ineanto.nicko.storage.PlayerDataStore;
import java.util.Optional;
import java.util.UUID;
public class NickoProfile implements Cloneable {
public static final PlayerDataStore dataStore = Nicko.getInstance().getDataStore();
public static final NickoProfile EMPTY_PROFILE = new NickoProfile(null, null, Language.ENGLISH, true);
private String name;
private String skin;
private Language language;
private boolean randomSkin;
public NickoProfile(String name, String skin, Language language, boolean randomSkin) {
this.name = name;
this.skin = skin;
this.language = language;
this.randomSkin = randomSkin;
}
public static Optional<NickoProfile> get(Player player) {
return dataStore.getData(player.getUniqueId());
}
public static Optional<NickoProfile> get(UUID uuid) {
return dataStore.getData(uuid);
}
public boolean hasData() {
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 Language getLocale() {
return language;
}
public void setLocale(Language language) {
this.language = language;
}
public boolean isRandomSkin() {
return randomSkin;
}
public void setRandomSkin(boolean randomSkin) {
this.randomSkin = randomSkin;
}
@Override
public String toString() {
return "NickoProfile{" +
"name='" + name + '\'' +
", skin='" + skin + '\'' +
", locale=" + language +
", randomSkin=" + randomSkin +
'}';
}
@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,29 @@
package xyz.ineanto.nicko.storage;
import xyz.ineanto.nicko.appearance.ActionResult;
import xyz.ineanto.nicko.profile.NickoProfile;
import java.util.Optional;
import java.util.UUID;
public abstract class Cache {
private boolean error = false;
public abstract CacheProvider getProvider();
public abstract ActionResult cache(UUID uuid, NickoProfile profile);
public abstract boolean isCached(UUID uuid);
public abstract Optional<NickoProfile> retrieve(UUID uuid);
public abstract ActionResult delete(UUID uuid);
public boolean isError() {
return error;
}
public void setError(boolean error) {
this.error = error;
}
}

View file

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

View file

@ -0,0 +1,106 @@
package xyz.ineanto.nicko.storage;
import org.bukkit.entity.Player;
import xyz.ineanto.nicko.appearance.ActionResult;
import xyz.ineanto.nicko.config.Configuration;
import xyz.ineanto.nicko.language.LanguageKey;
import xyz.ineanto.nicko.mojang.MojangAPI;
import xyz.ineanto.nicko.mojang.MojangUtils;
import xyz.ineanto.nicko.profile.NickoProfile;
import xyz.ineanto.nicko.storage.json.JSONStorage;
import xyz.ineanto.nicko.storage.map.MapCache;
import xyz.ineanto.nicko.storage.mariadb.MariaDBStorage;
import xyz.ineanto.nicko.storage.mysql.MySQLStorage;
import xyz.ineanto.nicko.storage.redis.RedisCache;
import java.io.IOException;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
public class PlayerDataStore {
private final MojangAPI mojangAPI;
private Storage storage;
private Cache cache;
public PlayerDataStore(MojangAPI mojangAPI, Configuration configuration) {
this.mojangAPI = mojangAPI;
this.storage = configuration.getSqlConfiguration().isEnabled() ?
configuration.getSqlConfiguration().isMariadb() ? new MariaDBStorage(configuration) : new MySQLStorage(configuration)
: new JSONStorage();
this.cache = configuration.getRedisConfiguration().isEnabled() ? new RedisCache(configuration) : new MapCache();
}
public ActionResult updateCache(UUID uuid, NickoProfile profile) {
if (storage.isError() || cache.isError()) {
return ActionResult.error(LanguageKey.Error.CACHE);
}
getCache().cache(uuid, profile);
return ActionResult.ok();
}
public Optional<NickoProfile> getData(UUID uuid) {
if (storage.isError() || cache.isError()) {
return Optional.empty();
}
if (cache.isCached(uuid)) {
return cache.retrieve(uuid);
} else if (storage.isStored(uuid)) {
final Optional<NickoProfile> retrievedProfile = storage.retrieve(uuid);
retrievedProfile.ifPresent(profile -> cache.cache(uuid, profile));
return retrievedProfile;
} else {
final NickoProfile newProfile = NickoProfile.EMPTY_PROFILE.clone();
cache.cache(uuid, newProfile);
return Optional.of(newProfile);
}
}
public Optional<NickoProfile> getOfflineData(String name) {
if (storage.isError() || cache.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 | ExecutionException e) {
return Optional.empty();
}
}
public ActionResult saveData(Player player) {
if (storage.isError()) return ActionResult.error();
if (cache.isError()) return ActionResult.error();
if (!cache.isCached(player.getUniqueId())) return ActionResult.error();
final Optional<NickoProfile> cachedProfile = cache.retrieve(player.getUniqueId());
if (cachedProfile.isEmpty()) return ActionResult.error();
cache.delete(player.getUniqueId());
return storage.store(player.getUniqueId(), cachedProfile.get());
}
public Storage getStorage() {
return storage;
}
public void setStorage(Storage storage) {
this.storage = storage;
}
public Cache getCache() {
return cache;
}
public void setCache(Cache cache) {
this.cache = cache;
}
}

View file

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

View file

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

View file

@ -0,0 +1,92 @@
package xyz.ineanto.nicko.storage.json;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import xyz.ineanto.nicko.Nicko;
import xyz.ineanto.nicko.appearance.ActionResult;
import xyz.ineanto.nicko.profile.NickoProfile;
import xyz.ineanto.nicko.storage.Storage;
import xyz.ineanto.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(Nicko.getInstance().getDataFolder() + "/players/");
private JSONStorageProvider provider;
@Override
public StorageProvider getProvider() {
if (provider == null) {
provider = new JSONStorageProvider(directory);
}
return provider;
}
@Override
public ActionResult 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 ActionResult.error();
}
}
} catch (IOException e) {
logger.warning("Could not create file.");
return ActionResult.error();
}
return ActionResult.ok();
}
@Override
public boolean isStored(UUID uuid) {
final File directory = new File(Nicko.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(Nicko.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();
}
}
@Override
public ActionResult delete(UUID uuid) {
final File directory = new File(Nicko.getInstance().getDataFolder() + "/players/");
final File file = new File(directory, uuid.toString() + ".json");
if (file.delete() || !file.exists()) {
return ActionResult.ok();
}
return ActionResult.error();
}
private boolean checkFileExists(File file) throws IOException {
if (!file.exists()) {
return file.createNewFile();
}
return true;
}
}

View file

@ -0,0 +1,22 @@
package xyz.ineanto.nicko.storage.json;
import xyz.ineanto.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,51 @@
package xyz.ineanto.nicko.storage.map;
import xyz.ineanto.nicko.appearance.ActionResult;
import xyz.ineanto.nicko.profile.NickoProfile;
import xyz.ineanto.nicko.storage.Cache;
import xyz.ineanto.nicko.storage.CacheProvider;
import java.util.HashMap;
import java.util.Optional;
import java.util.UUID;
public class MapCache extends Cache {
private MapCacheProvider provider;
@Override
public CacheProvider getProvider() {
if (provider == null) {
provider = new MapCacheProvider();
}
return provider;
}
@Override
public ActionResult cache(UUID uuid, NickoProfile profile) {
final HashMap<UUID, NickoProfile> profiles = provider.getMap();
profiles.put(uuid, profile);
return ActionResult.ok();
}
@Override
public boolean isCached(UUID uuid) {
final HashMap<UUID, NickoProfile> profiles = provider.getMap();
return profiles.containsKey(uuid);
}
@Override
public Optional<NickoProfile> retrieve(UUID uuid) {
final HashMap<UUID, NickoProfile> profiles = provider.getMap();
if (isCached(uuid)) {
return Optional.of(profiles.get(uuid));
}
return Optional.empty();
}
@Override
public ActionResult delete(UUID uuid) {
final HashMap<UUID, NickoProfile> profiles = provider.getMap();
profiles.remove(uuid);
return ActionResult.ok();
}
}

View file

@ -0,0 +1,28 @@
package xyz.ineanto.nicko.storage.map;
import xyz.ineanto.nicko.profile.NickoProfile;
import xyz.ineanto.nicko.storage.CacheProvider;
import java.util.HashMap;
import java.util.UUID;
public class MapCacheProvider implements CacheProvider {
private HashMap<UUID, NickoProfile> profiles;
@Override
public boolean init() {
if (profiles == null) {
profiles = new HashMap<>();
}
return true;
}
@Override
public boolean close() {
return true;
}
public HashMap<UUID, NickoProfile> getMap() {
return profiles;
}
}

View file

@ -0,0 +1,140 @@
package xyz.ineanto.nicko.storage.mariadb;
import xyz.ineanto.nicko.appearance.ActionResult;
import xyz.ineanto.nicko.config.Configuration;
import xyz.ineanto.nicko.language.Language;
import xyz.ineanto.nicko.profile.NickoProfile;
import xyz.ineanto.nicko.storage.Storage;
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 MariaDBStorage extends Storage {
private final Logger logger = Logger.getLogger("SQLStorage");
private final Configuration configuration;
private MariaDBStorageProvider provider;
public MariaDBStorage(Configuration configuration) {
this.configuration = configuration;
}
@Override
public MariaDBStorageProvider getProvider() {
if (provider == null) {
provider = new MariaDBStorageProvider(configuration);
}
return provider;
}
@Override
public ActionResult store(UUID uuid, NickoProfile profile) {
final Connection connection = getProvider().getConnection();
if (connection == null) return ActionResult.error();
try {
final PreparedStatement statement = isStored(uuid) ?
getUpdateStatement(connection, uuid, profile) : getInsertStatement(connection, uuid, profile);
statement.executeUpdate();
return ActionResult.ok();
} catch (SQLException e) {
logger.warning("Couldn't send SQL Request: " + e.getMessage());
return ActionResult.error();
}
}
@Override
public boolean isStored(UUID uuid) {
final Connection connection = getProvider().getConnection();
if (connection == null) return false;
try {
final String sql = "SELECT uuid FROM nicko.DATA WHERE uuid = ?";
final PreparedStatement statement = connection.prepareStatement(sql);
statement.setString(1, uuid.toString());
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();
if (!isStored(uuid)) return Optional.empty();
try {
final String sql = "SELECT * FROM nicko.DATA WHERE uuid = ?";
final PreparedStatement statement = connection.prepareStatement(sql);
statement.setString(1, uuid.toString());
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, Language.fromCode(locale), bungeecord);
return Optional.of(profile);
} catch (SQLException e) {
logger.warning("Couldn't fetch profile: " + e.getMessage());
return Optional.empty();
}
}
@Override
public ActionResult delete(UUID uuid) {
final Connection connection = getProvider().getConnection();
if (connection == null) return ActionResult.error();
try {
final String sql = "DELETE FROM nicko.DATA WHERE uuid = ?";
final PreparedStatement statement = connection.prepareStatement(sql);
statement.setString(1, uuid.toString());
int rows = statement.executeUpdate();
return (rows == 1 ? ActionResult.ok() : ActionResult.error());
} catch (SQLException e) {
logger.warning("Couldn't delete profile: " + e.getMessage());
return ActionResult.error();
}
}
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.setString(1, uuid.toString());
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.isRandomSkin());
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.isRandomSkin());
statement.setString(5, uuid.toString());
return statement;
}
}

View file

@ -0,0 +1,84 @@
package xyz.ineanto.nicko.storage.mariadb;
import org.mariadb.jdbc.MariaDbDataSource;
import xyz.ineanto.nicko.config.Configuration;
import xyz.ineanto.nicko.config.DataSourceConfiguration;
import xyz.ineanto.nicko.storage.StorageProvider;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.logging.Logger;
public class MariaDBStorageProvider implements StorageProvider {
private final Logger logger = Logger.getLogger("MariaDBStorageProvider");
private final Configuration configuration;
private Connection connection;
private final String schemaName = "xyz/ineanto/nicko";
public MariaDBStorageProvider(Configuration configuration) {
this.configuration = configuration;
}
@Override
public boolean init() {
try {
final MariaDbDataSource dataSource = new MariaDbDataSource();
final DataSourceConfiguration sqlConfiguration = configuration.getSqlConfiguration();
dataSource.setUrl("jdbc:mariadb://" + sqlConfiguration.getAddress() + ":" + sqlConfiguration.getPort());
dataSource.setUser(sqlConfiguration.getUsername());
dataSource.setPassword(sqlConfiguration.getPassword());
connection = dataSource.getConnection();
connection.setAutoCommit(true);
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 MariaDB 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();
final String query = "CREATE TABLE IF NOT EXISTS %s.DATA ".replace("%s", schemaName) +
"(uuid varchar(36) NOT NULL," +
"name varchar(16)," +
"skin varchar(16)," +
"locale char(2) NOT NULL," +
"bungeecord boolean NOT NULL," +
"PRIMARY KEY (uuid))";
final PreparedStatement statement = connection.prepareStatement(query);
statement.executeUpdate();
}
private void createDatabase() throws SQLException {
final Connection connection = getConnection();
final String query = "CREATE DATABASE IF NOT EXISTS %s".replace("%s", schemaName);
final PreparedStatement statement = connection.prepareStatement(query);
statement.executeUpdate();
}
public Connection getConnection() {
return connection;
}
}

View file

@ -0,0 +1,140 @@
package xyz.ineanto.nicko.storage.mysql;
import xyz.ineanto.nicko.appearance.ActionResult;
import xyz.ineanto.nicko.config.Configuration;
import xyz.ineanto.nicko.language.Language;
import xyz.ineanto.nicko.profile.NickoProfile;
import xyz.ineanto.nicko.storage.Storage;
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 MySQLStorage extends Storage {
private final Logger logger = Logger.getLogger("SQLStorage");
private final Configuration configuration;
private MySQLStorageProvider provider;
public MySQLStorage(Configuration configuration) {
this.configuration = configuration;
}
@Override
public MySQLStorageProvider getProvider() {
if (provider == null) {
provider = new MySQLStorageProvider(configuration);
}
return provider;
}
@Override
public ActionResult store(UUID uuid, NickoProfile profile) {
final Connection connection = getProvider().getConnection();
if (connection == null) return ActionResult.error();
try {
final PreparedStatement statement = isStored(uuid) ?
getUpdateStatement(connection, uuid, profile) : getInsertStatement(connection, uuid, profile);
statement.executeUpdate();
return ActionResult.ok();
} catch (SQLException e) {
logger.warning("Couldn't send SQL Request: " + e.getMessage());
return ActionResult.error();
}
}
@Override
public boolean isStored(UUID uuid) {
final Connection connection = getProvider().getConnection();
if (connection == null) return false;
try {
final String sql = "SELECT uuid FROM nicko.DATA WHERE uuid = ?";
final PreparedStatement statement = connection.prepareStatement(sql);
statement.setString(1, uuid.toString());
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();
if (!isStored(uuid)) return Optional.empty();
try {
final String sql = "SELECT * FROM nicko.DATA WHERE uuid = ?";
final PreparedStatement statement = connection.prepareStatement(sql);
statement.setString(1, uuid.toString());
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, Language.fromCode(locale), bungeecord);
return Optional.of(profile);
} catch (SQLException e) {
logger.warning("Couldn't fetch profile: " + e.getMessage());
return Optional.empty();
}
}
@Override
public ActionResult delete(UUID uuid) {
final Connection connection = getProvider().getConnection();
if (connection == null) return ActionResult.error();
try {
final String sql = "DELETE FROM nicko.DATA WHERE uuid = ?";
final PreparedStatement statement = connection.prepareStatement(sql);
statement.setString(1, uuid.toString());
int rows = statement.executeUpdate();
return (rows == 1 ? ActionResult.ok() : ActionResult.error());
} catch (SQLException e) {
logger.warning("Couldn't delete profile: " + e.getMessage());
return ActionResult.error();
}
}
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.setString(1, uuid.toString());
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.isRandomSkin());
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.isRandomSkin());
statement.setString(5, uuid.toString());
return statement;
}
}

View file

@ -0,0 +1,84 @@
package xyz.ineanto.nicko.storage.mysql;
import com.mysql.cj.jdbc.MysqlDataSource;
import xyz.ineanto.nicko.config.Configuration;
import xyz.ineanto.nicko.config.DataSourceConfiguration;
import xyz.ineanto.nicko.storage.StorageProvider;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.logging.Logger;
public class MySQLStorageProvider implements StorageProvider {
private final Logger logger = Logger.getLogger("MySQLStorageProvider");
private final Configuration configuration;
private Connection connection;
private final String schemaName = "xyz/ineanto/nicko";
public MySQLStorageProvider(Configuration configuration) {
this.configuration = configuration;
}
@Override
public boolean init() {
try {
final MysqlDataSource dataSource = new MysqlDataSource();
final DataSourceConfiguration sqlConfiguration = configuration.getSqlConfiguration();
dataSource.setUrl("jdbc:mysql://" + sqlConfiguration.getAddress() + ":" + sqlConfiguration.getPort());
dataSource.setUser(sqlConfiguration.getUsername());
dataSource.setPassword(sqlConfiguration.getPassword());
connection = dataSource.getConnection();
connection.setAutoCommit(true);
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();
final String query = "CREATE TABLE IF NOT EXISTS %s.DATA ".replace("%s", schemaName) +
"(uuid varchar(36) NOT NULL," +
"name varchar(16)," +
"skin varchar(16)," +
"locale char(2) NOT NULL," +
"bungeecord boolean NOT NULL," +
"PRIMARY KEY (uuid))";
final PreparedStatement statement = connection.prepareStatement(query);
statement.executeUpdate();
}
private void createDatabase() throws SQLException {
final Connection connection = getConnection();
final String query = "CREATE DATABASE IF NOT EXISTS %s".replace("%s", schemaName);
final PreparedStatement statement = connection.prepareStatement(query);
statement.executeUpdate();
}
public Connection getConnection() {
return connection;
}
}

View file

@ -0,0 +1,28 @@
package xyz.ineanto.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,78 @@
package xyz.ineanto.nicko.storage.redis;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.exceptions.JedisException;
import xyz.ineanto.nicko.appearance.ActionResult;
import xyz.ineanto.nicko.config.Configuration;
import xyz.ineanto.nicko.language.LanguageKey;
import xyz.ineanto.nicko.profile.NickoProfile;
import xyz.ineanto.nicko.storage.Cache;
import xyz.ineanto.nicko.storage.CacheProvider;
import java.util.Optional;
import java.util.UUID;
public class RedisCache extends Cache {
private final Gson gson = new GsonBuilder()
.serializeNulls()
.setPrettyPrinting()
.create();
private final Configuration configuration;
private RedisCacheProvider provider;
public RedisCache(Configuration configuration) {
this.configuration = configuration;
}
@Override
public CacheProvider getProvider() {
if (provider == null) {
provider = new RedisCacheProvider(configuration);
}
return provider;
}
@Override
public ActionResult cache(UUID uuid, NickoProfile profile) {
try (Jedis jedis = provider.getJedis()) {
jedis.set("nicko:" + uuid.toString(), gson.toJson(profile));
return ActionResult.ok();
} catch (JedisException exception) {
return ActionResult.error(LanguageKey.Error.CACHE);
}
}
@Override
public boolean isCached(UUID uuid) {
try (Jedis jedis = provider.getJedis()) {
return jedis.exists("nicko:" + uuid.toString());
} catch (JedisException exception) {
return false;
}
}
@Override
public Optional<NickoProfile> retrieve(UUID uuid) {
try (Jedis jedis = provider.getJedis()) {
// 08/29/23: what the fuck was I talking about?
// old_todo (Ineanto, 05/20/23): Check if cached before because Jedis returns a bulk reply so this is unsafe
final String data = jedis.get("nicko:" + uuid.toString());
final NickoProfile profile = gson.fromJson(data, NickoProfile.class);
return Optional.of(profile);
} catch (JedisException exception) {
return Optional.empty();
}
}
@Override
public ActionResult delete(UUID uuid) {
try (Jedis jedis = provider.getJedis()) {
jedis.del("nicko:" + uuid.toString());
return ActionResult.ok();
} catch (JedisException exception) {
return ActionResult.error(LanguageKey.Error.CACHE);
}
}
}

View file

@ -0,0 +1,38 @@
package xyz.ineanto.nicko.storage.redis;
import redis.clients.jedis.exceptions.JedisConnectionException;
import xyz.ineanto.nicko.config.Configuration;
import xyz.ineanto.nicko.config.DataSourceConfiguration;
import xyz.ineanto.nicko.storage.CacheProvider;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
public class RedisCacheProvider implements CacheProvider {
private final Configuration configuration;
private JedisPool pool;
public RedisCacheProvider(Configuration configuration) {
this.configuration = configuration;
}
@Override
public boolean init() {
final DataSourceConfiguration redisConfiguration = configuration.getRedisConfiguration();
pool = new JedisPool(redisConfiguration.getAddress(), redisConfiguration.getPort());
try {
return !pool.isClosed() && pool.getResource() != null;
} catch (JedisConnectionException exception) {
return false;
}
}
@Override
public boolean close() {
pool.close();
return pool.isClosed();
}
public Jedis getJedis() {
return pool.getResource();
}
}

View file

@ -0,0 +1,33 @@
package xyz.ineanto.nicko.version;
import org.jetbrains.annotations.NotNull;
import java.util.Comparator;
public record Version(int major, int minor, int patch) implements Comparable<Version> {
@Override
public int compareTo(@NotNull Version otherVersion) {
final Comparator<Version> comparator = Comparator
.comparingInt(Version::major)
.thenComparingInt(Version::minor)
.thenComparingInt(Version::patch);
return comparator.compare(this, otherVersion);
}
@Override
public String toString() {
return major + "." + minor + "." + patch;
}
public static Version fromString(String versionString) {
if (versionString == null || versionString.isEmpty()) { return new Version(0, 0, 0); }
final String[] split = versionString.split("\\.");
try {
return new Version(Integer.parseInt(split[0]),
Integer.parseInt(split[1]),
Integer.parseInt(split[2]));
} catch (NumberFormatException exception) {
return new Version(0, 0, 0);
}
}
}

View file

@ -0,0 +1,73 @@
/**
* PacketWrapper - ProtocolLib wrappers for Minecraft packets
* Copyright (C) dmulloy2 <http://dmulloy2.net>
* Copyright (C) Kristian S. Strangeland
* <p>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* <p>
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* <p>
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.ineanto.nicko.wrapper;
import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.ProtocolLibrary;
import com.comphenix.protocol.events.PacketContainer;
import com.google.common.base.Objects;
import org.bukkit.entity.Player;
public abstract class AbstractPacket {
protected PacketContainer handle;
/**
* Constructs a new strongly typed wrapper for the given packet.
*
* @param handle - handle to the raw packet data.
* @param type - the packet type.
*/
protected AbstractPacket(PacketContainer handle, PacketType type) {
// Make sure we're given a valid packet
if (handle == null)
throw new IllegalArgumentException("Packet handle cannot be NULL.");
if (!Objects.equal(handle.getType(), type))
throw new IllegalArgumentException(handle.getHandle()
+ " is not a packet of type " + type);
this.handle = handle;
}
/**
* Retrieve a handle to the raw packet data.
*
* @return Raw packet data.
*/
public PacketContainer getHandle() {
return handle;
}
/**
* Send the current packet to the given receiver.
*
* @param receiver - the receiver.
* @throws RuntimeException If the packet cannot be sent.
*/
public void sendPacket(Player receiver) {
ProtocolLibrary.getProtocolManager().sendServerPacket(receiver,
getHandle());
}
/**
* Send the current packet to all online players.
*/
public void broadcastPacket() {
ProtocolLibrary.getProtocolManager().broadcastServerPacket(getHandle());
}
}

View file

@ -0,0 +1,32 @@
package xyz.ineanto.nicko.wrapper;
import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.wrappers.Converters;
import it.unimi.dsi.fastutil.ints.IntList;
/**
* Sent by the server to the client to remove one or more entities.
*/
public class WrapperPlayServerEntityDestroy extends AbstractPacket {
/**
* The packet type that is wrapped by this wrapper.
*/
public static final PacketType TYPE = PacketType.Play.Server.ENTITY_DESTROY;
public WrapperPlayServerEntityDestroy() {
super(new PacketContainer(TYPE), TYPE);
handle.getModifier().writeDefaults();
}
/**
* Sets the list of entity ids to remove
*
* @param value New value for field 'entityIds'
*/
public void setEntityIds(IntList value) {
this.handle.getModifier().withType(IntList.class, Converters.passthrough(IntList.class)).writeSafely(0, value);
}
}

View file

@ -0,0 +1,114 @@
package xyz.ineanto.nicko.wrapper;
import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.events.InternalStructure;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.reflect.StructureModifier;
import com.comphenix.protocol.reflect.accessors.Accessors;
import com.comphenix.protocol.utility.MinecraftReflection;
import com.comphenix.protocol.utility.MinecraftVersion;
import com.comphenix.protocol.wrappers.EnumWrappers;
import com.comphenix.protocol.wrappers.MinecraftKey;
import com.google.common.hash.Hashing;
import org.bukkit.GameMode;
import org.bukkit.World;
/**
* PacketPlayServerRespawn Wrapper class (1.20.X to 1.21.X)
* <p>
* In 1.20.2, all the fields were merged inside a
* single "CommonPlayerSpawnInfo" record.
*
* @author inenato (w/ additional help from lukalt), based on work from dmulloy2 and Kristian S. Strangeland
*/
public class WrapperPlayServerRespawn extends AbstractPacket {
public static final PacketType TYPE = PacketType.Play.Server.RESPAWN;
private InternalStructure spawnInfoStructure = null;
public WrapperPlayServerRespawn() {
super(new PacketContainer(TYPE), TYPE);
handle.getModifier().writeDefaults();
if (MinecraftVersion.CONFIG_PHASE_PROTOCOL_UPDATE.atOrAbove()) {
spawnInfoStructure = handle.getStructures().read(0);
}
}
public void setDimension(World value) {
final MinecraftVersion v1_20_5 = new MinecraftVersion(1, 20, 5);
if (!MinecraftVersion.getCurrentVersion().isAtLeast(v1_20_5)) {
// 1.20 - 1.20.4
final StructureModifier<InternalStructure> structureModifier = spawnInfoStructure == null ?
handle.getStructures() : spawnInfoStructure.getStructures();
final StructureModifier<World> worldStructureModifier = spawnInfoStructure == null ?
handle.getWorldKeys() : spawnInfoStructure.getWorldKeys();
final InternalStructure dimensionType = structureModifier.read(0);
dimensionType.getMinecraftKeys().writeSafely(0, new MinecraftKey("minecraft", "dimension_type"));
dimensionType.getMinecraftKeys().writeSafely(1, new MinecraftKey("minecraft", "overworld"));
structureModifier.writeSafely(0, dimensionType);
worldStructureModifier.writeSafely(0, value);
} else {
// 1.20.5 to 1.21.1
// why is life so hard?
final Class<?> commonPlayerInfoClazz = MinecraftReflection.getMinecraftClass("network.protocol.game.CommonPlayerSpawnInfo");
try {
final Object commonSpawnData = Accessors.getFieldAccessor(TYPE.getPacketClass(), commonPlayerInfoClazz, true).getField()
.get(this);
final MinecraftKey key = MinecraftKey.fromHandle(
Accessors.getFieldAccessor(
commonPlayerInfoClazz,
MinecraftReflection.getResourceKey(),
true
)
.get(spawnInfoStructure));
Accessors.getFieldAccessor(
commonPlayerInfoClazz,
MinecraftReflection.getResourceKey(),
true
)
.set(commonSpawnData, key);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
public void setGameMode(GameMode value) {
if (!MinecraftVersion.CONFIG_PHASE_PROTOCOL_UPDATE.atOrAbove()) {
// 1.20 to 1.20.1
handle.getGameModes().writeSafely(0, EnumWrappers.NativeGameMode.fromBukkit(value));
return;
}
spawnInfoStructure.getGameModes().writeSafely(0, EnumWrappers.NativeGameMode.fromBukkit(value));
}
public void setPreviousGameMode(GameMode value) {
if (!MinecraftVersion.CONFIG_PHASE_PROTOCOL_UPDATE.atOrAbove()) {
// 1.20 to 1.20.1
handle.getGameModes().writeSafely(1, EnumWrappers.NativeGameMode.fromBukkit(value));
return;
}
spawnInfoStructure.getGameModes().writeSafely(1, EnumWrappers.NativeGameMode.fromBukkit(value));
}
public void setCopyMetadata(boolean value) {
if (!MinecraftVersion.CONFIG_PHASE_PROTOCOL_UPDATE.atOrAbove()) return;
// 1.20 to 1.20.1
handle.getBooleans().writeSafely(0, value);
}
public void setSeed(long value) {
if (!MinecraftVersion.CONFIG_PHASE_PROTOCOL_UPDATE.atOrAbove()) {
// 1.20 to 1.20.1
handle.getLongs().writeSafely(0, Hashing.sha256().hashLong(value).asLong());
}
}
}

View file

@ -0,0 +1,103 @@
package xyz.ineanto.nicko.wrapper;
import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.events.PacketContainer;
import org.bukkit.Location;
import org.bukkit.entity.EntityType;
import javax.annotation.Nonnull;
import java.util.UUID;
/**
* This packet is sent by the server when a player comes into visible range, not when a player joins.
*/
public class WrapperPlayServerSpawnEntity extends AbstractPacket {
/**
* The packet type that is wrapped by this wrapper.
*/
public static final PacketType TYPE = PacketType.Play.Server.SPAWN_ENTITY;
/**
* Constructors a new wrapper for the specified packet
*/
public WrapperPlayServerSpawnEntity() {
super(new PacketContainer(TYPE), TYPE);
handle.getModifier().writeDefaults();
}
/**
* Sets the entity id of the player
*
* @param value New value for field 'entityId'
*/
public void setEntityId(int value) {
this.handle.getIntegers().writeSafely(0, value);
this.handle.getEntityTypeModifier().writeSafely(0, EntityType.PLAYER);
}
/**
* Sets the unique id of the player
*
* @param value New value for field 'playerId'
*/
public void setPlayerId(UUID value) {
this.handle.getUUIDs().writeSafely(0, value);
}
/**
* Sets the value of field 'x'
*
* @param value New value for field 'x'
*/
public void setX(double value) {
this.handle.getDoubles().writeSafely(0, value);
}
/**
* Sets the value of field 'y'
*
* @param value New value for field 'y'
*/
public void setY(double value) {
this.handle.getDoubles().writeSafely(1, value);
}
/**
* Sets the value of field 'z'
*
* @param value New value for field 'z'
*/
public void setZ(double value) {
this.handle.getDoubles().write(2, value);
}
/**
* Sets the discrete rotation around the y-axis (yaw)
*
* @param value New value for field 'yRot'
*/
public void setYRotRaw(byte value) {
this.handle.getBytes().writeSafely(0, value);
}
/**
* Sets the discrete rotation around the x-axis (pitch)
*
* @param value New value for field 'xRot'
*/
public void setXRotRaw(byte value) {
this.handle.getBytes().writeSafely(1, value);
}
public void setLocation(@Nonnull Location location) {
setX(location.getX());
setY(location.getY());
setZ(location.getZ());
setYRotRaw(degreesToAngle(location.getYaw()));
setXRotRaw(degreesToAngle(location.getPitch()));
}
private byte degreesToAngle(float degree) {
return (byte)((int)(degree * 256.0F / 360.0F));
}
}

View file

@ -0,0 +1,37 @@
package xyz.ineanto.nicko.wrapper;
import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.utility.MinecraftVersion;
import com.comphenix.protocol.wrappers.EnumWrappers;
import com.comphenix.protocol.wrappers.PlayerInfoData;
import java.util.List;
import java.util.Set;
/**
* Up-to-date version of the Wrapper class
* for the PlayerServerPlayerInfo.
*
* @author ineanto, based on work from dmulloy2 and Kristian S. Strangeland
*/
public class WrapperPlayerServerPlayerInfo extends AbstractPacket {
public static final PacketType TYPE = PacketType.Play.Server.PLAYER_INFO;
public WrapperPlayerServerPlayerInfo() {
super(new PacketContainer(TYPE), TYPE);
handle.getModifier().writeDefaults();
}
public void setActions(Set<EnumWrappers.PlayerInfoAction> value) {
if (MinecraftVersion.FEATURE_PREVIEW_UPDATE.atOrAbove()) {
handle.getPlayerInfoActions().writeSafely(0, value);
} else {
handle.getPlayerInfoAction().writeSafely(0, value.stream().iterator().next()); // Get the first Value.
}
}
public void setData(List<PlayerInfoData> value) {
handle.getPlayerInfoDataLists().writeSafely(1, value);
}
}

View file

@ -0,0 +1,27 @@
package xyz.ineanto.nicko.wrapper;
import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.events.PacketContainer;
import java.util.List;
import java.util.UUID;
/**
* Up-to-date version of the Wrapper class
* for the PlayerServerPlayerInfoRemove.
*
* @author ineanto, based on work from dmulloy2 and Kristian S. Strangeland
*/
public class WrapperPlayerServerPlayerInfoRemove extends AbstractPacket {
public static final PacketType TYPE = PacketType.Play.Server.PLAYER_INFO_REMOVE;
public WrapperPlayerServerPlayerInfoRemove() {
super(new PacketContainer(TYPE), TYPE);
handle.getModifier().writeDefaults();
}
public void setUUIDs(List<UUID> value) {
handle.getUUIDLists().writeSafely(0, value);
}
}

View file

@ -0,0 +1,50 @@
# Nicko ${version} - Config:
# Specifies the configuration version, don't change.
version: "1.2.0"
# 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
sql:
# Indicates wherever the data will be stored locally
# inside a .json file or in an SQL database.
# Accepted values: false (Disabled), true (Enabled)
enabled: false
# Toggles between the MariaDB and MySQL drivers.
# If you use the MySQL database engine, switch this to off.
# Accepted values: false (Disabled), true (Enabled)
mariadb: true
# 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"
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"

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

@ -0,0 +1,153 @@
# Nicko ${version} - Language File:
# Specifies the configuration version, don't change.
version: "1.2.0"
prefix: "<b><gradient:#01a97c:#8ffd54>NICKO</gradient></b>"
whoosh: "<b><gradient:#01a97c:#8ffd54>WHOOSH!</gradient></b>"
oops: "<b><color:#ff4640>OOPS!</color></b>"
error:
permission: "<gray>You're missing the permission to do that.</gray>"
invalid_username: "<gray>This is an invalid Minecraft username.</gray>"
mojang_name: "<gray>There's is not Minecraft account with this username.</gray>"
mojang_skin: "<gray>This Minecraft account has no skin.</gray>"
cache: "<gray>Unable to get data from the cache.</gray>"
event:
settings:
error: "<gray>Wasn''t able to update your settings! ({0})</gray>"
appearance:
set:
error: "<gray>Wasn''t able to apply your disguise! ({0})</gray>"
ok: "<gray>You're now disguised.</gray>"
restore:
error: "<gray>Wasn''t able to apply the previous disguise! ({0})</gray>"
ok: "<gray>Disguise restored from last time.</gray>"
remove:
error: "<gray>Wasn''t able to remove your disguise!.</gray>"
missing: "<gray>You''re not currently disguised.</gray>"
ok: "<gray>Undisguised successfully.</gray>"
admin:
cache:
invalidate_cache: "<gray>Cache purged.</gray>"
invalidate_entry: "<gray>{0} was purged.</gray>"
check:
remove_skin: "<gray>Skin removed from player.</gray>"
gui:
title:
home: "Nicko - Home"
settings: "Settings"
admin: "Administration"
check: "Player Management"
confirm: "Are you sure?"
cache: "Cache Management"
invalidate_skin: "Purge cache..."
exit:
name: "Exit"
go_back:
name: "Back"
unavailable:
name: "Unavailable"
lore:
- "<gray><i>This button is disabled.</i></gray>"
error:
name: "Error!"
lore:
- "<gray>The item failed to load, but it might still work.</gray>"
loading:
name: "<gray><i>Loading...</i></gray>"
choice:
confirm:
name: "<green>Confirm</green>"
choose:
name: "<gold><i>Choose an option...</i></gold>"
cancel:
name: "<red>Cancel</red>"
scroll_up:
name: "Scroll up"
lore:
- "<dark_gray><i>(You can't scroll any higher.)</i></dark_gray>"
scroll_down:
name: "Scroll down"
lore:
- "<dark_gray><i>(You can't scroll any further down.)</i></dark_gray>"
new_skin:
name: "New skin..."
new_name:
name: "New name..."
home:
admin:
name: "Administration panel"
lore:
- "<gray>Configure and manage Nicko.</gray>"
settings:
name: "Settings"
lore:
- "<gray>Fine tune your experience with Nicko.</gray>"
change_name:
name: "Change your <gold>nickname</gold>"
change_skin:
name: "Change your <gold>skin</gold>"
change_both:
name: "Change <gold>both</gold>"
random_skin:
name: "<rainbow>Get a random appearance!</rainbow>"
reset:
name: "Reset appearance"
lore:
- "<gray>Completely remove your disguise.</gray>"
admin:
manage_cache:
name: "Manage the <gold>skin</gold> cache..."
lore:
- "<gray>View and manage the skin cache.</gray>"
manage_player:
name: "Inspect a player..."
lore:
- "<gray>See players' disguise information.</gray>"
check:
name: "<gold>{0}</gold>"
lore:
- "<red>Nicked:</red> {1}"
- "<red>Name:</red> <gold>{2}</gold>"
- "<red>Skin:</red> <gold>{3}</gold>"
- " "
- "<gray><i>Click to remove skin!</i></gray>"
cache:
statistics:
name: "Statistics"
lore:
- "Request count: <aqua>{0}</aqua>"
- "Number of skin cached: <aqua>{1}</aqua>"
- "<dark_gray><i>Cache is cleared every 24 hours.</i></dark_gray>"
invalidate_cache:
name: "Invalidate cache"
lore:
- "<red><i>NOT RECOMMENDED</i></red>"
- "<gray>Invalidate the entirety of the skin cache.</gray>"
- "<gray>This doesn't reset player's disguises.</gray>"
invalidate_skin:
name: "Invalidate a skin..."
lore:
- "<gray>Select a specific skin to invalidate.</gray>"
- "<gray>Useful if a skin has been recently updated.</gray>"
entry:
name: "<gold>{0}</gold>"
lore:
- "<gray>Click to invalidate...</gray>"
settings:
toggleable_button:
lore:
- "{0} Disabled"
- "{1} Enabled"
cycling_choices:
lore:
- "<gray><i>Cycle through the values</i></gray>"
- "<gray><i>by left or right clicking.</i></gray>"
language:
name: "Language"
random_skin:
name: "Random skin on login"

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

@ -0,0 +1,155 @@
# Nicko ${version} - Fichier de langue:
# Précise la version de la configuration, ne pas changer.
version: "1.2.0"
prefix: "<b><gradient:#01a97c:#8ffd54>NICKO</gradient></b>"
whoosh: "<b><gradient:#01a97c:#8ffd54>WHOOSH!</gradient></b>"
oops: "<b><color:#ff4640>OOPS!</color></b>"
error:
permission: "<gray>Vous n'avez pas la permission de faire cela.<gray>"
invalid_username: "<gray>Nom d'utilisateur Minecraft invalide.<gray>"
mojang_name: "<gray>Aucun compte Minecraft associé à ce nom d'utilisateur.<gray>"
mojang_skin: "<gray>Ce compte Minecraft n'a pas de skin.<gray>"
cache: "<gray>Impossible de récupérer les données depuis le cache.<gray>"
event:
settings:
error: "<gray>Impossible de mettre à jour vos paramètres ! ({0})</gray>"
appearance:
set:
error: "<gray>Impossible d''appliquer votre déguisement ! ({0})</gray>"
ok: "<gray>Déguisement appliqué avec succès.</gray>"
restore:
error: "<gray>Impossible d''appliquer le précédent déguisement ! ({0})</gray>"
ok: "<gray>Votre précédent déguisement a été appliqué.<gray>"
remove:
error: "<gray>Impossible de retirer votre déguisement.</gray>"
missing: "<gray>Vous n''avez pas de déguisement.</gray>"
ok: "<gray>Déguisement retiré.</gray>"
admin:
cache:
invalidate_cache: "<gray>Cache complet invalidé.</gray>"
invalidate_entry: "<gray>{0} a été invalidé.</gray>"
check:
remove_skin: "<gray>Déguisement retiré au joueur.</gray>"
gui:
title:
home: "Nicko - Accueil"
settings: "Paramètres"
admin: "Administration"
check: "Gestion des Joueurs"
confirm: "Êtes-vous sûr ?"
cache: "Gestion du Cache"
invalidate_skin: "Purge du cache..."
exit:
name: "Quitter"
go_back:
name: "Retour"
unavailable:
name: "Indisponible"
lore:
- "<gray><i>Ce boutton est désactivé.</i></gray>"
error:
name: "Erreur !"
lore:
- "<gray>La texture de l'objet n'a pas chargé</gray>"
- "<gray>correctement mais il fonctionne encore.</gray>"
loading:
name: "<gray><i>Chargement...</i></gray>"
choice:
confirm:
name: "<green>Confirmer</green>"
choose:
name: "<gold><i>Choisissez une option...</i></gold>"
cancel:
name: "<red>Annuler</red>"
scroll_up:
name: "Défiler vers le haut"
lore:
- "<dark_gray><i>(Impossible de défiler plus haut.)</i></dark_gray>"
scroll_down:
name: "Défiler vers le bas"
lore:
- "<dark_gray><i>(Impossible de défiler plus bas.)</i></dark_gray>"
new_skin:
name: "Nouveau skin..."
new_name:
name: "Nouveau nom..."
home:
admin:
name: "Panel d'administration"
lore:
- "<gray>Configurez et gérez Nicko.</gray>"
settings:
name: "Paramètres"
lore:
- "<gray>Gérez votre expérience avec Nicko.</gray>"
change_name:
name: "Changer le <gold>pseudo</gold>"
change_skin:
name: "Changer le <gold>skin</gold>"
change_both:
name: "Changer les <gold>deux</gold>"
random_skin:
name: "<rainbow>Obtenir une apparence aléatoire !</rainbow>"
reset:
name: "Réinitialiser l'apparence"
lore:
- "<gray>Supprime complètement votre déguisement.</gray>"
admin:
manage_cache:
name: "Gérer le cache de <gold>skin...</gold>"
lore:
- "<gray>Consultez et gérez le cache de skin.</gray>"
manage_player:
name: "Vérifier un joueur..."
lore:
- "<gray>Vérifiez les informations de déguisement d'un joueur.</gray>"
check:
name: "<gold>{0}</gold>"
lore:
- "<red>Déguisé:</red> {1}"
- "<red>Nom:</red> <gold>{2}</gold>"
- "<red>Skin:</red> <gold>{3}</gold>"
- " "
- "<gray><i>Cliquez pour retirer le skin !</i></gray>"
cache:
statistics:
name: "Statistiques"
lore:
- "Nombre de requêtes: <aqua>{0}</aqua>"
- "Nb. de skin dans le cache: <aqua>{1}</aqua>"
- "<dark_gray><i>Le cache est vidé toutes les 24 heures.</i></dark_gray>"
invalidate_cache:
name: "Purger le cache"
lore:
- "<red><i>DÉCONSEILLÉ</i></red>"
- "<gray>Purge l'entièreté du cache des skin.</gray>"
- "<gray>Ne retire pas les déguisements des joueurs.</gray>"
invalidate_skin:
name: "Invalider un skin..."
lore:
- "<gray>Sélectionnez une apparence spécifique à</gray>"
- "<gray>invalider. Utile dans le cas où un skin</gray>"
- "<gray>a récemment été mis à jour.</gray>"
entry:
name: "<gold>{0}</gold>"
lore:
- "<gray>Cliquez pour invalider...</gray>"
settings:
toggleable_button:
lore:
- "{0} Désactivé"
- "{1} Activé"
cycling_choices:
lore:
- "<gray><i>Parcourez les valeurs</i></gray>"
- "<gray><i>avec un clique gauche/droit.</i></gray>"
language:
name: "Langage"
random_skin:
name: "Apparence aléatoire à la connexion"

View file

@ -0,0 +1,490 @@
w4nderlost
TooParanoids
Der_OG_31er
9xxDaRkShAdOwxx9
giiiaan_
Jqstinnn
Tillysboy92
AlwaysCello
SyndrexG0D
Peypeycake
ThePerjurer
Tioe
Elternbaum
BarkersRover_16
pebsso
cyrus6950
Bigest_guy
RV0REU
R379
Shetell
_HEAPASS_
Iamaloner21
TheFardoxGamerHD
Flyboi43
Cha0smusik
kat00
Infreat
Crummymoofin
MijnVriend
momsrightkidney
dmacrado
Elephantman321
ii_hamoudi_YT
FaurePavane
ambiezzz
XD_Bandit695_XD
Nabingo
Cyl0re
ku5
SrAragon
StarlightDream9
CJ5370
rainbees
KeroTheWolf
Andrews9722
cursed_Assyrian
yamateni
ProgramEXE
exprso
harrypanda
LookerMD
migykins
Wintrous
ZzGaBi
Flayber
Grenixal
maeve_wells
Creeper10fr
10Chairs
2525lock
Shqipe
XenitsuZen
Berno17_
wolle1313
HalfDogHalfCat1
NachoGarcia
popsicoal
NemesiSevil2006
AnywayOj
Tanko12345
Samdweck
LYRECODE123
Resulten
SirBastii
Maku056
ItzArnizzz
Brsh3620
Masonita
kapplanium
shoezo
Mansur203
Waterboy15217
redragonne
ghko325
HopePVP_tw7
xtka
NimwenxZ
Hiro0408
PanderaWz
Shesu
_Aniste_NY_
Besceste
3ee3
ArcticGalaxy123
snooze_mingo
LizzyLomnh
FaZeChulupa
LineZeeK
liabilaty
BlackSheep1610
Simif69
Aficionado
riekin
XLuggas
MathExams
6fq
Marveel
lolme51
TaioSayUwU
Fonklift
blvw
Po1204
Pierre_Rabbit
mifimasters
MrRidge1
arnqen
Nick_cage102
Geo_9918
SSShudder
Nicolas_Mom
WolfMatrix101
frictionless_
Gughik
gold_dragon_4
nealxero
ClonedPickle
SourWatermelonxX
devilunion
Daryifuny
joaomarcos11
Dekeef
PadfootTheDog
DarkScopez80
flowers220
Gaiiya
The_Yeet_
juuuuwu
MrMafioz
Surpasses
TypicalOwen
0lober
Zerfixy
Sunny3803b
neostanley
Creeper_Kart
minestin
Goldenfredster
Vju
MrArchI_YT
Casper1709
Backiii_
DrCreate
Nova_Lux1
Jayvin_Blanco
ShadowMan1770
0KOPO40K
Silk_Altermann69
Alu____
Honey_MilkExe112
T0XEI
CB_13
Paragorn
HaGiang
Shivendra8i
Mayflower47
Not_Someguy
raxx111bg
IceyGlaceon
grasseffect
PoopTNTpvp
codecyber
Shrumpkin13
Shqdown
Cmartin82
KTCXD_5p0tty
rockesalt
goldjeong
TheRealAKD
NamiSwaaan
yaaratol
Dikiy_Flexer
PoLecker24
_SakuraTree
PikkOgP0rno
HawksFang
BlueWinterWolf
hskmerk
gurmaw
Lunggor
clashfield
Zelaste
ACommonMouse
TJ_Mystery
Dizzy3312
Raindropsss
minecraftxiaoju
sachilovebbh
Celina_LaZyCxt
firered6
55000000
Illunarnati
Jedidiah2003
setomz1
basically_e
TommyGreif02
Bongrip42069
Coco_Keopi
Lt_Colt
Kuurotta
GqmeKnight
WinCo_Foods
FKDLZ
IanPumpkin
tastywtf
natedawg0
ZQLFenyx
GamerMaster110
papajobi9
Yucaroon
Xion_69
AirJaw
funfun321234
khalleesii
Pozisch
thorso15
kyumisoup
Leonqrd
BmwDreamer
TehRos
pitplayer69
_ve0
Miss_Yuka
I_am_a_Bucket
nicolas_bean
xHorizonGC
RTX3060_
Borec188
c8to
megan3groCENG
ventriloquize
galczin
Scorch3dEarth3d
nightstarLP
VittyGam3r
GLaDOS__
hydrelo
JustACarter
MikeDropperPlay
NorthernWest
_Skelesam_
ZX_Style
Tamas_Boi
taylub
VyacheslavO_O
trippyaubs
udtpic
Lunarglow
Stoolman
legendary_meow
Loganii
CaptainStain56
FoofieGeto
Judgeavapl
OneBigDigger
Sorem13
Raisonneur
IlkoalI
Naxa
craigbabyonebay
NicholasG04
UtopianCrisi
CalamarPasGenti
ryluh_
Aceslimz
Howardygh99927
brandon257
MarcoswildHD
x_XSkylerX_x
Bronco09
That_Kookie_
Danigtz
Ricky_lol
999keyt
Thilow15
Difesito
ostehovl
isacano_12
big_esra
secretbaguette
MrCommunism
Jekube
GrandeMaster
DrGrip
TheArnek
JacsMars
Lliam14
MrGameandWatch84
Rinzap
XrazzeD
ukknown
ZohanPrent
Naspo
Rajem
VepiGHG
matoureal
BrianChen87
Jrocky
stivo999
Des_cole
ReqGames
Kingja1912
issssyy
Apache424
Nick_Zockt_
Mr_Haider_10
ValentineBun
fedorPro228
xTilz
blockbuster02k
4ck
FrostedTree
VegasTortoise
ZBellaV12121
paypales
qDread
itsRiven
i8oreo
_kimcream_
Phisuss
Oscargray
Elsiff
callofdutydog00
BruhTheMoment
Pazmaa
MythicalOak
komuchi33
awf4
Jacobsaurus21
itsjohannaa
Jello12
Adrien183
jajazzywazzy
Jorjie22
SuperBrawlr5788
KaraageV
_Hanime_tv
Padilhao
Tikkas
ordinaryducky
Mothytix95
renopotouwu
1000voices
niclas05
Felipstein
DoutorBauer
FireballPlaysMC
vapelordsheep
Aboain
ImGrexy
Aivokolo
SuperAmazing101
va75a77a
_DawYans_
AceT1223
Livvyboo7
Saaaaaaaaaaaaans
okaychill
AwesomeBro1122
absolutesnek
jogie5000
curtainSenpai
gabbyisaloser
gamergurl6969
ZeusDk
FranaRibas
Discoboss
SYZ_1
Nakoe
FIU_Captive
xSiFan_
ilyDeathxz
da_fipsi
Lochy
The_Lavie37
Tonion
vnvrchy
xX0meg4Xx
haohxtheone
VtTronic
xDaniGum
tikkelill
DatYoshi
eyehamstewpit
nicholas460_
Memsly445
nugunugu
AndreSlayz
jashik1
Qweenoftheocean
Coltable
treblaclef
Kisaxx
69Dxddy69
RaulCuh
3Wheat
_OscarTheGrouch
oIsqk
Blockbusterweng
AntoineDegauss
ValeIsTheBest
SwaggyCrabby
DieOfSanity
SirenMC
Jade_Jewel
Tropic44
666splendor
TallnessTallness
breadgang9827
Muffin_Worlds
DedicatedVeggie
Gonzalox_7
datrandomasian
Chasemon01
Nyavix
Lonely_Summers
_RoveN
ok_kyro
LN_hunter
saharsabz
Roselilianna
Gadx
xtytan
RoRo_levosgien88
Bowsesqe_21
Bennett528
TheShipSailsWest
KaiserGaming
Layna_Shinozaki
OP_greatly
D3rpTaco
Loufink
Jorlmungus
Snichol1801
Ludixeo
Imoeto
MarshallNebunu
crazycrystals
Parapatus
HahaDani
MrQuaring
DonTurnt
SailorRoberts101
FluffieBear
TripleThick
KingSparta
MummysHome
Cooga3
Technosista
Youmerstudios
SkyyRaine
criss102
mrfailt
CraftingBasic
qnxkdifh
Igorex2k20
LaLisette
ReBoredGamer
warlordwest
ExoTemporal
KingLonmc
666Horus
IslandCity2
TheBigSavage1
Trishke2003
skyrowin
Krissy3D
AntonWTobias
SaddyWasTaken
Ahoy_Peko_Hao_Yo
T4nTr1Ss
aleciolike
ninja_shenley
Lordmord1337
eatmypoopfather
Ktanner
The42OWeedGOD
CooperBee
_MikuMiku_
althume
Tr3bba93

View file

@ -0,0 +1,19 @@
name: Nicko
main: xyz.ineanto.nicko.Nicko
version: ${version}
author: Ineanto
description: "The feature packed, next generation disguise plugin for Minecraft."
api-version: 1.20
softdepend: [ PlaceholderAPI ]
load: POSTWORLD
commands:
nicko:
description: "Opens Nicko's GUI."
permission: nicko.use
permissions:
nicko.*:
default: op
children:
- nicko.use
nicko.use:
default: false