diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..a3961dfa --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,13 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: skytasul +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] \ No newline at end of file diff --git a/README.md b/README.md index 3476daf3..e5e5404e 100644 --- a/README.md +++ b/README.md @@ -51,12 +51,12 @@ In *pom.xml*, add this to the `repositories` section: https://repo.codemc.org/repository/maven-public ``` -And add this to the `dependencies` section: (replace VERSION by whatever version you want, i.e. `0.19.2`, `0.19.3-SNAPSHOT`...) +And add this to the `dependencies` section: (replace VERSION by whatever version you want, i.e. `0.19.7`, `0.20-SNAPSHOT`...) ```xml fr.skytasul beautyquests-core VERSION - compile + provided ``` diff --git a/core/libs.sh b/core/libs.sh index 896cf1ef..91882d9a 100644 --- a/core/libs.sh +++ b/core/libs.sh @@ -16,14 +16,15 @@ echo -e "Maven path: $mavenPath\e[39m" "$mavenPath" install:install-file -Dfile=$jarsPath/Factions.jar -DgroupId=com.massivecraft -DartifactId=factions -Dversion=1.0 -Dpackaging=jar "$mavenPath" install:install-file -Dfile=$jarsPath/MassiveCore.jar -DgroupId=com.massivecraft -DartifactId=massivecore -Dversion=1.0 -Dpackaging=jar "$mavenPath" install:install-file -Dfile=$jarsPath/GPS.jar -DgroupId=com.live.bemmamin -DartifactId=gps -Dversion=1.0 -Dpackaging=jar -"$mavenPath" install:install-file -Dfile=$jarsPath/Jobs.jar -DgroupId=com.gamingmesh -DartifactId=jobs -Dversion=5.0.0.9 -Dpackaging=jar +"$mavenPath" install:install-file -Dfile=$jarsPath/Jobs.jar -DgroupId=com.gamingmesh -DartifactId=jobs -Dversion=5.1.0.1 -Dpackaging=jar "$mavenPath" install:install-file -Dfile=$jarsPath/McCombatLevel.jar -DgroupId=com.gmail.mrphpfan -DartifactId=mccombatlevel -Dversion=1.0 -Dpackaging=jar "$mavenPath" install:install-file -Dfile=$jarsPath/mcMMO.jar -DgroupId=com.gmail.nossr50 -DartifactId=mcmmo -Dversion=1.0 -Dpackaging=jar "$mavenPath" install:install-file -Dfile=$jarsPath/SkillAPI.jar -DgroupId=com.suxy -DartifactId=skillapi -Dversion=1.0 -Dpackaging=jar "$mavenPath" install:install-file -Dfile=$jarsPath/Boss.jar -DgroupId=org.mineacademy -DartifactId=boss -Dversion=4.2.1 -Dpackaging=jar "$mavenPath" install:install-file -Dfile=$jarsPath/CMI.jar -DgroupId=com.zrips -DartifactId=cmi -Dversion=9.0.2.1 -Dpackaging=jar -"$mavenPath" install:install-file -Dfile=$jarsPath/CMILib.jar -DgroupId=com.zrips -DartifactId=cmilib -Dversion=1.0.4.1 -Dpackaging=jar +"$mavenPath" install:install-file -Dfile=$jarsPath/CMILib.jar -DgroupId=com.zrips -DartifactId=cmilib -Dversion=1.2.3.3 -Dpackaging=jar "$mavenPath" install:install-file -Dfile=$jarsPath/UltimateTimber.jar -DgroupId=com.songoda -DartifactId=ultimatetimber -Dversion=2.2.5 -Dpackaging=jar +"$mavenPath" install:install-file -Dfile=$jarsPath/AdvancedSpawners-API.jar -DgroupId=gcspawners -DartifactId=gcspawners -Dversion=3.3.0 -Dpackaging=jar #"$mavenPath" install:install-file -Dfile=$jarsPath/MythicMobs.jar -DgroupId=io.lumine.xikage -DartifactId=MythicMobs -Dversion=4.12.0 -Dpackaging=jar #"$mavenPath" install:install-file -Dfile=$jarsPath/TokenEnchantAPI.jar -DgroupId=com.vk2gpz.tokenenchant -DartifactId=TokenEnchantAPI -Dversion=18.15.2 -Dpackaging=jar diff --git a/core/pom.xml b/core/pom.xml index f16dc7e8..282d4d95 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -8,7 +8,7 @@ fr.skytasul beautyquests-parent - 0.19.7 + 0.20.0 @@ -20,6 +20,7 @@ *.yml locales/*.yml + *.properties @@ -43,9 +44,17 @@ fr.skytasul.quests.utils.configupdater - de.jeff_media.updatechecker + com.jeff_media.updatechecker fr.skytasul.quests.utils.updatechecker + + revxrsal.commands + fr.skytasul.quests.commands.revxrsal + + + com.zaxxer.hikari + fr.skytasul.quests.utils.hikari + @@ -57,10 +66,28 @@ + + maven-compiler-plugin + 3.8.0 + + true + + + + maven-javadoc-plugin + 3.4.1 + + false + + + + papermc + https://repo.papermc.io/repository/maven-public/ + placeholderapi https://repo.extendedclip.com/content/repositories/placeholderapi/ @@ -75,7 +102,7 @@ codemc-repo - https://repo.codemc.org/repository/maven-public/ + https://repo.codemc.io/repository/maven-public/ mineacademy-repo @@ -117,13 +144,17 @@ jeff-media-public https://hub.jeff-media.com/nexus/repository/jeff-media-public/ + + bg-repo + https://repo.bg-software.com/repository/api/ + - org.spigotmc - spigot - 1.18.2-R0.1-SNAPSHOT + io.papermc.paper + paper-api + 1.19.2-R0.1-SNAPSHOT provided @@ -213,7 +244,7 @@ io.lumine Mythic-Dist - 5.0.1 + 5.2.0 provided @@ -225,13 +256,13 @@ com.github.BlueMap-Minecraft BlueMapAPI - v1.7.0 + v2.1.0 provided - de.jeff_media + com.jeff_media SpigotUpdateChecker - 1.3.2 + 3.0.0 compile @@ -241,10 +272,43 @@ provided - org.mariadb.jdbc - mariadb-java-client + com.github.decentsoftware-eu + decentholograms 2.7.5 - provided + provided + + + com.github.Flo0 + PlayerBlockTracker + 1.0.2 + provided + + + com.github.lokka30 + LevelledMobs + 3.2.6 + provided + + + com.bgsoftware + WildStackerAPI + 2022.6 + provided + + + com.github.Revxrsal.Lamp + bukkit + d72483065c + + + com.github.Revxrsal.Lamp + common + d72483065c + + + com.zaxxer + HikariCP + 4.0.3 @@ -287,7 +351,7 @@ com.gamingmesh jobs - 5.0.0.9 + 5.1.0.1 provided @@ -317,7 +381,13 @@ com.zrips cmilib - 1.0.4.1 + 1.2.3.3 + provided + + + gcspawners + gcspawners + 3.3.0 provided diff --git a/core/src/main/java/fr/skytasul/quests/BeautyQuests.java b/core/src/main/java/fr/skytasul/quests/BeautyQuests.java index bbd5d13f..2b5aeac6 100644 --- a/core/src/main/java/fr/skytasul/quests/BeautyQuests.java +++ b/core/src/main/java/fr/skytasul/quests/BeautyQuests.java @@ -2,9 +2,6 @@ import java.io.File; import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -12,31 +9,31 @@ import java.util.Date; import java.util.HashMap; import java.util.Map; +import java.util.function.Function; import java.util.logging.Level; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; import java.util.stream.Stream; - import org.bstats.bukkit.Metrics; +import org.bstats.charts.AdvancedPie; import org.bstats.charts.DrilldownPie; import org.bstats.charts.SimplePie; import org.bstats.charts.SingleLineChart; import org.bukkit.Bukkit; import org.bukkit.command.CommandSender; -import org.bukkit.command.PluginCommand; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.entity.Player; import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.scheduler.BukkitRunnable; - -import com.google.common.base.Charsets; +import com.jeff_media.updatechecker.UpdateCheckSource; +import com.jeff_media.updatechecker.UpdateChecker; import com.tchristofferson.configupdater.ConfigUpdater; - +import fr.skytasul.quests.api.Locale; import fr.skytasul.quests.api.QuestsAPI; import fr.skytasul.quests.api.bossbar.BQBossBarImplementation; -import fr.skytasul.quests.commands.Commands; import fr.skytasul.quests.commands.CommandsManager; import fr.skytasul.quests.editors.Editor; import fr.skytasul.quests.gui.Inventories; @@ -44,9 +41,7 @@ import fr.skytasul.quests.gui.creation.QuestObjectGUI; import fr.skytasul.quests.gui.creation.stages.StagesGUI; import fr.skytasul.quests.gui.misc.ItemComparisonGUI; -import fr.skytasul.quests.gui.quests.PlayerListGUI; import fr.skytasul.quests.options.OptionAutoQuest; -import fr.skytasul.quests.players.PlayerAccount; import fr.skytasul.quests.players.PlayersManager; import fr.skytasul.quests.players.PlayersManagerDB; import fr.skytasul.quests.players.PlayersManagerYAML; @@ -58,19 +53,19 @@ import fr.skytasul.quests.utils.DebugUtils; import fr.skytasul.quests.utils.Lang; import fr.skytasul.quests.utils.compatibility.DependenciesManager; +import fr.skytasul.quests.utils.compatibility.Post1_16; import fr.skytasul.quests.utils.compatibility.mobs.BukkitEntityFactory; import fr.skytasul.quests.utils.logger.ILoggerHandler; import fr.skytasul.quests.utils.logger.LoggerExpanded; import fr.skytasul.quests.utils.logger.LoggerHandler; import fr.skytasul.quests.utils.nms.NMS; -import de.jeff_media.updatechecker.UpdateChecker; - public class BeautyQuests extends JavaPlugin { public static LoggerExpanded logger; private static BeautyQuests instance; private BukkitRunnable saveTask; + private boolean isPaper; /* --------- Storage --------- */ @@ -85,6 +80,8 @@ public class BeautyQuests extends JavaPlugin { private File dataFile; private File saveFolder; + private Path backupDir = null; + /* --------- Datas --------- */ private ScoreboardManager scoreboards; @@ -99,6 +96,7 @@ public class BeautyQuests extends JavaPlugin { public static boolean loaded = false; public DependenciesManager dependencies = new DependenciesManager(); + private CommandsManager command; private LoggerHandler loggerHandler; @@ -122,18 +120,21 @@ public void onEnable(){ try { logger.info("------------ BeautyQuests ------------"); + checkPaper(); + dependencies.testCompatibilities(); Bukkit.getPluginManager().registerEvents(dependencies, this); saveDefaultConfig(); NMS.getMCVersion(); - registerCommands(); saveFolder = new File(getDataFolder(), "quests"); if (!saveFolder.exists()) saveFolder.mkdirs(); loadDataFile(); loadConfigParameters(true); + registerCommands(); + try { dependencies.initializeCompatibilities(); }catch (Exception ex) { @@ -156,6 +157,8 @@ public void run() { + (((double) System.currentTimeMillis() - lastMillis) / 1000D) + "s)!"); getServer().getPluginManager().registerEvents(new QuestsListener(), BeautyQuests.this); + if (NMS.getMCVersion() >= 16) + getServer().getPluginManager().registerEvents(new Post1_16(), BeautyQuests.this); launchSaveCycle(); @@ -188,15 +191,30 @@ public void run() { logger.severe("This is a fatal error. Now disabling."); disable = true; setEnabled(false); + }catch (Exception ex) { + logger.severe("An unexpected exception occurred while loading plugin.", ex); + logger.severe("This is a fatal error. Now disabling."); + disable = true; + setEnabled(false); } } @Override public void onDisable(){ try { - Editor.leaveAll(); - Inventories.closeAll(); - stopSaveCycle(); + try { + if (command != null) command.unload(); + }catch (Throwable ex) { + logger.severe("An error occurred while disabling command manager.", ex); + } + + try { + Editor.leaveAll(); + Inventories.closeAll(); + stopSaveCycle(); + }catch (Throwable ex) { + logger.severe("An error occurred while disabling editing systems.", ex); + } try { if (!disable) saveAllConfig(true); @@ -216,26 +234,21 @@ public void onDisable(){ } /* ---------- Various init ---------- */ + + private void checkPaper() { + try { + isPaper = Class.forName("com.destroystokyo.paper.ParticleBuilder") != null; + DebugUtils.logMessage("Paper detected."); + }catch (ClassNotFoundException ex) { + isPaper = false; + logger.warning("You are not running the Paper software.\n" + + "It is highly recommended to use it for extended features and more stability."); + } + } private void registerCommands(){ - CommandsManager questCommand = new CommandsManager((sender) -> { - if (!(sender instanceof Player)) return; - Player p = (Player) sender; - if (!p.hasPermission("beautyquests.command.listPlayer")){ - Lang.INCORRECT_SYNTAX.send(p); - }else { - PlayerAccount acc = PlayersManager.getPlayerAccount(p); - if (acc == null) { - Lang.ERROR_OCCURED.send(p, "no account data"); - logger.severe("Player " + p.getName() + " has got no account. This is a CRITICAL issue."); - }else Inventories.create(p, new PlayerListGUI(acc)); - } - }); - PluginCommand cmd = getCommand("beautyquests"); - cmd.setPermission("beautyquests.command"); - cmd.setExecutor(questCommand); - cmd.setTabCompleter(questCommand); - questCommand.registerCommandsClass(new Commands()); + command = new CommandsManager(); + command.initializeCommands(); } private void launchSaveCycle(){ @@ -291,31 +304,42 @@ private void launchMetrics(String pluginVersion) { if (size > 5) return "5 - 10"; return "0 - 5"; })); + metrics.addCustomChart(new AdvancedPie("hooks", () -> { // replace with bar chart when bStats add them back + return dependencies.getDependencies() + .stream() + .filter(dep -> dep.isEnabled()) + .map(dep -> dep.getFoundPlugin().getName()) + .distinct() + .collect(Collectors.toMap(Function.identity(), __ -> 1)); + })); DebugUtils.logMessage("Started bStats metrics"); } private void launchUpdateChecker(String pluginVersion) { DebugUtils.logMessage("Starting Spigot updater"); + UpdateChecker checker; if (pluginVersion.contains("_")) { Matcher matcher = Pattern.compile("_BUILD(\\d+)").matcher(pluginVersion); if (matcher.find()) { String build = matcher.group(1); - UpdateChecker.init(instance, "https://ci.codemc.io/job/SkytAsul/job/BeautyQuests/lastSuccessfulBuild/buildNumber") + checker = new UpdateChecker(this, UpdateCheckSource.CUSTOM_URL, "https://ci.codemc.io/job/SkytAsul/job/BeautyQuests/lastSuccessfulBuild/buildNumber") .setUserAgent("") .setDownloadLink("https://ci.codemc.io/job/SkytAsul/job/BeautyQuests") - .setNotifyOpsOnJoin(false) .setUsedVersion(build) - .setNameFreeVersion("(dev builds)") - .checkNow(); + .setNameFreeVersion("(dev builds)"); }else { logger.warning("Unknown plugin version, cannot check for updates."); + return; } }else { - UpdateChecker.init(this, 39255) - .setDownloadLink(39255) - .setNotifyOpsOnJoin(false) - .checkNow(); + checker = new UpdateChecker(this, UpdateCheckSource.SPIGOT, "39255") + .setDownloadLink(39255); } + checker + .setDonationLink("https://paypal.me/SkytAsul") + .setSupportLink("https://discord.gg/H8fXrkD") + .setNotifyOpsOnJoin(false) + .checkNow(); } /* ---------- YAML ---------- */ @@ -328,25 +352,25 @@ private void loadConfigParameters(boolean init) throws LoadingException { config.getConfig().save(configFile); logger.info("Updated config."); } - if (init && loadLang() == null) return; + if (init) loadLang(); ConfigUpdater.update(this, "config.yml", configFile); config.init(); ConfigurationSection dbConfig = config.getConfig().getConfigurationSection("database"); if (dbConfig.getBoolean("enabled")) { + db = null; try { db = new Database(dbConfig); db.testConnection(); logger.info("Connection to database etablished."); }catch (Exception ex) { - if (db != null) { - db.closeConnection(); - db = null; - } + db = null; throw new LoadingException("Connection to database has failed.", ex); } } + PlayersManager.manager = db == null ? new PlayersManagerYAML() : new PlayersManagerDB(db); + /* static initialization */ if (init) { StagesGUI.initialize(); // initializing default stage types @@ -365,47 +389,10 @@ private void loadConfigParameters(boolean init) throws LoadingException { private YamlConfiguration loadLang() throws LoadingException { try { - for (String language : new String[] { "en_US", "fr_FR", "zh_CN", "zh_HK", "de_DE", "pt_PT", "it_IT", "es_ES", "sv_SE", "hu_HU", "ru_RU", "pl_PL", "th_TH", "lt_LT", "vi_VN" }) { - File file = new File(getDataFolder(), "locales/" + language + ".yml"); - if (!file.exists()) saveResource("locales/" + language + ".yml", false); - } - - long lastMillis = System.currentTimeMillis(); loadedLanguage = config.getConfig().getString("lang", "en_US"); - String language = "locales/" + loadedLanguage + ".yml"; - File file = new File(getDataFolder(), language); - InputStream res = getResource(language); - boolean created = false; - if (!file.exists()){ - logger.warning("Language file " + language + " does not exist. Using default english strings."); - file.createNewFile(); - res = getResource("locales/en_US.yml"); - created = true; - } - YamlConfiguration conf = YamlConfiguration.loadConfiguration(file); - boolean changes = false; - if (res != null){ // if it's a local resource - YamlConfiguration def = YamlConfiguration.loadConfiguration(new InputStreamReader(res, StandardCharsets.UTF_8)); - for (String key : def.getKeys(true)){ // get all keys in resource - if (!def.isConfigurationSection(key)){ // if not a block - if (!conf.contains(key)){ // if string does not exist in the file - conf.set(key, def.get(key)); // copy string - if (!created) DebugUtils.logMessage("String copied from source file to " + language + ". Key: " + key); - changes = true; - } - } - } - } - Lang.loadStrings(YamlConfiguration.loadConfiguration(new InputStreamReader(getResource("locales/en_US.yml"), Charsets.UTF_8)), conf); - - if (changes) { - getLogger().info("Copied new strings into " + language + " language file."); - conf.save(file); // if there has been changes before, save the edited file - } - getLogger().info("Loaded language file " + language + " (" + (((double) System.currentTimeMillis() - lastMillis) / 1000D) + "s)!"); - return conf; - } catch(Exception e) { - throw new LoadingException("Couldn't create language file.", e); + return Locale.loadLang(this, Lang.values(), loadedLanguage); + }catch (Exception ex) { + throw new LoadingException("Couldn't load language file.", ex); } } @@ -426,8 +413,9 @@ private void loadDataFile() throws LoadingException { lastVersion = data.getString("version"); if (!lastVersion.equals(getDescription().getVersion())){ logger.info("You are using a new version for the first time. (last version: " + lastVersion + ")"); - createFolderBackup(); - createDataBackup(); + backupDir = backupDir(); + createFolderBackup(backupDir); + createDataBackup(backupDir); } }else lastVersion = getDescription().getVersion(); data.options().header("Do not edit ANYTHING here."); @@ -437,6 +425,7 @@ private void loadDataFile() throws LoadingException { private void loadAllDatas() throws Throwable { if (disable) return; dependencies.lockDependencies(); + command.lockCommands(); if (scoreboards == null && QuestsConfiguration.showScoreboards()) { File scFile = new File(getDataFolder(), "scoreboard.yml"); @@ -446,10 +435,11 @@ private void loadAllDatas() throws Throwable { } try{ - PlayersManager.manager = db == null ? new PlayersManagerYAML() : new PlayersManagerDB(db); + if (db == null && backupDir != null) createPlayerDatasBackup(backupDir, (PlayersManagerYAML) PlayersManager.manager); + PlayersManager.manager.load(); }catch (Exception ex) { - createDataBackup(); + if (backupDir == null) createDataBackup(backupDir()); logger.severe("Error while loading player datas.", ex); } @@ -512,7 +502,6 @@ public void saveAllConfig(boolean unload) throws Exception { try { PlayersManager.manager.save(); }catch (Exception ex) { - createDataBackup(); logger.severe("Error when saving player datas.", ex); } data.save(dataFile); @@ -528,20 +517,27 @@ public void saveAllConfig(boolean unload) throws Exception { private void resetDatas(){ quests = null; pools = null; - if (db != null) db.closeConnection(); + try { + if (db != null) db.close(); + }catch (Exception ex) { + logger.severe("An error occurred while closing database connection.", ex); + } + PlayersManager.manager = null; //HandlerList.unregisterAll(this); loaded = false; } /* ---------- Backups ---------- */ - public boolean createFolderBackup() { + public boolean createFolderBackup(Path backup) { if (!QuestsConfiguration.backups) return false; logger.info("Creating quests backup..."); - Path backupDir = backupDir(); + Path backupDir = backup.resolve("quests"); Path saveFolderPath = saveFolder.toPath(); try (Stream stream = Files.walk(saveFolderPath)) { + Files.createDirectories(backupDir); stream.forEach(path -> { + if (path.equals(saveFolderPath)) return; try { Files.copy(path, backupDir.resolve(saveFolderPath.relativize(path))); }catch (IOException ex) { @@ -556,11 +552,41 @@ public boolean createFolderBackup() { } } - public boolean createDataBackup() { + public boolean createDataBackup(Path backup) { if (!QuestsConfiguration.backups) return false; logger.info("Creating data backup..."); try{ - logger.info("Datas backup created in " + Files.copy(dataFile.toPath(), backupDir().resolve("data.yml")).getParent().getFileName()); + Path target = backup.resolve("data.yml"); + if (Files.exists(target)) { + logger.warning("File " + target.toString() + " already exist. This should not happen."); + }else { + Files.createDirectories(backup); + logger.info("Datas backup created in " + Files.copy(dataFile.toPath(), target).getParent().getFileName()); + } + return true; + }catch (Exception e) { + logger.severe("An error occured while creating the backup.", e); + return false; + } + } + + public boolean createPlayerDatasBackup(Path backup, PlayersManagerYAML yamlManager) { + if (!QuestsConfiguration.backups) return false; + + logger.info("Creating player datas backup..."); + Path backupDir = backup.resolve("players"); + Path playersFolderPath = yamlManager.getDirectory().toPath(); + try (Stream stream = Files.walk(playersFolderPath)) { + Files.createDirectories(backupDir); + stream.forEach(path -> { + if (path.equals(playersFolderPath)) return; + try { + Files.copy(path, backupDir.resolve(playersFolderPath.relativize(path))); + }catch (IOException ex) { + throw new RuntimeException(ex); + } + }); + logger.info("Player datas backup created in " + backupDir.getFileName().toString()); return true; }catch (Exception e) { logger.severe("An error occured while creating the backup.", e); @@ -572,7 +598,12 @@ public boolean createQuestBackup(Path file, String msg) { if (!QuestsConfiguration.backups) return false; logger.info("Creating single quest backup..."); try{ - logger.info("Quest backup created at " + Files.copy(file, Paths.get(file.toString() + "-backup" + format.format(new Date()) + ".yml")).getFileName()); + Path target = Paths.get(file.toString() + "-backup" + format.format(new Date()) + ".yml"); + if (Files.exists(target)) { + logger.warning("File " + target.toString() + " already exist. This should not happen."); + }else { + logger.info("Quest backup created at " + Files.copy(file, target).getFileName()); + } return true; }catch (Exception e) { logger.severe("An error occured while creating the backup.", e); @@ -582,7 +613,7 @@ public boolean createQuestBackup(Path file, String msg) { private SimpleDateFormat format = new SimpleDateFormat("yyyy'-'MM'-'dd'-'hh'-'mm'-'ss"); - private Path backupDir() { + public Path backupDir() { return getDataFolder().toPath().resolve("backup-" + format.format(new Date())); } @@ -624,6 +655,10 @@ public void run() { }.runTaskLater(BeautyQuests.getInstance(), 20L); } + public CommandsManager getCommand() { + return command; + } + public QuestsConfiguration getConfiguration() { return config; } @@ -651,13 +686,17 @@ public QuestPoolsManager getPoolsManager() { public ILoggerHandler getLoggerHandler() { return loggerHandler == null ? ILoggerHandler.EMPTY_LOGGER : loggerHandler; } + + public boolean isRunningPaper() { + return isPaper; + } public static BeautyQuests getInstance(){ return instance; } - class LoadingException extends Exception { + public static class LoadingException extends Exception { private static final long serialVersionUID = -2811265488885752109L; private String loggerMessage; @@ -670,6 +709,11 @@ public LoadingException(String loggerMessage, Throwable cause) { super(cause); this.loggerMessage = loggerMessage; } + + public String getLoggerMessage() { + return loggerMessage; + } + } } diff --git a/core/src/main/java/fr/skytasul/quests/QuestsConfiguration.java b/core/src/main/java/fr/skytasul/quests/QuestsConfiguration.java index 04190097..14195d54 100644 --- a/core/src/main/java/fr/skytasul/quests/QuestsConfiguration.java +++ b/core/src/main/java/fr/skytasul/quests/QuestsConfiguration.java @@ -1,10 +1,11 @@ package fr.skytasul.quests; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.Set; import java.util.stream.Collectors; - import org.bukkit.Bukkit; import org.bukkit.Color; import org.bukkit.FireworkEffect; @@ -14,14 +15,12 @@ import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.FireworkMeta; - import com.google.common.collect.Sets; - import fr.skytasul.quests.api.QuestsAPI; +import fr.skytasul.quests.api.options.description.QuestDescription; import fr.skytasul.quests.gui.ItemUtils; import fr.skytasul.quests.gui.quests.PlayerListGUI.Category; import fr.skytasul.quests.structure.QuestBranch.Source; -import fr.skytasul.quests.structure.QuestDescription; import fr.skytasul.quests.utils.Lang; import fr.skytasul.quests.utils.MinecraftNames; import fr.skytasul.quests.utils.ParticleEffect; @@ -54,13 +53,14 @@ public class QuestsConfiguration { private static boolean mobsProgressBar = false; private static int progressBarTimeoutSeconds = 15; private static boolean hookAcounts = false; + private static boolean usePlayerBlockTracker = false; private static ParticleEffect particleStart; private static ParticleEffect particleTalk; private static ParticleEffect particleNext; private static boolean sendUpdate = true; private static boolean stageStart = true; private static boolean questConfirmGUI = false; - private static ClickType npcClick = ClickType.RIGHT; + private static Collection npcClicks = Arrays.asList(ClickType.RIGHT, ClickType.SHIFT_RIGHT); private static String dSetName = "Quests"; private static String dIcon = "bookshelf"; private static int dMinZoom = 0; @@ -166,9 +166,18 @@ void init() { mobsProgressBar = config.getBoolean("mobsProgressBar"); progressBarTimeoutSeconds = config.getInt("progressBarTimeoutSeconds"); try { - npcClick = ClickType.valueOf(config.getString("npcClick").toUpperCase()); + if (config.isString("npcClick")) { + String click = config.getString("npcClick"); + npcClicks = Arrays.asList(click.equals("ANY") ? ClickType.values() : new ClickType[] { ClickType.valueOf(click.toUpperCase()) }); + }else { + npcClicks = config.getStringList("npcClick") + .stream() + .map(String::toUpperCase) + .map(ClickType::valueOf) + .collect(Collectors.toList()); + } }catch (IllegalArgumentException ex) { - BeautyQuests.logger.warning("Unknown click type " + config.getString("npcClick") + " for config entry \"npcClick\""); + BeautyQuests.logger.warning("Unknown click type " + config.get("npcClick") + " for config entry \"npcClick\""); } enablePrefix = config.getBoolean("enablePrefix"); disableTextHologram = config.getBoolean("disableTextHologram"); @@ -179,6 +188,7 @@ void init() { Bukkit.getPluginManager().registerEvents(new Accounts(), BeautyQuests.getInstance()); BeautyQuests.logger.info("AccountsHook is now managing player datas for quests !"); } + usePlayerBlockTracker = DependenciesManager.PlayerBlockTracker.isEnabled() && config.getBoolean("usePlayerBlockTracker"); dSetName = config.getString("dynmap.markerSetName"); if (dSetName == null || dSetName.isEmpty()) DependenciesManager.dyn.disable(); dIcon = config.getString("dynmap.markerIcon"); @@ -305,8 +315,8 @@ public static int getProgressBarTimeout(){ return progressBarTimeoutSeconds; } - public static ClickType getNPCClick() { - return npcClick; + public static Collection getNPCClicks() { + return npcClicks; } public static boolean handleGPS(){ @@ -417,6 +427,10 @@ public static boolean hookAccounts(){ return hookAcounts; } + public static boolean usePlayerBlockTracker() { + return usePlayerBlockTracker; + } + public static String dynmapSetName(){ return dSetName; } @@ -476,10 +490,14 @@ public static QuestsMenuConfig getMenuConfig() { } public enum ClickType { - RIGHT, LEFT, ANY; + RIGHT, SHIFT_RIGHT, LEFT, SHIFT_LEFT; - public boolean applies(ClickType type) { - return (this == type) || (this == ANY) || (type == ANY); + public static ClickType of(boolean left, boolean shift) { + if (left) { + return shift ? SHIFT_LEFT : LEFT; + }else { + return shift ? SHIFT_RIGHT : RIGHT; + } } } diff --git a/core/src/main/java/fr/skytasul/quests/QuestsListener.java b/core/src/main/java/fr/skytasul/quests/QuestsListener.java index 804b6896..34b3b6c7 100644 --- a/core/src/main/java/fr/skytasul/quests/QuestsListener.java +++ b/core/src/main/java/fr/skytasul/quests/QuestsListener.java @@ -5,14 +5,15 @@ import java.util.List; import java.util.Set; import java.util.stream.Collectors; - import org.bukkit.Bukkit; +import org.bukkit.Material; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.block.BlockBreakEvent; import org.bukkit.event.entity.EntityDamageByEntityEvent; +import org.bukkit.event.entity.PlayerDeathEvent; import org.bukkit.event.inventory.CraftItemEvent; import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.event.inventory.InventoryCloseEvent; @@ -22,11 +23,14 @@ import org.bukkit.event.player.PlayerItemConsumeEvent; import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.inventory.ComplexRecipe; import org.bukkit.inventory.ItemStack; - import fr.skytasul.quests.api.QuestsAPI; import fr.skytasul.quests.api.events.BQBlockBreakEvent; +import fr.skytasul.quests.api.events.BQCraftEvent; import fr.skytasul.quests.api.events.BQNPCClickEvent; +import fr.skytasul.quests.api.events.accounts.PlayerAccountJoinEvent; +import fr.skytasul.quests.api.events.accounts.PlayerAccountLeaveEvent; import fr.skytasul.quests.api.npcs.BQNPC; import fr.skytasul.quests.api.options.QuestOption; import fr.skytasul.quests.gui.Inventories; @@ -36,21 +40,20 @@ import fr.skytasul.quests.options.OptionAutoQuest; import fr.skytasul.quests.players.PlayerAccount; import fr.skytasul.quests.players.PlayersManager; -import fr.skytasul.quests.players.events.PlayerAccountJoinEvent; -import fr.skytasul.quests.players.events.PlayerAccountLeaveEvent; import fr.skytasul.quests.structure.Quest; import fr.skytasul.quests.structure.pools.QuestPool; import fr.skytasul.quests.utils.DebugUtils; import fr.skytasul.quests.utils.Lang; import fr.skytasul.quests.utils.Utils; import fr.skytasul.quests.utils.XMaterial; +import fr.skytasul.quests.utils.compatibility.Paper; public class QuestsListener implements Listener{ @EventHandler (priority = EventPriority.HIGHEST, ignoreCancelled = true) public void onNPCClick(BQNPCClickEvent e) { if (e.isCancelled()) return; - if (!QuestsConfiguration.getNPCClick().applies(e.getClick())) return; + if (!QuestsConfiguration.getNPCClicks().contains(e.getClick())) return; Player p = e.getPlayer(); BQNPC npc = e.getNPC(); @@ -198,11 +201,40 @@ public void onEat(PlayerItemConsumeEvent e) { } } - @EventHandler (priority = EventPriority.MONITOR) + @EventHandler (priority = EventPriority.HIGH) + public void onDeath(PlayerDeathEvent e) { + if (BeautyQuests.getInstance().isRunningPaper()) Paper.handleDeathItems(e, Utils::isQuestItem); + } + + @EventHandler(priority = EventPriority.HIGHEST) public void onBreak(BlockBreakEvent e) { if (e.isCancelled()) return; if (e.getPlayer() == null) return; Bukkit.getPluginManager().callEvent(new BQBlockBreakEvent(e.getPlayer(), Arrays.asList(e.getBlock()))); } + + @EventHandler (priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onCraftMonitor(CraftItemEvent e){ + if (e.getInventory().getResult() == null) return; + + int resultCount = e.getInventory().getResult().getAmount(); + int materialCount = Integer.MAX_VALUE; + + for (ItemStack is : e.getInventory().getMatrix()) + if (is != null && is.getAmount() < materialCount) + materialCount = is.getAmount(); + + int maxCraftAmount = resultCount * materialCount; + + ItemStack item = e.getRecipe().getResult(); + if (item.getType() == Material.AIR && e.getRecipe() instanceof ComplexRecipe) { + String key = ((ComplexRecipe) e.getRecipe()).getKey().toString(); + if (key.equals("minecraft:suspicious_stew")) { + item = XMaterial.SUSPICIOUS_STEW.parseItem(); + } + } + + Bukkit.getPluginManager().callEvent(new BQCraftEvent(e, item, maxCraftAmount)); + } } diff --git a/core/src/main/java/fr/skytasul/quests/api/Locale.java b/core/src/main/java/fr/skytasul/quests/api/Locale.java new file mode 100644 index 00000000..21ca0eb6 --- /dev/null +++ b/core/src/main/java/fr/skytasul/quests/api/Locale.java @@ -0,0 +1,101 @@ +package fr.skytasul.quests.api; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.function.Supplier; + +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.Plugin; + +import fr.skytasul.quests.utils.ChatUtils; +import fr.skytasul.quests.utils.DebugUtils; +import fr.skytasul.quests.utils.Utils; + +public interface Locale { + + String getPath(); + + String getValue(); + + void setValue(String value); + + default String format(Object... replace) { + return Utils.format(getValue(), replace); + } + + default String format(Supplier... replace) { + return Utils.format(getValue(), replace); + } + + default void send(CommandSender sender, Object... args) { + Utils.sendMessage(sender, getValue(), args); + } + + default void sendWP(CommandSender p, Object... args) { + Utils.sendMessageWP(p, getValue(), args); + } + + public static void loadStrings(Locale[] locales, YamlConfiguration defaultConfig, YamlConfiguration config) { + for (Locale l : locales) { + String value = config.getString(l.getPath(), null); + if (value == null) value = defaultConfig.getString(l.getPath(), null); + if (value == null) DebugUtils.logMessage("Unavailable string in config for key " + l.getPath()); + l.setValue(ChatUtils.translateHexColorCodes(ChatColor.translateAlternateColorCodes('&', value == null ? "§cunknown string" : value))); + } + } + + public static YamlConfiguration loadLang(Plugin plugin, Locale[] locales, String loadedLanguage) throws IOException, URISyntaxException { + long lastMillis = System.currentTimeMillis(); + + Utils.walkResources(plugin.getClass(), "/locales", 1, path -> { + String localeFileName = path.getFileName().toString(); + if (!localeFileName.toLowerCase().endsWith(".yml")) return; + + if (!Files.exists(plugin.getDataFolder().toPath().resolve("locales").resolve(localeFileName))) { + plugin.saveResource("locales/" + localeFileName, false); + } + }); + + String language = "locales/" + loadedLanguage + ".yml"; + File file = new File(plugin.getDataFolder(), language); + InputStream res = plugin.getResource(language); + boolean created = false; + if (!file.exists()) { + plugin.getLogger().warning("Language file " + language + " does not exist. Using default english strings."); + file.createNewFile(); + res = plugin.getResource("locales/en_US.yml"); + created = true; + } + YamlConfiguration conf = YamlConfiguration.loadConfiguration(file); + boolean changes = false; + if (res != null) { // if it's a local resource + YamlConfiguration def = YamlConfiguration.loadConfiguration(new InputStreamReader(res, StandardCharsets.UTF_8)); + for (String key : def.getKeys(true)) { // get all keys in resource + if (!def.isConfigurationSection(key)) { // if not a block + if (!conf.contains(key)) { // if string does not exist in the file + conf.set(key, def.get(key)); // copy string + if (!created) DebugUtils.logMessage("String copied from source file to " + language + ". Key: " + key); + changes = true; + } + } + } + } + loadStrings(locales, YamlConfiguration.loadConfiguration(new InputStreamReader(plugin.getResource("locales/en_US.yml"), StandardCharsets.UTF_8)), conf); + + if (changes) { + plugin.getLogger().info("Copied new strings into " + language + " language file."); + conf.save(file); // if there has been changes before, save the edited file + } + + plugin.getLogger().info("Loaded language " + loadedLanguage + " (" + (((double) System.currentTimeMillis() - lastMillis) / 1000D) + "s)!"); + return conf; + } + +} \ No newline at end of file diff --git a/core/src/main/java/fr/skytasul/quests/api/QuestsAPI.java b/core/src/main/java/fr/skytasul/quests/api/QuestsAPI.java index 5bb5579c..ae98ada1 100644 --- a/core/src/main/java/fr/skytasul/quests/api/QuestsAPI.java +++ b/core/src/main/java/fr/skytasul/quests/api/QuestsAPI.java @@ -1,20 +1,20 @@ package fr.skytasul.quests.api; +import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; import java.util.function.Consumer; - import org.apache.commons.lang.Validate; import org.bukkit.Bukkit; import org.bukkit.event.HandlerList; - import fr.skytasul.quests.BeautyQuests; import fr.skytasul.quests.api.bossbar.BQBossBarManager; import fr.skytasul.quests.api.comparison.ItemComparison; import fr.skytasul.quests.api.mobs.MobFactory; +import fr.skytasul.quests.api.mobs.MobStacker; import fr.skytasul.quests.api.npcs.BQNPCsManager; import fr.skytasul.quests.api.objects.QuestObjectsRegistry; import fr.skytasul.quests.api.options.QuestOptionCreator; @@ -24,17 +24,23 @@ import fr.skytasul.quests.api.rewards.RewardCreator; import fr.skytasul.quests.api.stages.AbstractStage; import fr.skytasul.quests.api.stages.StageType; +import fr.skytasul.quests.api.stages.StageTypeRegistry; import fr.skytasul.quests.structure.QuestsManager; import fr.skytasul.quests.structure.pools.QuestPoolsManager; import fr.skytasul.quests.utils.DebugUtils; import fr.skytasul.quests.utils.Lang; +/** + * This class contains most of the useful accessors to fetch data from BeautyQuests + * and methods to implement custom behaviors. + */ public final class QuestsAPI { - private static final QuestObjectsRegistry requirements = new QuestObjectsRegistry<>(Lang.INVENTORY_REQUIREMENTS.toString()); - private static final QuestObjectsRegistry rewards = new QuestObjectsRegistry<>(Lang.INVENTORY_REWARDS.toString()); - public static final List> stages = new LinkedList<>(); - public static final List itemComparisons = new LinkedList<>(); + private static final QuestObjectsRegistry requirements = new QuestObjectsRegistry<>("requirements", Lang.INVENTORY_REQUIREMENTS.toString()); + private static final QuestObjectsRegistry rewards = new QuestObjectsRegistry<>("rewards", Lang.INVENTORY_REWARDS.toString()); + private static final StageTypeRegistry stages = new StageTypeRegistry(); + private static final List itemComparisons = new LinkedList<>(); + private static final List mobStackers = new ArrayList<>(); private static BQNPCsManager npcsManager = null; private static AbstractHolograms hologramsManager = null; @@ -46,11 +52,16 @@ private QuestsAPI() {} /** * Register new stage type into the plugin - * @param creator StageType instance + * @param type StageType instance + * @deprecated use {@link StageTypeRegistry#register(StageType)} */ - public static void registerStage(StageType creator) { - stages.add(creator); - DebugUtils.logMessage("Stage registered (" + creator.name + ", " + (stages.size() - 1) + ")"); + @Deprecated + public static void registerStage(StageType type) { // TODO remove, edited on 0.20 + stages.register(type); + } + + public static StageTypeRegistry getStages() { + return stages; } /** @@ -70,11 +81,25 @@ public static void registerQuestOption(QuestOptionCreator creator) { DebugUtils.logMessage("Quest option registered (id: " + creator.id + ")"); } + public static List getItemComparisons() { + return itemComparisons; + } + public static void registerItemComparison(ItemComparison comparison) { + Validate.isTrue(itemComparisons.stream().noneMatch(x -> x.getID().equals(comparison.getID())), "This item comparison was already registerd"); itemComparisons.add(comparison); DebugUtils.logMessage("Item comparison registered (id: " + comparison.getID() + ")"); } + public static List getMobStackers() { + return mobStackers; + } + + public static void registerMobStacker(MobStacker stacker) { + mobStackers.add(stacker); + DebugUtils.logMessage("Added " + stacker.toString() + " mob stacker"); + } + public static QuestObjectsRegistry getRequirements() { return requirements; } diff --git a/core/src/main/java/fr/skytasul/quests/api/QuestsHandler.java b/core/src/main/java/fr/skytasul/quests/api/QuestsHandler.java index f59c22d8..4d89d34b 100644 --- a/core/src/main/java/fr/skytasul/quests/api/QuestsHandler.java +++ b/core/src/main/java/fr/skytasul/quests/api/QuestsHandler.java @@ -2,11 +2,11 @@ import org.bukkit.entity.Player; -import fr.skytasul.quests.api.stages.AbstractStage; +import fr.skytasul.quests.api.stages.StageHandler; import fr.skytasul.quests.players.PlayerAccount; import fr.skytasul.quests.structure.Quest; -public interface QuestsHandler { +public interface QuestsHandler extends StageHandler { public default void load() {} @@ -30,16 +30,4 @@ public default void questReset(PlayerAccount acc, Quest quest) {} public default void questUpdated(PlayerAccount acc, Player p, Quest quest) {} - public default void stageStart(PlayerAccount acc, AbstractStage stage) {} - - public default void stageEnd(PlayerAccount acc, AbstractStage stage) {} - - public default void stageJoin(PlayerAccount acc, Player p, AbstractStage stage) {} - - public default void stageLeave(PlayerAccount acc, Player p, AbstractStage stage) {} - - public default void stageLoad(AbstractStage stage) {} - - public default void stageUnload(AbstractStage stage) {} - } diff --git a/core/src/main/java/fr/skytasul/quests/api/comparison/ItemComparisonMap.java b/core/src/main/java/fr/skytasul/quests/api/comparison/ItemComparisonMap.java index 0796c083..15394563 100644 --- a/core/src/main/java/fr/skytasul/quests/api/comparison/ItemComparisonMap.java +++ b/core/src/main/java/fr/skytasul/quests/api/comparison/ItemComparisonMap.java @@ -6,6 +6,7 @@ import java.util.Map; import org.bukkit.Material; +import org.bukkit.configuration.ConfigurationSection; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemStack; @@ -20,15 +21,28 @@ public ItemComparisonMap() { this(new HashMap<>()); } + public ItemComparisonMap(ConfigurationSection notDefault) { + setNotDefaultComparisons(notDefault); + } + public ItemComparisonMap(Map notDefault) { setNotDefaultComparisons(notDefault); } + public void setNotDefaultComparisons(ConfigurationSection section) { + this.notDefault = (Map) section.getValues(false); + + effective = new ArrayList<>(); + for (ItemComparison comp : QuestsAPI.getItemComparisons()) { + if (section.getBoolean(comp.getID(), comp.isEnabledByDefault())) effective.add(comp); + } + } + public void setNotDefaultComparisons(Map comparisons) { this.notDefault = comparisons; effective = new ArrayList<>(); - for (ItemComparison comp : QuestsAPI.itemComparisons) { + for (ItemComparison comp : QuestsAPI.getItemComparisons()) { Boolean bool = notDefault.get(comp.getID()); if (Boolean.FALSE.equals(bool)) continue; if (!comp.isEnabledByDefault() && !Boolean.TRUE.equals(bool)) continue; @@ -40,6 +54,10 @@ public Map getNotDefault() { return notDefault; } + public boolean isDefault() { + return notDefault.isEmpty(); + } + public List getEffective() { return effective; } diff --git a/core/src/main/java/fr/skytasul/quests/api/data/SQLDataSaver.java b/core/src/main/java/fr/skytasul/quests/api/data/SQLDataSaver.java new file mode 100644 index 00000000..aa121302 --- /dev/null +++ b/core/src/main/java/fr/skytasul/quests/api/data/SQLDataSaver.java @@ -0,0 +1,160 @@ +package fr.skytasul.quests.api.data; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.sql.Types; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import com.google.common.collect.ImmutableMap; + +import fr.skytasul.quests.utils.CustomizedObjectTypeAdapter; + +public class SQLDataSaver { + + private static final SQLType TYPE_DATE = new SQLType(Types.TIMESTAMP, "TIMESTAMP", (resultSet, column) -> new Date(resultSet.getTimestamp(column).getTime())) { + @Override + public Object convert(Date obj) { + return new Timestamp(obj.getTime()); + } + }; + private static final SQLType TYPE_CHAR = new SQLType(Types.CHAR, "CHAR(1)", (resultSet, column) -> resultSet.getString(column).charAt(0)) { + @Override + public Object convert(Character obj) { + return obj.toString(); + } + }.omitLength(); + private static final SQLType TYPE_STRING = new SQLType(Types.VARCHAR, "VARCHAR", ResultSet::getString).requiresLength(); + private static final SQLType TYPE_BOOLEAN = new SQLType<>(Types.BOOLEAN, "BOOLEAN", ResultSet::getBoolean); + private static final SQLType TYPE_FLOAT = new SQLType<>(Types.FLOAT, "FLOAT", ResultSet::getFloat); + private static final SQLType TYPE_DOUBLE = new SQLType<>(Types.DOUBLE, "DOUBLE", ResultSet::getDouble); + private static final SQLType TYPE_BIGINT = new SQLType<>(Types.BIGINT, "BIGINT", ResultSet::getLong); + private static final SQLType TYPE_INT = new SQLType<>(Types.INTEGER, "INTEGER", ResultSet::getInt); + private static final SQLType TYPE_SMALLINT = new SQLType<>(Types.SMALLINT, "SMALLINT", ResultSet::getShort); + private static final SQLType TYPE_TINYINT = new SQLType<>(Types.TINYINT, "TINYINT", ResultSet::getByte); + + private static final Map, SQLType> SQL_TYPES = new HashMap<>(ImmutableMap., SQLType>builder() + .put(byte.class, TYPE_TINYINT) + .put(Byte.class, TYPE_TINYINT) + .put(short.class, TYPE_SMALLINT) + .put(Short.class, TYPE_SMALLINT) + .put(int.class, TYPE_INT) + .put(Integer.class, TYPE_INT) + .put(long.class, TYPE_BIGINT) + .put(Long.class, TYPE_BIGINT) + .put(double.class, TYPE_DOUBLE) + .put(Double.class, TYPE_DOUBLE) + .put(float.class, TYPE_FLOAT) + .put(Float.class, TYPE_FLOAT) + .put(boolean.class, TYPE_BOOLEAN) + .put(Boolean.class, TYPE_BOOLEAN) + .put(char.class, TYPE_CHAR) + .put(Character.class, TYPE_CHAR) + .put(String.class, TYPE_STRING) + .put(Date.class, TYPE_DATE) + .build()); + + private final SavableData wrappedData; + private final SQLType sqlType; + private final String updateStatement; + private final String columnDefinition; + private final String defaultValueString; + + public SQLDataSaver(SavableData wrappedData, String updateStatement) { + this.wrappedData = wrappedData; + this.updateStatement = updateStatement; + + sqlType = (SQLType) SQL_TYPES.computeIfAbsent(wrappedData.getDataType(), JsonSQLType::new); + + String length = ""; + if (!sqlType.omitLength) { + if (wrappedData.getMaxLength().isPresent()) { + length = "(" + wrappedData.getMaxLength().getAsInt() + ")"; + }else { + if (sqlType.requiresLength) + throw new IllegalArgumentException("Column " + wrappedData.getColumnName() + " requires a max length."); + } + } + + defaultValueString = Objects.toString(wrappedData.getDefaultValue()); + columnDefinition = String.format("`%s` %s%s DEFAULT %s", wrappedData.getColumnName(), sqlType.sqlTypeName, length, defaultValueString); + } + + public SavableData getWrappedData() { + return wrappedData; + } + + public String getUpdateStatement() { + return updateStatement; + } + + public String getColumnDefinition() { + return columnDefinition; + } + + public String getDefaultValueString() { + return defaultValueString; + } + + public void setInStatement(PreparedStatement statement, int index, T value) throws SQLException { + statement.setObject(index, sqlType.convert(value), sqlType.jdbcTypeCode); + } + + public T getFromResultSet(ResultSet resultSet) throws SQLException { + return sqlType.getter.get(resultSet, wrappedData.getColumnName()); + } + + private static class SQLType { + private final int jdbcTypeCode; + private final String sqlTypeName; + private final ResultSetProcessor getter; + + private boolean requiresLength = false; + private boolean omitLength = false; + + private SQLType(int jdbcTypeCode, String sqlTypeName, ResultSetProcessor getter) { + this.jdbcTypeCode = jdbcTypeCode; + this.sqlTypeName = sqlTypeName; + this.getter = getter; + } + + public SQLType requiresLength() { + requiresLength = true; + return this; + } + + public SQLType omitLength() { + omitLength = true; + return this; + } + + public Object convert(T obj) { + return obj; + } + + } + + private static class JsonSQLType extends SQLType { + private JsonSQLType(Class type) { + super(Types.VARCHAR, "JSON", (resultSet, column) -> { + String json = resultSet.getString(column); + return CustomizedObjectTypeAdapter.GSON.fromJson(json, type); + }); + } + + @Override + public Object convert(T obj) { + return CustomizedObjectTypeAdapter.GSON.toJson(obj); + } + } + + @FunctionalInterface + public static interface ResultSetProcessor { + T get(ResultSet resultSet, String column) throws SQLException; + } + +} diff --git a/core/src/main/java/fr/skytasul/quests/api/data/SavableData.java b/core/src/main/java/fr/skytasul/quests/api/data/SavableData.java new file mode 100644 index 00000000..c9ea7839 --- /dev/null +++ b/core/src/main/java/fr/skytasul/quests/api/data/SavableData.java @@ -0,0 +1,75 @@ +package fr.skytasul.quests.api.data; + +import java.util.Objects; +import java.util.OptionalInt; + +public final class SavableData { + + private final String id; + private final Class dataType; + private final T defaultValue; + private final OptionalInt maxLength; + + private String columnName; + + public SavableData(String id, Class dataType, T defaultValue) { + this(id, dataType, defaultValue, OptionalInt.empty()); + } + + public SavableData(String id, Class dataType, T defaultValue, OptionalInt maxLength) { + if (id == null || id.isEmpty()) throw new IllegalArgumentException("Data id cannot be null or empty"); + if (dataType == null) throw new IllegalArgumentException("Data type cannot be null"); + if (maxLength == null) throw new IllegalArgumentException("Data max length cannot be a null optional"); + this.id = id; + this.dataType = dataType; + this.defaultValue = defaultValue; + this.maxLength = maxLength; + } + + public String getId() { + return id; + } + + public Class getDataType() { + return dataType; + } + + public T getDefaultValue() { + return defaultValue; + } + + public OptionalInt getMaxLength() { + return maxLength; + } + + public String getColumnName() { + return columnName == null ? id : columnName; + } + + public void setColumnName(String columnName) { + this.columnName = columnName; + } + + @Override + public int hashCode() { + int hash = 7; + + hash = hash * 23 + id.hashCode(); + hash = hash * 23 + dataType.hashCode(); + hash = hash * 23 + maxLength.hashCode(); + hash = hash * 23 + Objects.hashCode(defaultValue); + + return hash; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof SavableData)) return false; + SavableData oth = (SavableData) obj; + return oth.id.equals(id) + && oth.dataType.equals(dataType) + && Objects.equals(oth.defaultValue, defaultValue) + && oth.maxLength.equals(maxLength); + } + +} diff --git a/core/src/main/java/fr/skytasul/quests/api/events/BQCraftEvent.java b/core/src/main/java/fr/skytasul/quests/api/events/BQCraftEvent.java new file mode 100644 index 00000000..da6d2eed --- /dev/null +++ b/core/src/main/java/fr/skytasul/quests/api/events/BQCraftEvent.java @@ -0,0 +1,45 @@ +package fr.skytasul.quests.api.events; + +import org.bukkit.entity.Player; +import org.bukkit.event.HandlerList; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.player.PlayerEvent; +import org.bukkit.inventory.ItemStack; + +public class BQCraftEvent extends PlayerEvent { + + private final InventoryClickEvent clickEvent; + private final ItemStack result; + private final int maxCraftable; + + public BQCraftEvent(InventoryClickEvent clickEvent, ItemStack result, int maxCraftable) { + super((Player) clickEvent.getView().getPlayer()); + this.clickEvent = clickEvent; + this.result = result; + this.maxCraftable = maxCraftable; + } + + public InventoryClickEvent getClickEvent() { + return clickEvent; + } + + public ItemStack getResult() { + return result; + } + + public int getMaxCraftable() { + return maxCraftable; + } + + @Override + public HandlerList getHandlers() { + return handlers; + } + + public static HandlerList getHandlerList() { + return handlers; + } + + private static final HandlerList handlers = new HandlerList(); + +} diff --git a/core/src/main/java/fr/skytasul/quests/api/events/accounts/PlayerAccountEvent.java b/core/src/main/java/fr/skytasul/quests/api/events/accounts/PlayerAccountEvent.java new file mode 100644 index 00000000..26be1092 --- /dev/null +++ b/core/src/main/java/fr/skytasul/quests/api/events/accounts/PlayerAccountEvent.java @@ -0,0 +1,30 @@ +package fr.skytasul.quests.api.events.accounts; + +import org.bukkit.entity.Player; +import org.bukkit.event.Event; + +import fr.skytasul.quests.players.PlayerAccount; + +public abstract class PlayerAccountEvent extends Event { + + private Player who; + private PlayerAccount account; + + protected PlayerAccountEvent(Player who, PlayerAccount account) { + this.who = who; + this.account = account; + } + + public boolean isAccountCurrent() { + return who != null; + } + + public Player getPlayer() { + return who; + } + + public PlayerAccount getPlayerAccount() { + return account; + } + +} \ No newline at end of file diff --git a/core/src/main/java/fr/skytasul/quests/players/events/PlayerAccountJoinEvent.java b/core/src/main/java/fr/skytasul/quests/api/events/accounts/PlayerAccountJoinEvent.java similarity index 83% rename from core/src/main/java/fr/skytasul/quests/players/events/PlayerAccountJoinEvent.java rename to core/src/main/java/fr/skytasul/quests/api/events/accounts/PlayerAccountJoinEvent.java index bc61d03e..264c4862 100644 --- a/core/src/main/java/fr/skytasul/quests/players/events/PlayerAccountJoinEvent.java +++ b/core/src/main/java/fr/skytasul/quests/api/events/accounts/PlayerAccountJoinEvent.java @@ -1,4 +1,4 @@ -package fr.skytasul.quests.players.events; +package fr.skytasul.quests.api.events.accounts; import org.bukkit.entity.Player; import org.bukkit.event.HandlerList; @@ -10,8 +10,7 @@ public class PlayerAccountJoinEvent extends PlayerAccountEvent { private boolean firstJoin; public PlayerAccountJoinEvent(Player who, PlayerAccount account, boolean firstJoin) { - super(who); - this.account = account; + super(who, account); this.firstJoin = firstJoin; } @@ -19,6 +18,7 @@ public boolean isFirstJoin() { return firstJoin; } + @Override public HandlerList getHandlers() { return handlers; } diff --git a/core/src/main/java/fr/skytasul/quests/players/events/PlayerAccountLeaveEvent.java b/core/src/main/java/fr/skytasul/quests/api/events/accounts/PlayerAccountLeaveEvent.java similarity index 80% rename from core/src/main/java/fr/skytasul/quests/players/events/PlayerAccountLeaveEvent.java rename to core/src/main/java/fr/skytasul/quests/api/events/accounts/PlayerAccountLeaveEvent.java index d516ad29..5c304316 100644 --- a/core/src/main/java/fr/skytasul/quests/players/events/PlayerAccountLeaveEvent.java +++ b/core/src/main/java/fr/skytasul/quests/api/events/accounts/PlayerAccountLeaveEvent.java @@ -1,4 +1,4 @@ -package fr.skytasul.quests.players.events; +package fr.skytasul.quests.api.events.accounts; import org.bukkit.entity.Player; import org.bukkit.event.HandlerList; @@ -8,10 +8,10 @@ public class PlayerAccountLeaveEvent extends PlayerAccountEvent { public PlayerAccountLeaveEvent(Player who, PlayerAccount account) { - super(who); - this.account = account; + super(who, account); } + @Override public HandlerList getHandlers() { return handlers; } diff --git a/core/src/main/java/fr/skytasul/quests/api/events/accounts/PlayerAccountResetEvent.java b/core/src/main/java/fr/skytasul/quests/api/events/accounts/PlayerAccountResetEvent.java new file mode 100644 index 00000000..fa2de5cc --- /dev/null +++ b/core/src/main/java/fr/skytasul/quests/api/events/accounts/PlayerAccountResetEvent.java @@ -0,0 +1,25 @@ +package fr.skytasul.quests.api.events.accounts; + +import org.bukkit.entity.Player; +import org.bukkit.event.HandlerList; + +import fr.skytasul.quests.players.PlayerAccount; + +public class PlayerAccountResetEvent extends PlayerAccountEvent { + + public PlayerAccountResetEvent(Player who, PlayerAccount account) { + super(who, account); + } + + @Override + public HandlerList getHandlers() { + return handlers; + } + + public static HandlerList getHandlerList() { + return handlers; + } + + private static final HandlerList handlers = new HandlerList(); + +} diff --git a/core/src/main/java/fr/skytasul/quests/api/mobs/LeveledMobFactory.java b/core/src/main/java/fr/skytasul/quests/api/mobs/LeveledMobFactory.java new file mode 100644 index 00000000..01e356aa --- /dev/null +++ b/core/src/main/java/fr/skytasul/quests/api/mobs/LeveledMobFactory.java @@ -0,0 +1,9 @@ +package fr.skytasul.quests.api.mobs; + +import org.bukkit.entity.Entity; + +public interface LeveledMobFactory extends MobFactory { + + double getMobLevel(T type, Entity entity); + +} diff --git a/core/src/main/java/fr/skytasul/quests/api/mobs/Mob.java b/core/src/main/java/fr/skytasul/quests/api/mobs/Mob.java index dd1a7fca..d7a0b4ef 100644 --- a/core/src/main/java/fr/skytasul/quests/api/mobs/Mob.java +++ b/core/src/main/java/fr/skytasul/quests/api/mobs/Mob.java @@ -1,61 +1,91 @@ package fr.skytasul.quests.api.mobs; -import java.util.ArrayList; +import java.text.DecimalFormat; +import java.text.NumberFormat; import java.util.HashMap; -import java.util.List; import java.util.Map; - import org.apache.commons.lang.Validate; -import org.bukkit.inventory.ItemStack; - +import org.bukkit.entity.Entity; import fr.skytasul.quests.BeautyQuests; -import fr.skytasul.quests.gui.ItemUtils; -import fr.skytasul.quests.utils.Lang; +import fr.skytasul.quests.utils.Utils; import fr.skytasul.quests.utils.XMaterial; -public class Mob implements Cloneable { +public class Mob implements Cloneable { + + private static final NumberFormat LEVEL_FORMAT = new DecimalFormat(); - protected final MobFactory factory; - protected final Data data; + protected final MobFactory factory; + protected final D data; protected String customName; + protected Double minLevel; - public Mob(MobFactory factory, Data data) { + private String formattedName; + + public Mob(MobFactory factory, D data) { Validate.notNull(factory, "Mob factory cannot be null"); Validate.notNull(data, "Mob data cannot be null"); this.factory = factory; this.data = data; } + public MobFactory getFactory() { + return factory; + } + + public D getData() { + return data; + } + public String getName() { - return customName == null ? factory.getName(data) : customName; + if (formattedName == null) { + if (customName != null) { + formattedName = customName; + } else { + formattedName = factory.getName(data); + + if (minLevel != null) + formattedName += " lvl " + LEVEL_FORMAT.format(minLevel.doubleValue()); + } + } + return formattedName; } public void setCustomName(String customName) { this.customName = customName; } - public ItemStack createItemStack(int amount) { - List lore = new ArrayList<>(); - lore.add(Lang.Amount.format(amount)); - lore.addAll(factory.getDescriptiveLore(data)); - lore.add(""); - lore.add(Lang.click.toString()); - XMaterial mobItem; + public Double getMinLevel() { + return minLevel; + } + + public void setMinLevel(Double minLevel) { + this.minLevel = minLevel; + } + + public XMaterial getMobItem() { try { - mobItem = XMaterial.mobItem(factory.getEntityType(data)); + return Utils.mobItem(factory.getEntityType(data)); }catch (Exception ex) { - mobItem = XMaterial.SPONGE; BeautyQuests.logger.warning("Unknow entity type for mob " + factory.getName(data), ex); + return XMaterial.SPONGE; } - ItemStack item = ItemUtils.item(mobItem, getName(), lore); - item.setAmount(Math.min(amount, 64)); - return item; } public boolean applies(Object data) { return factory.mobApplies(this.data, data); } + public boolean appliesEntity(Entity entity) { + return factory.bukkitMobApplies(data, entity); + } + + public double getLevel(Entity entity) { + if (!(factory instanceof LeveledMobFactory)) + throw new UnsupportedOperationException( + "Cannot get the level of a mob from an unleveled mob factory: " + factory.getID()); + return ((LeveledMobFactory) factory).getMobLevel(data, entity); + } + @Override public int hashCode() { int hash = 1; @@ -75,9 +105,9 @@ public boolean equals(Object obj) { } @Override - public Mob clone(){ + public Mob clone() { try { - return (Mob) super.clone(); + return (Mob) super.clone(); }catch (CloneNotSupportedException e) { e.printStackTrace(); return null; @@ -89,7 +119,10 @@ public Map serialize(){ map.put("factoryName", factory.getID()); map.put("value", factory.getValue(data)); - if (customName != null) map.put("name", customName); + if (customName != null) + map.put("name", customName); + if (minLevel != null) + map.put("minLevel", minLevel); return map; } @@ -104,7 +137,10 @@ public static Mob deserialize(Map map) { Object object = factory.fromValue(value); if (object == null) throw new IllegalArgumentException("Can't find the mob " + value + " for factory " + factoryName); Mob mob = new Mob(factory, object); - if (map.containsKey("name")) mob.setCustomName((String) map.get("name")); + if (map.containsKey("name")) + mob.setCustomName((String) map.get("name")); + if (map.containsKey("minLevel")) + mob.setMinLevel((Double) map.get("minLevel")); return mob; } diff --git a/core/src/main/java/fr/skytasul/quests/api/mobs/MobFactory.java b/core/src/main/java/fr/skytasul/quests/api/mobs/MobFactory.java index 8f32bf2c..a8f96782 100644 --- a/core/src/main/java/fr/skytasul/quests/api/mobs/MobFactory.java +++ b/core/src/main/java/fr/skytasul/quests/api/mobs/MobFactory.java @@ -1,120 +1,128 @@ -package fr.skytasul.quests.api.mobs; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.concurrent.TimeUnit; -import java.util.function.Consumer; - -import org.apache.commons.lang.Validate; -import org.bukkit.Bukkit; -import org.bukkit.entity.Entity; -import org.bukkit.entity.EntityType; -import org.bukkit.entity.Player; -import org.bukkit.event.Event; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.inventory.ItemStack; - -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; - -import fr.skytasul.quests.BeautyQuests; -import fr.skytasul.quests.utils.compatibility.mobs.CompatMobDeathEvent; - -/** - * This class implements {@link Listener} to permit the implementation to have at least one {@link EventHandler}. - * This event method will be used to fire the {@link #callEvent(Event, Object, Entity, Player)}. - * - * @param object which should represents a mob type from whatever plugin - */ -public abstract interface MobFactory extends Listener { - - /** - * @return internal ID of this Mob Factory - */ - public abstract String getID(); - - /** - * @return item which will represent this Mob Factory in the Mobs Create GUI - */ - public abstract ItemStack getFactoryItem(); - - /** - * Called when a player click on the {@link #getFactoryItem()} - * @param p Player who clicked on the item - * @param run - */ - public abstract void itemClick(Player p, Consumer run); - - /** - * @param value value returned from {@link #getValue(Object)} - * @return object created with the value - */ - public abstract T fromValue(String value); - - /** - * @param data object to get a String value from - * @return String value - */ - public abstract String getValue(T data); - - /** - * @param data object to get a name from - * @return name of the object - */ - public abstract String getName(T data); - - /** - * @param data object to get the entity type from - * @return entity type of the object - */ - public abstract EntityType getEntityType(T data); - - /** - * @param data object to get a description from - * @return list of string which will be displayed as the lore of the mob item - */ - public default List getDescriptiveLore(T data) { - return Collections.EMPTY_LIST; - } - - public default boolean mobApplies(T first, Object other) { - return Objects.equals(first, other); - } - - /** - * Has to be called when a mob corresponding to this factory has been killed - * @param originalEvent original event - * @param pluginMob mob killed - * @param entity bukkit entity killed - * @param player killer - */ - public default void callEvent(Event originalEvent, T pluginMob, Entity entity, Player player) { - Validate.notNull(pluginMob, "Plugin mob object cannot be null"); - Validate.notNull(entity, "Bukkit entity object cannot be null"); - Validate.notNull(player, "Player cannot be null"); - if (originalEvent != null) { - CompatMobDeathEvent existingCompat = eventsCache.getIfPresent(originalEvent); - if (existingCompat != null && mobApplies(pluginMob, existingCompat.getPluginMob())) { - BeautyQuests.logger.warning("MobFactory.callEvent() called twice!"); - return; - } - } - CompatMobDeathEvent compatEvent = new CompatMobDeathEvent(pluginMob, player, entity); - if (originalEvent != null) eventsCache.put(originalEvent, compatEvent); - Bukkit.getPluginManager().callEvent(compatEvent); - } - - static final Cache eventsCache = CacheBuilder.newBuilder().expireAfterWrite(2, TimeUnit.SECONDS).build(); - public static final List> factories = new ArrayList<>(); - - public static MobFactory getMobFactory(String id) { - for (MobFactory factory : factories) { - if (factory.getID().equals(id)) return factory; - } - return null; - } - -} +package fr.skytasul.quests.api.mobs; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.OptionalInt; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import org.apache.commons.lang.Validate; +import org.bukkit.Bukkit; +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; +import org.bukkit.event.Event; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.inventory.ItemStack; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import fr.skytasul.quests.BeautyQuests; +import fr.skytasul.quests.api.QuestsAPI; +import fr.skytasul.quests.utils.compatibility.mobs.CompatMobDeathEvent; + +/** + * This class implements {@link Listener} to permit the implementation to have at least one {@link EventHandler}. + * This event method will be used to fire the {@link #callEvent(Event, Object, Entity, Player)}. + * + * @param object which should represents a mob type from whatever plugin + */ +public abstract interface MobFactory extends Listener { + + /** + * @return internal ID of this Mob Factory + */ + public abstract String getID(); + + /** + * @return item which will represent this Mob Factory in the Mobs Create GUI + */ + public abstract ItemStack getFactoryItem(); + + /** + * Called when a player click on the {@link #getFactoryItem()} + * @param p Player who clicked on the item + * @param run + */ + public abstract void itemClick(Player p, Consumer run); + + /** + * @param value value returned from {@link #getValue(Object)} + * @return object created with the value + */ + public abstract T fromValue(String value); + + /** + * @param data object to get a String value from + * @return String value + */ + public abstract String getValue(T data); + + /** + * @param data object to get a name from + * @return name of the object + */ + public abstract String getName(T data); + + /** + * @param data object to get the entity type from + * @return entity type of the object + */ + public abstract EntityType getEntityType(T data); + + /** + * @param data object to get a description from + * @return list of string which will be displayed as the lore of the mob item + */ + public default List getDescriptiveLore(T data) { + return Collections.emptyList(); + } + + public default boolean mobApplies(T first, Object other) { + return Objects.equals(first, other); + } + + public default boolean bukkitMobApplies(T first, Entity entity) { // TODO abstract (introduced in 0.20) + BeautyQuests.logger.warning("The mob factory " + getID() + " has not been updated. Nag its author about it!"); + return false; + } + + /** + * Has to be called when a mob corresponding to this factory has been killed + * + * @param originalEvent original event + * @param pluginMob mob killed + * @param entity bukkit entity killed + * @param player killer + */ + public default void callEvent(Event originalEvent, T pluginMob, Entity entity, Player player) { + Validate.notNull(pluginMob, "Plugin mob object cannot be null"); + Validate.notNull(player, "Player cannot be null"); + if (originalEvent != null) { + CompatMobDeathEvent existingCompat = eventsCache.getIfPresent(originalEvent); + if (existingCompat != null && mobApplies(pluginMob, existingCompat.getPluginMob())) { + BeautyQuests.logger.warning("MobFactory.callEvent() called twice!"); + return; + } + } + + OptionalInt optionalStackSize = QuestsAPI.getMobStackers().stream() + .mapToInt(stacker -> stacker.getEntityStackSize(entity)).filter(size -> size > 1).max(); + + CompatMobDeathEvent compatEvent = new CompatMobDeathEvent(pluginMob, player, entity, optionalStackSize.orElse(1)); + if (originalEvent != null) eventsCache.put(originalEvent, compatEvent); + Bukkit.getPluginManager().callEvent(compatEvent); + } + + static final Cache eventsCache = CacheBuilder.newBuilder().expireAfterWrite(2, TimeUnit.SECONDS).build(); + public static final List> factories = new ArrayList<>(); + + public static MobFactory getMobFactory(String id) { + for (MobFactory factory : factories) { + if (factory.getID().equals(id)) return factory; + } + return null; + } + +} diff --git a/core/src/main/java/fr/skytasul/quests/api/mobs/MobStacker.java b/core/src/main/java/fr/skytasul/quests/api/mobs/MobStacker.java new file mode 100644 index 00000000..f8f804a8 --- /dev/null +++ b/core/src/main/java/fr/skytasul/quests/api/mobs/MobStacker.java @@ -0,0 +1,9 @@ +package fr.skytasul.quests.api.mobs; + +import org.bukkit.entity.Entity; + +public interface MobStacker { + + int getEntityStackSize(Entity entity); + +} diff --git a/core/src/main/java/fr/skytasul/quests/api/npcs/BQNPC.java b/core/src/main/java/fr/skytasul/quests/api/npcs/BQNPC.java index 29b5837e..286faad3 100644 --- a/core/src/main/java/fr/skytasul/quests/api/npcs/BQNPC.java +++ b/core/src/main/java/fr/skytasul/quests/api/npcs/BQNPC.java @@ -17,6 +17,7 @@ import fr.skytasul.quests.QuestsConfiguration; import fr.skytasul.quests.api.AbstractHolograms; import fr.skytasul.quests.api.QuestsAPI; +import fr.skytasul.quests.api.stages.types.Locatable.Located; import fr.skytasul.quests.options.OptionHologramLaunch; import fr.skytasul.quests.options.OptionHologramLaunchNo; import fr.skytasul.quests.options.OptionHologramText; @@ -29,7 +30,7 @@ import fr.skytasul.quests.utils.Lang; import fr.skytasul.quests.utils.Utils; -public abstract class BQNPC { +public abstract class BQNPC implements Located.LocatedEntity { private Map> quests = new TreeMap<>(); private Set pools = new TreeSet<>(); @@ -40,6 +41,7 @@ public abstract class BQNPC { private BukkitTask launcheableTask; /* Holograms */ + private boolean debug = false; private BukkitTask hologramsTask; private boolean hologramsRemoved = true; private Hologram hologramText = new Hologram(false, QuestsAPI.hasHologramsManager() && !QuestsConfiguration.isTextHologramDisabled(), Lang.HologramText.toString()); @@ -64,8 +66,10 @@ protected BQNPC() { public abstract boolean isSpawned(); + @Override public abstract Entity getEntity(); + @Override public abstract Location getLocation(); public abstract void setSkin(String skin); @@ -174,7 +178,7 @@ public void run() { if (isSpawned() && getEntity() instanceof LivingEntity) en = (LivingEntity) getEntity(); if (en == null) { - if (!hologramsRemoved) removeHolograms(); // if the NPC is not living and holograms have not been already removed before + if (!hologramsRemoved) removeHolograms(false); // if the NPC is not living and holograms have not been already removed before return; } hologramsRemoved = false; @@ -182,7 +186,7 @@ public void run() { if (hologramText.canAppear && hologramText.visible) hologramText.refresh(en); if (hologramLaunch.canAppear) hologramLaunch.refresh(en); if (hologramLaunchNo.canAppear) hologramLaunchNo.refresh(en); - if (hologramPool.canAppear && hologramPool.visible) hologramPool.refresh(en); + if (hologramPool.canAppear) hologramPool.refresh(en); } }.runTaskTimer(BeautyQuests.getInstance(), 20L, 1L); } @@ -237,7 +241,6 @@ public Set getPools() { public void addPool(QuestPool pool) { if (!pools.add(pool)) return; if (hologramPool.enabled && (pool.getHologram() != null)) hologramPool.setText(pool.getHologram()); - hologramPool.visible = true; addStartablePredicate(pool::canGive, pool); updatedObjects(); } @@ -246,10 +249,7 @@ public boolean removePool(QuestPool pool) { boolean b = pools.remove(pool); removeStartablePredicate(pool); updatedObjects(); - if (pools.isEmpty()) { - hologramPool.visible = false; - hologramPool.delete(); - } + if (pools.isEmpty()) hologramPool.delete(); return b; } @@ -280,13 +280,13 @@ public boolean canGiveSomething(Player p) { return startable.values().stream().anyMatch(predicate -> predicate.test(p, acc)); } - private void removeHolograms() { + private void removeHolograms(boolean cancelRefresh) { hologramText.delete(); hologramLaunch.delete(); hologramLaunchNo.delete(); hologramPool.delete(); hologramsRemoved = true; - if (hologramsTask != null) { + if (cancelRefresh && hologramsTask != null) { hologramsTask.cancel(); hologramsTask = null; } @@ -298,14 +298,14 @@ private boolean isEmpty() { private void updatedObjects() { if (isEmpty()) { - removeHolograms(); + removeHolograms(true); }else if (holograms && hologramsTask == null) { hologramsTask = startHologramsTask(); } } public void unload() { - removeHolograms(); + removeHolograms(true); if (launcheableTask != null) { launcheableTask.cancel(); launcheableTask = null; @@ -326,6 +326,30 @@ public void delete(String cause) { unload(); } + public void toggleDebug() { + if (debug) + debug = false; + else debug = true; + } + + @Override + public String toString() { + String npcInfo = "NPC #" + getId() + ", " + quests.size() + " quests, " + pools.size() + " pools"; + String hologramsInfo; + if (!holograms) { + hologramsInfo = "no holograms"; + }else if (hologramsRemoved) { + hologramsInfo = "holograms removed"; + }else { + hologramsInfo = "holograms:"; + hologramsInfo += "\n- text=" + hologramText.toString(); + hologramsInfo += "\n- launch=" + hologramLaunch.toString(); + hologramsInfo += "\n- launchNo=" + hologramLaunchNo.toString(); + hologramsInfo += "\n- pool=" + hologramPool.toString(); + } + return npcInfo + " " + hologramsInfo; + } + public class Hologram { final boolean enabled; boolean visible; @@ -349,9 +373,12 @@ public Hologram(boolean visible, boolean enabled, ItemStack item) { public void refresh(LivingEntity en) { Location lc = Utils.upLocationForEntity(en, getYAdd()); + if (debug) System.out.println("refreshing " + toString() + " (hologram null: " + (hologram == null) + ")"); if (hologram == null) { create(lc); - }else hologram.teleport(lc); + }else { + hologram.teleport(lc); + } } public double getYAdd() { @@ -390,10 +417,22 @@ public void create(Location lc) { } public void delete() { + if (debug) System.out.println("deleting " + toString()); if (hologram == null) return; hologram.delete(); hologram = null; } + + @Override + public String toString() { + if (!enabled) return "disabled"; + if (!canAppear) return "cannot appear"; + return (visible ? "visible" : "invisible") + " by default, " + + (item == null ? "" : item.getType().name() + ", ") + + (text == null ? "no text" : "text=" + text) + ", " + + (hologram == null ? " not spawned" : "spawned"); + } + } } \ No newline at end of file diff --git a/core/src/main/java/fr/skytasul/quests/api/objects/QuestObject.java b/core/src/main/java/fr/skytasul/quests/api/objects/QuestObject.java index 762a98a5..acaddd4c 100644 --- a/core/src/main/java/fr/skytasul/quests/api/objects/QuestObject.java +++ b/core/src/main/java/fr/skytasul/quests/api/objects/QuestObject.java @@ -1,32 +1,29 @@ package fr.skytasul.quests.api.objects; -import java.util.ArrayList; import java.util.HashMap; -import java.util.List; import java.util.Map; -import java.util.function.Function; +import org.bukkit.configuration.ConfigurationSection; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; -import fr.skytasul.quests.BeautyQuests; +import fr.skytasul.quests.api.serializable.SerializableObject; import fr.skytasul.quests.gui.ItemUtils; import fr.skytasul.quests.structure.Quest; import fr.skytasul.quests.utils.Lang; +import fr.skytasul.quests.utils.Utils; -public abstract class QuestObject implements Cloneable { - - private final QuestObjectCreator creator; +public abstract class QuestObject extends SerializableObject implements Cloneable { private Quest quest; protected QuestObject(QuestObjectsRegistry registry) { - this.creator = registry.getByClass(getClass()); - if (creator == null) throw new IllegalArgumentException(getClass().getName() + " has not been registered as a reward via the API."); + super(registry); } + @Override public QuestObjectCreator getCreator() { - return creator; + return (QuestObjectCreator) super.getCreator(); } public void attach(Quest quest) { @@ -41,10 +38,6 @@ public Quest getAttachedQuest() { return quest; } - public String getName() { - return getCreator().id; - } - public String debugName() { return getClass().getSimpleName() + (quest == null ? ", unknown quest" : (", quest " + quest.getID())); } @@ -52,9 +45,23 @@ public String debugName() { @Override public abstract QuestObject clone(); - protected abstract void save(Map datas); + @Deprecated + protected void save(Map datas) {} + + @Deprecated + protected void load(Map savedDatas) {} + + @Override + public void save(ConfigurationSection section) { + Map datas = new HashMap<>(); + save(datas); + Utils.setConfigurationSectionContent(section, datas); + } - protected abstract void load(Map savedDatas); + @Override + public void load(ConfigurationSection section) { + load(section.getValues(false)); + } public String getDescription(Player p) { // will maybe eventually be abstract (and therefore needs to be implemented) return null; @@ -65,61 +72,9 @@ public String[] getLore() { } public ItemStack getItemStack() { - return ItemUtils.lore(getCreator().item.clone(), getLore()); + return ItemUtils.lore(getCreator().getItem().clone(), getLore()); } public abstract void itemClick(QuestObjectClickEvent event); - public final Map serialize() { - Map map = new HashMap<>(); - - save(map); - map.put("id", creator.id); - - return map; - } - - public static List deserializeList(List> objectList, Function, T> deserializeFunction) { - List objects = new ArrayList<>(objectList.size()); - for (Map objectMap : objectList) { - try { - T object = deserializeFunction.apply((Map) objectMap); - if (object == null) { - BeautyQuests.loadingFailure = true; - BeautyQuests.getInstance().getLogger().severe("The quest object for class " + String.valueOf(objectMap.get("class")) + " has not been deserialized."); - }else objects.add(object); - }catch (Exception e) { - BeautyQuests.logger.severe("An exception occured while deserializing a quest object (class " + objectMap.get("class") + ").", e); - BeautyQuests.loadingFailure = true; - } - } - return objects; - } - - public static > T deserialize(Map map, QuestObjectsRegistry registry) { - QuestObjectCreator creator = null; - - String id = (String) map.get("id"); - if (id != null) creator = registry.getByID(id); - - if (creator == null && map.containsKey("class")) { - String className = (String) map.get("class"); - try { - creator = registry.getByClass(Class.forName(className)); - }catch (ClassNotFoundException e) {} - - if (creator == null) { - BeautyQuests.logger.severe("Cannot find object class " + className); - return null; - } - } - if (creator == null) { - BeautyQuests.logger.severe("Cannot find object creator with id: " + id); - return null; - } - T reward = creator.newObjectSupplier.get(); - reward.load(map); - return reward; - } - } \ No newline at end of file diff --git a/core/src/main/java/fr/skytasul/quests/api/objects/QuestObjectClickEvent.java b/core/src/main/java/fr/skytasul/quests/api/objects/QuestObjectClickEvent.java index 4f88fa44..c33235cc 100644 --- a/core/src/main/java/fr/skytasul/quests/api/objects/QuestObjectClickEvent.java +++ b/core/src/main/java/fr/skytasul/quests/api/objects/QuestObjectClickEvent.java @@ -1,7 +1,5 @@ package fr.skytasul.quests.api.objects; -import java.util.List; - import org.bukkit.entity.Player; import org.bukkit.event.inventory.ClickType; import org.bukkit.inventory.ItemStack; @@ -12,24 +10,26 @@ public class QuestObjectClickEvent { private final Player player; - private final QuestObjectGUI gui; + private final QuestObjectGUI gui; private final ItemStack item; private final ClickType click; private final boolean creation; + private final QuestObject clickedObject; - public QuestObjectClickEvent(Player player, QuestObjectGUI gui, ItemStack item, ClickType click, boolean creation) { + public QuestObjectClickEvent(Player player, QuestObjectGUI gui, ItemStack item, ClickType click, boolean creation, QuestObject clickedObject) { this.player = player; this.gui = gui; this.item = item; this.click = click; this.creation = creation; + this.clickedObject = clickedObject; } public Player getPlayer() { return player; } - public QuestObjectGUI getGUI() { + public QuestObjectGUI getGUI() { return gui; } @@ -46,15 +46,27 @@ public boolean isInCreation() { } public void reopenGUI() { + updateItemLore(); + gui.reopen(); + } + + public void cancel() { + if (creation) gui.remove(clickedObject); gui.reopen(); } + public void remove() { + gui.remove(clickedObject); + gui.reopen(); + } + + @Deprecated public void updateItemLore(String... lore) { ItemUtils.lore(item, lore); } - public void updateItemLore(List lore) { - ItemUtils.lore(item, lore); + public void updateItemLore() { + ItemUtils.lore(item, clickedObject.getLore()); } } diff --git a/core/src/main/java/fr/skytasul/quests/api/objects/QuestObjectCreator.java b/core/src/main/java/fr/skytasul/quests/api/objects/QuestObjectCreator.java index fe632c69..ded73102 100644 --- a/core/src/main/java/fr/skytasul/quests/api/objects/QuestObjectCreator.java +++ b/core/src/main/java/fr/skytasul/quests/api/objects/QuestObjectCreator.java @@ -4,15 +4,13 @@ import org.bukkit.inventory.ItemStack; +import fr.skytasul.quests.api.serializable.SerializableCreator; import fr.skytasul.quests.gui.creation.QuestObjectGUI; -public class QuestObjectCreator { +public class QuestObjectCreator extends SerializableCreator { - public final String id; - public final Class clazz; - public final ItemStack item; - public final Supplier newObjectSupplier; - public final boolean multiple; + private final ItemStack item; + private final boolean multiple; private QuestObjectLocation[] allowedLocations; /** @@ -31,17 +29,24 @@ public QuestObjectCreator(String id, Class clazz, ItemStack item, S * @param item ItemStack shown in {@link QuestObjectGUI} * @param newObjectSupplier lambda returning an instance of this Object ({@link T}::new) * @param multiple can the object be present multiple times - * @param allowedLocations if present, specifies where the object can be used. If no location specified, the + * @param allowedLocations if present, specifies where the object can be used. + * If no location is specified, then all locations are accepted. */ public QuestObjectCreator(String id, Class clazz, ItemStack item, Supplier newObjectSupplier, boolean multiple, QuestObjectLocation... allowedLocations) { - this.id = id; - this.clazz = clazz; + super(id, clazz, newObjectSupplier); this.item = item; - this.newObjectSupplier = newObjectSupplier; this.multiple = multiple; this.allowedLocations = allowedLocations; } + public ItemStack getItem() { + return item; + } + + public boolean canBeMultiple() { + return multiple; + } + public boolean isAllowed(QuestObjectLocation location) { if (allowedLocations.length == 0) return true; for (QuestObjectLocation allowed : allowedLocations) { diff --git a/core/src/main/java/fr/skytasul/quests/api/objects/QuestObjectsRegistry.java b/core/src/main/java/fr/skytasul/quests/api/objects/QuestObjectsRegistry.java index 0889cda2..7149ae00 100644 --- a/core/src/main/java/fr/skytasul/quests/api/objects/QuestObjectsRegistry.java +++ b/core/src/main/java/fr/skytasul/quests/api/objects/QuestObjectsRegistry.java @@ -1,61 +1,41 @@ package fr.skytasul.quests.api.objects; -import java.util.ArrayList; import java.util.Collection; -import java.util.Iterator; import java.util.List; import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import fr.skytasul.quests.api.serializable.SerializableRegistry; import fr.skytasul.quests.gui.creation.QuestObjectGUI; -import fr.skytasul.quests.utils.DebugUtils; -public class QuestObjectsRegistry> implements Iterable { +public class QuestObjectsRegistry> extends SerializableRegistry { - private final List creators = new ArrayList<>(); private final String inventoryName; - public QuestObjectsRegistry(String inventoryName) { + public QuestObjectsRegistry(String id, String inventoryName) { + super(id); this.inventoryName = inventoryName; } - public void register(C creator) { - if (creators.stream().anyMatch(x -> x.id.equals(creator.id))) - throw new IllegalStateException("A creator with the same id " + creator.id + " has been registered."); - creators.add(creator); - DebugUtils.logMessage("Quest object registered (id: " + creator.id + ", class: " + creator.clazz.getName() + ")"); + public String getInventoryName() { + return inventoryName; } - public C getByClass(Class clazz) { - return creators - .stream() - .filter(creator -> creator.clazz.equals(clazz)) - .findAny() - .orElse(null); - } - - public C getByID(String id) { - return creators - .stream() - .filter(creator -> creator.id.equals(id)) - .findAny() - .orElse(null); - } - - public List getCreators() { - return creators; + public QuestObjectGUI createGUI(QuestObjectLocation location, Consumer> end, List objects) { + return createGUI(inventoryName, location, end, objects, null); } - @Override - public Iterator iterator() { - return creators.iterator(); + public QuestObjectGUI createGUI(QuestObjectLocation location, Consumer> end, List objects, Predicate filter) { + return createGUI(inventoryName, location, end, objects, filter); } - public QuestObjectGUI createGUI(QuestObjectLocation location, Consumer> end, List objects) { - return createGUI(inventoryName, location, end, objects); + public QuestObjectGUI createGUI(String name, QuestObjectLocation location, Consumer> end, List objects) { + return createGUI(name, location, end, objects, null); } - public QuestObjectGUI createGUI(String name, QuestObjectLocation location, Consumer> end, List objects) { - return new QuestObjectGUI<>(name, location, (Collection>) creators, end, objects); + public QuestObjectGUI createGUI(String name, QuestObjectLocation location, Consumer> end, List objects, Predicate filter) { + return new QuestObjectGUI<>(name, location, (Collection>) (filter == null ? creators : creators.stream().filter(filter).collect(Collectors.toList())), end, objects); } } diff --git a/core/src/main/java/fr/skytasul/quests/api/options/QuestOption.java b/core/src/main/java/fr/skytasul/quests/api/options/QuestOption.java index d95632a4..da651bb8 100644 --- a/core/src/main/java/fr/skytasul/quests/api/options/QuestOption.java +++ b/core/src/main/java/fr/skytasul/quests/api/options/QuestOption.java @@ -2,11 +2,17 @@ import java.util.Objects; +import org.apache.commons.lang.Validate; +import org.bukkit.Bukkit; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.entity.Player; +import org.bukkit.event.HandlerList; +import org.bukkit.event.Listener; import org.bukkit.event.inventory.ClickType; import org.bukkit.inventory.ItemStack; +import fr.skytasul.quests.BeautyQuests; +import fr.skytasul.quests.api.options.description.QuestDescriptionProvider; import fr.skytasul.quests.gui.creation.FinishGUI; import fr.skytasul.quests.structure.Quest; import fr.skytasul.quests.utils.Lang; @@ -68,11 +74,30 @@ public Quest getAttachedQuest() { } public void attach(Quest quest) { + Validate.notNull(quest, "Attached quest cannot be null"); + if (this.attachedQuest != null) throw new IllegalStateException("This option is already attached to " + attachedQuest.getID()); this.attachedQuest = quest; + + if (this instanceof Listener) { + Bukkit.getPluginManager().registerEvents((Listener) this, BeautyQuests.getInstance()); + } + + if (this instanceof QuestDescriptionProvider) { + quest.getDescriptions().add((QuestDescriptionProvider) this); + } } public void detach() { + Quest previous = this.attachedQuest; this.attachedQuest = null; + + if (this instanceof Listener) { + HandlerList.unregisterAll((Listener) this); + } + + if (previous != null && this instanceof QuestDescriptionProvider) { + previous.getDescriptions().remove((QuestDescriptionProvider) this); + } } public abstract Object save(); diff --git a/core/src/main/java/fr/skytasul/quests/api/options/QuestOptionObject.java b/core/src/main/java/fr/skytasul/quests/api/options/QuestOptionObject.java index eca9c232..b31de3f1 100644 --- a/core/src/main/java/fr/skytasul/quests/api/options/QuestOptionObject.java +++ b/core/src/main/java/fr/skytasul/quests/api/options/QuestOptionObject.java @@ -3,9 +3,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.function.Function; -import org.apache.commons.lang.Validate; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.entity.Player; import org.bukkit.event.inventory.ClickType; @@ -15,17 +13,16 @@ import fr.skytasul.quests.api.objects.QuestObjectCreator; import fr.skytasul.quests.api.objects.QuestObjectLocation; import fr.skytasul.quests.api.objects.QuestObjectsRegistry; +import fr.skytasul.quests.api.serializable.SerializableObject; import fr.skytasul.quests.gui.ItemUtils; import fr.skytasul.quests.gui.creation.FinishGUI; import fr.skytasul.quests.structure.Quest; -import fr.skytasul.quests.utils.Utils; import fr.skytasul.quests.utils.XMaterial; public abstract class QuestOptionObject> extends QuestOption> { @Override public void attach(Quest quest) { - Validate.notNull(quest, "Attached quest cannot be null"); super.attach(quest); attachObjects(); } @@ -57,7 +54,7 @@ protected void attachObject(T object) { @Override public Object save() { - return Utils.serializeList(getValue(), getSerializeFunction()); + return SerializableObject.serializeList(getValue()); } @Override @@ -70,8 +67,6 @@ public List cloneValue(List value) { return new ArrayList<>(value); } - protected abstract Function> getSerializeFunction(); - protected abstract T deserialize(Map map); protected abstract String getSizeString(int size); diff --git a/core/src/main/java/fr/skytasul/quests/api/options/QuestOptionRewards.java b/core/src/main/java/fr/skytasul/quests/api/options/QuestOptionRewards.java index 6d16f80e..33c6db02 100644 --- a/core/src/main/java/fr/skytasul/quests/api/options/QuestOptionRewards.java +++ b/core/src/main/java/fr/skytasul/quests/api/options/QuestOptionRewards.java @@ -1,7 +1,6 @@ package fr.skytasul.quests.api.options; import java.util.Map; -import java.util.function.Function; import fr.skytasul.quests.api.QuestsAPI; import fr.skytasul.quests.api.objects.QuestObjectsRegistry; @@ -18,11 +17,6 @@ protected void attachObject(AbstractReward object) { } protected abstract void attachedAsyncReward(AbstractReward reward); - - @Override - protected Function> getSerializeFunction() { - return AbstractReward::serialize; - } @Override protected AbstractReward deserialize(Map map) { diff --git a/core/src/main/java/fr/skytasul/quests/api/options/description/QuestDescription.java b/core/src/main/java/fr/skytasul/quests/api/options/description/QuestDescription.java new file mode 100644 index 00000000..2154f23a --- /dev/null +++ b/core/src/main/java/fr/skytasul/quests/api/options/description/QuestDescription.java @@ -0,0 +1,43 @@ +package fr.skytasul.quests.api.options.description; + +import org.bukkit.configuration.ConfigurationSection; + +public class QuestDescription { + + private boolean requirements; + private String requirementsValid; + private String requirementsInvalid; + + private boolean rewards; + private String rewardsFormat; + + public QuestDescription(ConfigurationSection config) { + requirements = config.getBoolean("requirements.display"); + requirementsValid = config.getString("requirements.valid"); + requirementsInvalid = config.getString("requirements.invalid"); + + rewards = config.getBoolean("rewards.display"); + rewardsFormat = config.getString("rewards.format"); + } + + public boolean showRewards() { + return rewards; + } + + public String getRewardsFormat() { + return rewardsFormat; + } + + public boolean showRequirements() { + return requirements; + } + + public String getRequirementsValid() { + return requirementsValid; + } + + public String getRequirementsInvalid() { + return requirementsInvalid; + } + +} diff --git a/core/src/main/java/fr/skytasul/quests/api/options/description/QuestDescriptionContext.java b/core/src/main/java/fr/skytasul/quests/api/options/description/QuestDescriptionContext.java new file mode 100644 index 00000000..19fb2a95 --- /dev/null +++ b/core/src/main/java/fr/skytasul/quests/api/options/description/QuestDescriptionContext.java @@ -0,0 +1,73 @@ +package fr.skytasul.quests.api.options.description; + +import java.util.ArrayList; +import java.util.List; +import fr.skytasul.quests.gui.quests.PlayerListGUI; +import fr.skytasul.quests.gui.quests.PlayerListGUI.Category; +import fr.skytasul.quests.players.PlayerAccount; +import fr.skytasul.quests.players.PlayerQuestDatas; +import fr.skytasul.quests.structure.Quest; +import fr.skytasul.quests.structure.QuestBranch.Source; + +public class QuestDescriptionContext { + + private final QuestDescription descriptionOptions; + private final Quest quest; + private final PlayerAccount acc; + private final Category category; + private final Source source; + + private PlayerQuestDatas cachedDatas; + + public QuestDescriptionContext(QuestDescription descriptionOptions, Quest quest, PlayerAccount acc, + PlayerListGUI.Category category, Source source) { + this.descriptionOptions = descriptionOptions; + this.quest = quest; + this.acc = acc; + this.category = category; + this.source = source; + } + + public QuestDescription getDescriptionOptions() { + return descriptionOptions; + } + + public Quest getQuest() { + return quest; + } + + public PlayerAccount getPlayerAccount() { + return acc; + } + + public Category getCategory() { + return category; + } + + public Source getSource() { + return source; + } + + public PlayerQuestDatas getQuestDatas() { + if (cachedDatas == null) cachedDatas = acc.getQuestDatasIfPresent(quest); + return cachedDatas; + } + + public List formatDescription() { + List list = new ArrayList<>(); + + quest.getDescriptions() + .stream() + .sorted(QuestDescriptionProvider.COMPARATOR) + .forEach(provider -> { + List description = provider.provideDescription(this); + if (description == null || description.isEmpty()) return; + + if (!list.isEmpty() && provider.prefixDescriptionWithNewLine()) list.add(""); + list.addAll(description); + }); + + return list; + } + +} diff --git a/core/src/main/java/fr/skytasul/quests/api/options/description/QuestDescriptionProvider.java b/core/src/main/java/fr/skytasul/quests/api/options/description/QuestDescriptionProvider.java new file mode 100644 index 00000000..0b968a7a --- /dev/null +++ b/core/src/main/java/fr/skytasul/quests/api/options/description/QuestDescriptionProvider.java @@ -0,0 +1,20 @@ +package fr.skytasul.quests.api.options.description; + +import java.util.Comparator; +import java.util.List; + +public interface QuestDescriptionProvider { + + public static final Comparator COMPARATOR = Comparator.comparingDouble(QuestDescriptionProvider::getDescriptionPriority); + + List provideDescription(QuestDescriptionContext context); + + String getDescriptionId(); + + double getDescriptionPriority(); + + default boolean prefixDescriptionWithNewLine() { + return true; + } + +} diff --git a/core/src/main/java/fr/skytasul/quests/api/requirements/TargetNumberRequirement.java b/core/src/main/java/fr/skytasul/quests/api/requirements/TargetNumberRequirement.java index c5277267..3847b78a 100644 --- a/core/src/main/java/fr/skytasul/quests/api/requirements/TargetNumberRequirement.java +++ b/core/src/main/java/fr/skytasul/quests/api/requirements/TargetNumberRequirement.java @@ -1,8 +1,8 @@ package fr.skytasul.quests.api.requirements; import java.text.NumberFormat; -import java.util.Map; +import org.bukkit.configuration.ConfigurationSection; import org.bukkit.entity.Player; import fr.skytasul.quests.api.objects.QuestObjectClickEvent; @@ -10,7 +10,6 @@ import fr.skytasul.quests.editors.checkers.NumberParser; import fr.skytasul.quests.utils.ComparisonMethod; import fr.skytasul.quests.utils.Lang; -import fr.skytasul.quests.utils.Utils; public abstract class TargetNumberRequirement extends AbstractRequirement { @@ -58,15 +57,15 @@ public String[] getLore() { public abstract void sendHelpString(Player p); @Override - protected void save(Map datas) { - datas.put("comparison", comparison.name()); - datas.put("target", target); + public void save(ConfigurationSection section) { + section.set("comparison", comparison.name()); + section.set("target", target); } @Override - protected void load(Map savedDatas) { - if (savedDatas.containsKey("comparison")) comparison = ComparisonMethod.valueOf((String) savedDatas.get("comparison")); - target = Utils.parseDouble(savedDatas.get("target")); + public void load(ConfigurationSection section) { + if (section.contains("comparison")) comparison = ComparisonMethod.valueOf(section.getString("comparison")); + target = section.getDouble("target"); } @Override @@ -80,13 +79,9 @@ public void itemClick(QuestObjectClickEvent event) { Lang.COMPARISON_TYPE.send(event.getPlayer(), ComparisonMethod.getComparisonParser().getNames(), ComparisonMethod.GREATER_OR_EQUAL.name().toLowerCase()); new TextEditor<>(event.getPlayer(), null, comp -> { this.comparison = comp == null ? ComparisonMethod.GREATER_OR_EQUAL : comp; - event.updateItemLore(getLore()); event.reopenGUI(); }, ComparisonMethod.getComparisonParser()).passNullIntoEndConsumer().enter(); - }, () -> { - event.getGUI().remove(this); - event.reopenGUI(); - }, new NumberParser<>(numberClass(), true)).enter(); + }, event::remove, new NumberParser<>(numberClass(), true)).enter(); } } \ No newline at end of file diff --git a/core/src/main/java/fr/skytasul/quests/api/serializable/SerializableCreator.java b/core/src/main/java/fr/skytasul/quests/api/serializable/SerializableCreator.java new file mode 100644 index 00000000..611a3f5b --- /dev/null +++ b/core/src/main/java/fr/skytasul/quests/api/serializable/SerializableCreator.java @@ -0,0 +1,35 @@ +package fr.skytasul.quests.api.serializable; + +import java.util.function.Supplier; + +public class SerializableCreator { + + private final String id; + private final Class clazz; + private final Supplier newObjectSupplier; + + /** + * Creates a new creator for the serializable object of type <T> + * @param id unique string id for the serializable object type + * @param clazz class of the serializable object type + * @param newObjectSupplier function used to instanciate a serializable object + */ + public SerializableCreator(String id, Class clazz, Supplier newObjectSupplier) { + this.id = id; + this.clazz = clazz; + this.newObjectSupplier = newObjectSupplier; + } + + public String getID() { + return id; + } + + public Class getSerializableClass() { + return clazz; + } + + public T newObject() { + return newObjectSupplier.get(); + } + +} \ No newline at end of file diff --git a/core/src/main/java/fr/skytasul/quests/api/serializable/SerializableObject.java b/core/src/main/java/fr/skytasul/quests/api/serializable/SerializableObject.java new file mode 100644 index 00000000..6ebcc93c --- /dev/null +++ b/core/src/main/java/fr/skytasul/quests/api/serializable/SerializableObject.java @@ -0,0 +1,104 @@ +package fr.skytasul.quests.api.serializable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.MemoryConfiguration; + +import fr.skytasul.quests.BeautyQuests; +import fr.skytasul.quests.utils.Utils; + +public abstract class SerializableObject { + + protected final SerializableCreator creator; + + protected SerializableObject(SerializableRegistry registry) { + this.creator = registry.getByClass(getClass()); + if (creator == null) throw new IllegalArgumentException(getClass().getName() + " has not been registered as an object."); + } + + protected SerializableObject(SerializableCreator creator) { + this.creator = creator; + if (creator == null) throw new IllegalArgumentException("Creator cannot be null."); + } + + public SerializableCreator getCreator() { + return creator; + } + + public String getName() { + return getCreator().getID(); + } + + @Override + public abstract SerializableObject clone(); + + public abstract void save(ConfigurationSection section); + + public abstract void load(ConfigurationSection section); + + public final void serialize(ConfigurationSection section) { + section.set("id", creator.getID()); + save(section); + } + + public static > T deserialize(Map map, SerializableRegistry registry) { + return deserialize(Utils.createConfigurationSection(map), registry); + } + + public static > T deserialize(ConfigurationSection section, SerializableRegistry registry) { + SerializableCreator creator = null; + + String id = section.getString("id"); + if (id != null) creator = registry.getByID(id); + + if (creator == null && section.contains("class")) { + String className = section.getString("class"); + try { + creator = registry.getByClass(Class.forName(className)); + }catch (ClassNotFoundException e) {} + + if (creator == null) { + BeautyQuests.logger.severe("Cannot find object class " + className); + return null; + } + } + if (creator == null) { + BeautyQuests.logger.severe("Cannot find object creator with id: " + id); + return null; + } + T reward = creator.newObject(); + reward.load(section); + return reward; + } + + public static List deserializeList(List> objectList, Function, T> deserializeFunction) { + List objects = new ArrayList<>(objectList.size()); + for (Map objectMap : objectList) { + try { + T object = deserializeFunction.apply((Map) objectMap); + if (object == null) { + BeautyQuests.loadingFailure = true; + BeautyQuests.getInstance().getLogger().severe("The quest object for class " + String.valueOf(objectMap.get("class")) + " has not been deserialized."); + }else objects.add(object); + }catch (Exception e) { + BeautyQuests.logger.severe("An exception occured while deserializing a quest object (class " + objectMap.get("class") + ").", e); + BeautyQuests.loadingFailure = true; + } + } + return objects; + } + + public static List> serializeList(List objects) { + return objects.stream().map(object -> { + MemoryConfiguration section = new MemoryConfiguration(); + object.serialize(section); + return section.getValues(false); + }).collect(Collectors.toList()); + } + +} \ No newline at end of file diff --git a/core/src/main/java/fr/skytasul/quests/api/serializable/SerializableRegistry.java b/core/src/main/java/fr/skytasul/quests/api/serializable/SerializableRegistry.java new file mode 100644 index 00000000..058eb347 --- /dev/null +++ b/core/src/main/java/fr/skytasul/quests/api/serializable/SerializableRegistry.java @@ -0,0 +1,65 @@ +package fr.skytasul.quests.api.serializable; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import fr.skytasul.quests.utils.DebugUtils; + +/** + * This class is a registry for types of objects that can be serialized + * into Spigot configuration system. + * + * @param type of serializable object + * @param type of the creator associated with the serializable objects + */ +public class SerializableRegistry> implements Iterable { + + protected final String id; + protected final List creators = new ArrayList<>(); + + public SerializableRegistry(String id) { + this.id = id; + } + + public String getID() { + return id; + } + + /** + * Registers a new type of serializable object. + * @param creator object that will be used to instanciate objects of type <T> + */ + public void register(C creator) { + if (creators.stream().anyMatch(x -> x.getID().equals(creator.getID()))) + throw new IllegalStateException("A creator with the same id " + creator.getID() + " has been registered."); + creators.add(creator); + DebugUtils.logMessage("Quest object registered in registry " + id + " (id: " + creator.getID() + ", class: " + creator.getSerializableClass().getName() + ")"); + } + + public C getByClass(Class clazz) { + return creators + .stream() + .filter(creator -> creator.getSerializableClass().equals(clazz)) + .findAny() + .orElse(null); + } + + public C getByID(String id) { + return creators + .stream() + .filter(creator -> creator.getID().equals(id)) + .findAny() + .orElse(null); + } + + public List getCreators() { + return creators; + } + + @Override + public Iterator iterator() { + return creators.iterator(); + } + +} \ No newline at end of file diff --git a/core/src/main/java/fr/skytasul/quests/api/stages/AbstractCountableStage.java b/core/src/main/java/fr/skytasul/quests/api/stages/AbstractCountableStage.java index 3d19d118..6fcbceb2 100644 --- a/core/src/main/java/fr/skytasul/quests/api/stages/AbstractCountableStage.java +++ b/core/src/main/java/fr/skytasul/quests/api/stages/AbstractCountableStage.java @@ -1,295 +1,16 @@ -package fr.skytasul.quests.api.stages; - -import java.util.AbstractMap; -import java.util.HashMap; -import java.util.Map; -import java.util.Map.Entry; -import java.util.function.Supplier; - -import org.bukkit.Bukkit; -import org.bukkit.boss.BarColor; -import org.bukkit.boss.BarStyle; -import org.bukkit.configuration.ConfigurationSection; -import org.bukkit.configuration.MemoryConfiguration; -import org.bukkit.entity.Player; -import org.bukkit.scheduler.BukkitTask; - -import fr.skytasul.quests.BeautyQuests; -import fr.skytasul.quests.QuestsConfiguration; -import fr.skytasul.quests.api.QuestsAPI; -import fr.skytasul.quests.api.bossbar.BQBossBarManager.BQBossBar; -import fr.skytasul.quests.players.PlayerAccount; -import fr.skytasul.quests.structure.QuestBranch; -import fr.skytasul.quests.structure.QuestBranch.Source; -import fr.skytasul.quests.utils.Lang; -import fr.skytasul.quests.utils.Utils; - -public abstract class AbstractCountableStage extends AbstractStage { - - protected Map> objects; - - protected Map bars = new HashMap<>(); - private boolean barsEnabled = false; - private int cachedSize = 0; - - public AbstractCountableStage(QuestBranch branch, Map> objects) { - super(branch); - this.objects = objects; - calculateSize(); - } - - public Map> getObjects() { - return objects; - } - - public Map> cloneObjects() { - Map> map = new HashMap<>(); - for (Entry> entry : objects.entrySet()) { - map.put(entry.getKey(), new AbstractMap.SimpleEntry<>(cloneObject(entry.getValue().getKey()), entry.getValue().getValue())); - } - return map; - } - - public Map getPlayerRemainings(PlayerAccount acc) { - return getData(acc, "remaining"); - } - - protected void calculateSize() { - cachedSize = 0; - for (Entry objectsEntry : objects.values()) { - cachedSize += objectsEntry.getValue(); - } - barsEnabled = QuestsConfiguration.showMobsProgressBar() && cachedSize > 0; - } - - @Override - protected String descriptionLine(PlayerAccount acc, Source source){ - return Utils.descriptionLines(source, buildRemainingArray(acc, source)); - } - - @Override - protected Supplier[] descriptionFormat(PlayerAccount acc, Source source) { - return new Supplier[] { () -> Utils.descriptionLines(source, buildRemainingArray(acc, source)) }; - } - - private String[] buildRemainingArray(PlayerAccount acc, Source source) { - Map playerAmounts = getPlayerRemainings(acc); - if (playerAmounts == null) { - BeautyQuests.logger.severe("The plugin has been unable to retrieve stage datas for account " + acc.debugName() + " on " + super.debugName()); - return new String[] { "§4§lerror" }; - } - String[] elements = new String[playerAmounts.size()]; - int i = 0; - for (Entry obj : playerAmounts.entrySet()) { - Entry object = objects.get(obj.getKey()); - elements[i] = object == null ? "no object " + obj.getKey() : QuestsConfiguration.getItemNameColor() + Utils.getStringFromNameAndAmount(getName(object.getKey()), QuestsConfiguration.getItemAmountColor(), obj.getValue(), object.getValue(), QuestsConfiguration.showDescriptionItemsXOne(source)); - i++; - } - return elements; - } - - @Override - protected void initPlayerDatas(PlayerAccount acc, Map datas) { - super.initPlayerDatas(acc, datas); - Map amounts = new HashMap<>(); - for (Entry> entry : objects.entrySet()) { - amounts.put(entry.getKey(), entry.getValue().getValue()); - } - datas.put("remaining", amounts); - } - - /** - * When called, this will test the player datas for the passed object. - * If found, the remaining amount will be lowered. - * If no remaining items are found, the stage will complete. - * @param acc player account - * @param p player - * @param object object of the event - * @param amount amount completed - * @return false if there is no need to call this method again in the same game tick. - */ - public boolean event(PlayerAccount acc, Player p, Object object, int amount) { - if (amount < 0) throw new IllegalArgumentException("Event amount must be positive (" + amount + ")"); - if (!canUpdate(p)) return true; - for (Entry> entry : objects.entrySet()) { - int id = entry.getKey(); - if (objectApplies(entry.getValue().getKey(), object)) { - Map playerAmounts = getPlayerRemainings(acc); - if (playerAmounts == null) { - BeautyQuests.logger.warning(p.getName() + " does not have object datas for stage " + debugName() + ". This is a bug!"); - return true; - } - if (playerAmounts.containsKey(id)) { - int playerAmount = playerAmounts.get(id); - if (playerAmount <= amount) { - playerAmounts.remove(id); - }else playerAmounts.put(id, playerAmount -= amount); - } - - if (playerAmounts.isEmpty()) { - finishStage(p); - return true; - }else { - if (barsEnabled) { - BossBar bar = bars.get(p); - if (bar == null) { - BeautyQuests.logger.warning(p.getName() + " does not have boss bar for stage " + debugName() + ". This is a bug!"); - }else bar.update(playerAmounts.values().stream().mapToInt(Integer::intValue).sum()); - } - updateObjective(acc, p, "remaining", playerAmounts); - return false; - } - } - } - return true; - } - - @Override - public void start(PlayerAccount acc) { - super.start(acc); - if (acc.isCurrent()) createBar(acc.getPlayer(), cachedSize); - } - - @Override - public void end(PlayerAccount acc) { - super.end(acc); - if (acc.isCurrent()) removeBar(acc.getPlayer()); - } - - @Override - public void unload() { - super.unload(); - bars.values().forEach(BossBar::remove); - } - - @Override - public void joins(PlayerAccount acc, Player p) { - super.joins(acc, p); - Map remainings = getPlayerRemainings(acc); - if (remainings == null) { - BeautyQuests.logger.severe(p.getName() + " does not have remaining datas for stage " + debugName() + ". This is a bug!"); - return; - } - createBar(p, remainings.values().stream().mapToInt(Integer::intValue).sum()); - } - - @Override - public void leaves(PlayerAccount acc, Player p) { - super.leaves(acc, p); - removeBar(p); - } - - protected void createBar(Player p, int amount) { - if (barsEnabled) bars.put(p, new BossBar(p, amount)); - } - - protected void removeBar(Player p) { - if (bars.containsKey(p)) bars.remove(p).remove(); - } - - protected boolean objectApplies(T object, Object other) { - return object.equals(other); - } - - protected T cloneObject(T object) { - return object; - } - - protected abstract String getName(T object); - - protected abstract Object serialize(T object); - - protected abstract T deserialize(Object object); - - /** - * @deprecated for removal, {@link #serialize(ConfigurationSection)} should be used instead. - */ - @Override - @Deprecated - protected void serialize(Map map) {} - - @Override - protected void serialize(ConfigurationSection section) { - ConfigurationSection objectsSection = section.createSection("objects"); - for (Entry> obj : objects.entrySet()) { - ConfigurationSection objectSection = objectsSection.createSection(Integer.toString(obj.getKey())); - objectSection.set("amount", obj.getValue().getValue()); - objectSection.set("object", serialize(obj.getValue().getKey())); - } - Map serialized = new HashMap<>(); - serialize(serialized); - serialized.forEach(section::set); - } - - /** - * @deprecated for removal, {@link #deserialize(ConfigurationSection)} should be used instead. - */ - @Deprecated - protected void deserialize(Map serializedDatas) { - MemoryConfiguration configuration = new MemoryConfiguration(); - serializedDatas.forEach(configuration::set); - deserialize(configuration); - } - - protected void deserialize(ConfigurationSection section) { - ConfigurationSection objectsSection = section.getConfigurationSection("objects"); - if (objectsSection != null) { - for (String key : objectsSection.getKeys(false)) { - ConfigurationSection object = objectsSection.getConfigurationSection(key); - Object serialized = object.get("object"); - if (serialized instanceof ConfigurationSection) serialized = ((ConfigurationSection) serialized).getValues(false); - objects.put(Integer.parseInt(key), new AbstractMap.SimpleEntry<>(deserialize(serialized), object.getInt("amount"))); - } - } - - if (objects.isEmpty()) BeautyQuests.logger.warning("Stage with no content: " + debugName()); - calculateSize(); - } - - class BossBar { - private Player p; - private BQBossBar bar; - private BukkitTask timer; - - public BossBar(Player p, int amount) { - this.p = p; - - BarStyle style = null; - if (cachedSize % 20 == 0) { - style = BarStyle.SEGMENTED_20; - }else if (cachedSize % 10 == 0) { - style = BarStyle.SEGMENTED_10; - }else if (cachedSize % 12 == 0) { - style = BarStyle.SEGMENTED_12; - }else if (cachedSize % 6 == 0) { - style = BarStyle.SEGMENTED_6; - }else style = BarStyle.SOLID; - bar = QuestsAPI.getBossBarManager().buildBossBar(Lang.MobsProgression.format(branch.getQuest().getName(), 100, 100), BarColor.YELLOW, style); - update(amount); - } - - public void remove() { - bar.removeAll(); - if (timer != null) timer.cancel(); - } - - public void update(int amount) { - if (amount >= 0 && amount <= cachedSize) { - bar.setProgress((double) (cachedSize - amount) / (double) cachedSize); - }else BeautyQuests.logger.warning("Amount of objects superior to max objects in " + debugName() + " for player " + p.getName() + ": " + amount + " > " + cachedSize); - bar.setTitle(Lang.MobsProgression.format(branch.getQuest().getName(), cachedSize - amount, cachedSize)); - bar.addPlayer(p); - timer(); - } - - private void timer() { - if (QuestsConfiguration.getProgressBarTimeout() <= 0) return; - if (timer != null) timer.cancel(); - timer = Bukkit.getScheduler().runTaskLater(BeautyQuests.getInstance(), () -> { - bar.removePlayer(p); - timer = null; - }, QuestsConfiguration.getProgressBarTimeout() * 20L); - } - } - -} +package fr.skytasul.quests.api.stages; + +import java.util.Map; +import java.util.Map.Entry; + +import fr.skytasul.quests.structure.QuestBranch; + +@Deprecated +public abstract class AbstractCountableStage extends fr.skytasul.quests.api.stages.types.AbstractCountableStage { + + @Deprecated + protected AbstractCountableStage(QuestBranch branch, Map> objects) { + super(branch, objects); + } + +} diff --git a/core/src/main/java/fr/skytasul/quests/api/stages/AbstractStage.java b/core/src/main/java/fr/skytasul/quests/api/stages/AbstractStage.java index 1f43f75d..8757bcd7 100644 --- a/core/src/main/java/fr/skytasul/quests/api/stages/AbstractStage.java +++ b/core/src/main/java/fr/skytasul/quests/api/stages/AbstractStage.java @@ -6,6 +6,8 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.function.Consumer; +import java.util.stream.Collectors; import org.apache.commons.lang.Validate; import org.bukkit.Bukkit; @@ -18,18 +20,21 @@ import fr.skytasul.quests.BeautyQuests; import fr.skytasul.quests.QuestsConfiguration; import fr.skytasul.quests.api.QuestsAPI; +import fr.skytasul.quests.api.events.accounts.PlayerAccountJoinEvent; +import fr.skytasul.quests.api.events.accounts.PlayerAccountLeaveEvent; import fr.skytasul.quests.api.objects.QuestObject; import fr.skytasul.quests.api.requirements.AbstractRequirement; import fr.skytasul.quests.api.rewards.AbstractReward; +import fr.skytasul.quests.api.serializable.SerializableCreator; +import fr.skytasul.quests.api.serializable.SerializableObject; +import fr.skytasul.quests.api.stages.options.StageOption; import fr.skytasul.quests.players.PlayerAccount; import fr.skytasul.quests.players.PlayersManager; -import fr.skytasul.quests.players.events.PlayerAccountJoinEvent; -import fr.skytasul.quests.players.events.PlayerAccountLeaveEvent; import fr.skytasul.quests.structure.QuestBranch; import fr.skytasul.quests.structure.QuestBranch.Source; import fr.skytasul.quests.utils.Utils; -public abstract class AbstractStage implements Listener{ +public abstract class AbstractStage implements Listener { private final StageType type; protected boolean asyncEnd = false; @@ -41,11 +46,11 @@ public abstract class AbstractStage implements Listener{ private List rewards = new ArrayList<>(); private List validationRequirements = new ArrayList<>(); + private List options; + protected AbstractStage(QuestBranch branch) { this.branch = branch; - - this.type = QuestsAPI.stages.stream().filter(type -> type.clazz == getClass()).findAny() - .orElseThrow(() -> new IllegalArgumentException(getClass().getName() + "has not been registered as a stage type via the API.")); + this.type = QuestsAPI.getStages().getType(getClass()).orElseThrow(() -> new IllegalArgumentException(getClass().getName() + "has not been registered as a stage type via the API.")); Bukkit.getPluginManager().registerEvents(this, BeautyQuests.getInstance()); } @@ -80,6 +85,14 @@ public void setValidationRequirements(List validationRequir this.validationRequirements = validationRequirements; validationRequirements.forEach(requirement -> requirement.attach(branch.getQuest())); } + + public List getOptions() { + return options; + } + + public void setOptions(List options) { + this.options = options; + } public String getCustomText(){ return customText; @@ -140,16 +153,29 @@ protected boolean canUpdate(Player p, boolean msg) { return true; } - public String debugName() { - return "quest " + branch.getQuest().getID() + ", branch " + branch.getID() + ", stage " + getID() + "(" + type.id + ")"; + @Override + public String toString() { + return "stage " + getID() + "(" + type.getID() + ") of quest " + branch.getQuest().getID() + ", branch " + branch.getID(); } + private void propagateStageHandlers(Consumer consumer) { + Consumer newConsumer = handler -> { + try { + consumer.accept(handler); + }catch (Exception ex) { + BeautyQuests.logger.severe("An error occurred while updating stage handler.", ex); + } + }; + QuestsAPI.getQuestsHandlers().forEach(newConsumer); + options.forEach(newConsumer); + } + /** * Called internally when a player finish stage's objectives * @param p Player who finish the stage */ protected final void finishStage(Player p) { - branch.finishStage(p, this); + Utils.runSync(() -> branch.finishStage(p, this)); } /** @@ -170,7 +196,7 @@ public void start(PlayerAccount acc) { Map datas = new HashMap<>(); initPlayerDatas(acc, datas); acc.getQuestDatas(branch.getQuest()).setStageDatas(getStoredID(), datas); - QuestsAPI.propagateQuestsHandlers(handler -> handler.stageStart(acc, this)); + propagateStageHandlers(handler -> handler.stageStart(acc, this)); } protected void initPlayerDatas(PlayerAccount acc, Map datas) {} @@ -181,7 +207,7 @@ protected void initPlayerDatas(PlayerAccount acc, Map datas) {} */ public void end(PlayerAccount acc) { acc.getQuestDatas(branch.getQuest()).setStageDatas(getStoredID(), null); - QuestsAPI.propagateQuestsHandlers(handler -> handler.stageEnd(acc, this)); + propagateStageHandlers(handler -> handler.stageEnd(acc, this)); } /** @@ -189,7 +215,7 @@ public void end(PlayerAccount acc) { * @param acc PlayerAccount which just joined */ public void joins(PlayerAccount acc, Player p) { - QuestsAPI.propagateQuestsHandlers(handler -> handler.stageJoin(acc, p, this)); + propagateStageHandlers(handler -> handler.stageJoin(acc, p, this)); } /** @@ -197,7 +223,7 @@ public void joins(PlayerAccount acc, Player p) { * @param acc PlayerAccount which just left */ public void leaves(PlayerAccount acc, Player p) { - QuestsAPI.propagateQuestsHandlers(handler -> handler.stageLeave(acc, p, this)); + propagateStageHandlers(handler -> handler.stageLeave(acc, p, this)); } public final String getDescriptionLine(PlayerAccount acc, Source source){ @@ -205,8 +231,8 @@ public final String getDescriptionLine(PlayerAccount acc, Source source){ try{ return descriptionLine(acc, source); }catch (Exception ex){ - BeautyQuests.logger.severe("An error occurred while getting the description line for player " + acc.getName() + " in " + debugName(), ex); - return "§a" + type.name; + BeautyQuests.logger.severe("An error occurred while getting the description line for player " + acc.getName() + " in " + toString(), ex); + return "§a" + type.getName(); } } @@ -227,7 +253,7 @@ public final String getDescriptionLine(PlayerAccount acc, Source source){ public void updateObjective(PlayerAccount acc, Player p, String dataKey, Object dataValue) { Map datas = acc.getQuestDatas(branch.getQuest()).getStageDatas(getStoredID()); - Validate.notNull(datas, "Account " + acc.debugName() + " does not have datas for " + debugName()); + Validate.notNull(datas, "Account " + acc.debugName() + " does not have datas for " + toString()); datas.put(dataKey, dataValue); acc.getQuestDatas(branch.getQuest()).setStageDatas(getStoredID(), datas); branch.getBranchesManager().objectiveUpdated(p, acc); @@ -242,7 +268,7 @@ protected T getData(PlayerAccount acc, String dataKey) { * Called when the stage has to be unloaded */ public void unload(){ - QuestsAPI.propagateQuestsHandlers(handler -> handler.stageUnload(this)); + propagateStageHandlers(handler -> handler.stageUnload(this)); HandlerList.unregisterAll(this); rewards.forEach(AbstractReward::detach); validationRequirements.forEach(AbstractRequirement::detach); @@ -252,11 +278,12 @@ public void unload(){ * Called when the stage loads */ public void load() { - QuestsAPI.propagateQuestsHandlers(handler -> handler.stageLoad(this)); + propagateStageHandlers(handler -> handler.stageLoad(this)); } @EventHandler public void onJoin(PlayerAccountJoinEvent e) { + if (e.isFirstJoin()) return; if (branch.hasStageLaunched(e.getPlayerAccount(), this)) { joins(e.getPlayerAccount(), e.getPlayer()); } @@ -278,24 +305,26 @@ protected void serialize(Map map) {} protected void serialize(ConfigurationSection section) { Map map = new HashMap<>(); serialize(map); - map.forEach(section::set); + Utils.setConfigurationSectionContent(section, map); } public final void save(ConfigurationSection section) { serialize(section); - section.set("stageType", type.id); + section.set("stageType", type.getID()); section.set("customText", customText); if (startMessage != null) section.set("text", startMessage); - if (!rewards.isEmpty()) section.set("rewards", Utils.serializeList(rewards, AbstractReward::serialize)); - if (!validationRequirements.isEmpty()) section.set("requirements", Utils.serializeList(validationRequirements, AbstractRequirement::serialize)); + if (!rewards.isEmpty()) section.set("rewards", SerializableObject.serializeList(rewards)); + if (!validationRequirements.isEmpty()) section.set("requirements", SerializableObject.serializeList(validationRequirements)); + + options.stream().filter(StageOption::shouldSave).forEach(option -> option.save(section.createSection("options." + option.getCreator().getID()))); } public static AbstractStage deserialize(ConfigurationSection section, QuestBranch branch) { String typeID = section.getString("stageType"); - Optional> stageTypeOptional = QuestsAPI.stages.stream().filter(type -> type.id.equals(typeID)).findAny(); + Optional> stageTypeOptional = QuestsAPI.getStages().getType(typeID); if (!stageTypeOptional.isPresent()) { BeautyQuests.getInstance().getLogger().severe("Unknown stage type : " + typeID); return null; @@ -307,12 +336,25 @@ public static AbstractStage deserialize(ConfigurationSection section, QuestBranc return null; } - AbstractStage st = stageType.loader.supply(section, branch); + AbstractStage st = stageType.getLoader().supply(section, branch); if (section.contains("text")) st.startMessage = section.getString("text"); if (section.contains("customText")) st.customText = section.getString("customText"); if (section.contains("rewards")) st.setRewards(QuestObject.deserializeList(section.getMapList("rewards"), AbstractReward::deserialize)); if (section.contains("requirements")) st.setValidationRequirements(QuestObject.deserializeList(section.getMapList("requirements"), AbstractRequirement::deserialize)); + st.options = stageType.getOptionsRegistry().getCreators().stream().map(SerializableCreator::newObject).collect(Collectors.toList()); + if (section.contains("options")) { + ConfigurationSection optionsSection = section.getConfigurationSection("options"); + optionsSection.getKeys(false).forEach(optionID -> { + st.options + .stream() + .filter(option -> option.getCreator().getID().equals(optionID)) + .findAny() + .ifPresent(option -> option.load(optionsSection.getConfigurationSection(optionID))); + }); + + } + return st; } } diff --git a/core/src/main/java/fr/skytasul/quests/api/stages/Locatable.java b/core/src/main/java/fr/skytasul/quests/api/stages/Locatable.java deleted file mode 100644 index cf865395..00000000 --- a/core/src/main/java/fr/skytasul/quests/api/stages/Locatable.java +++ /dev/null @@ -1,11 +0,0 @@ -package fr.skytasul.quests.api.stages; - -import org.bukkit.Location; - -public interface Locatable { - - Location getLocation(); - - boolean isShown(); - -} diff --git a/core/src/main/java/fr/skytasul/quests/api/stages/StageCreation.java b/core/src/main/java/fr/skytasul/quests/api/stages/StageCreation.java index ec004ead..45eaba72 100644 --- a/core/src/main/java/fr/skytasul/quests/api/stages/StageCreation.java +++ b/core/src/main/java/fr/skytasul/quests/api/stages/StageCreation.java @@ -2,6 +2,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; import org.bukkit.entity.Player; @@ -10,6 +11,8 @@ import fr.skytasul.quests.api.options.QuestOption; import fr.skytasul.quests.api.requirements.AbstractRequirement; import fr.skytasul.quests.api.rewards.AbstractReward; +import fr.skytasul.quests.api.serializable.SerializableCreator; +import fr.skytasul.quests.api.stages.options.StageOption; import fr.skytasul.quests.editors.TextEditor; import fr.skytasul.quests.gui.ItemUtils; import fr.skytasul.quests.gui.creation.stages.Line; @@ -21,10 +24,13 @@ public abstract class StageCreation { protected final Line line; private final boolean ending; + private StageType type; private List rewards; private List requirements; + private List> options; + private String customDescription, startMessage; private StagesGUI leadingBranch; @@ -62,6 +68,10 @@ public StageCreation(Line line, boolean ending) { }); } + public StageType getType() { + return type; + } + public Line getLine() { return line; } @@ -129,6 +139,10 @@ public void setLeadingBranch(StagesGUI leadingBranch) { this.leadingBranch = leadingBranch; } + public final void setup(StageType type) { + this.type = type; + } + /** * Called when stage item clicked * @param p player who click on the item @@ -138,6 +152,9 @@ public void start(Player p) { setRequirements(new ArrayList<>()); setCustomDescription(null); setStartMessage(null); + + options = type.getOptionsRegistry().getCreators().stream().map(SerializableCreator::newObject).collect(Collectors.toList()); + options.forEach(option -> option.startEdition(this)); } /** @@ -149,6 +166,9 @@ public void edit(T stage) { setRequirements(stage.getValidationRequirements()); setStartMessage(stage.getStartMessage()); setCustomDescription(stage.getCustomText()); + + options = stage.getOptions().stream().map(StageOption::clone).map(x -> (StageOption) x).collect(Collectors.toList()); + options.forEach(option -> option.startEdition(this)); } public final T finish(QuestBranch branch) { @@ -157,6 +177,7 @@ public final T finish(QuestBranch branch) { stage.setValidationRequirements(requirements); stage.setCustomText(customDescription); stage.setStartMessage(startMessage); + stage.setOptions((List) options); return stage; } diff --git a/core/src/main/java/fr/skytasul/quests/api/stages/StageHandler.java b/core/src/main/java/fr/skytasul/quests/api/stages/StageHandler.java new file mode 100644 index 00000000..f153f3c4 --- /dev/null +++ b/core/src/main/java/fr/skytasul/quests/api/stages/StageHandler.java @@ -0,0 +1,21 @@ +package fr.skytasul.quests.api.stages; + +import org.bukkit.entity.Player; + +import fr.skytasul.quests.players.PlayerAccount; + +public interface StageHandler { + + default void stageStart(PlayerAccount acc, AbstractStage stage) {} + + default void stageEnd(PlayerAccount acc, AbstractStage stage) {} + + default void stageJoin(PlayerAccount acc, Player p, AbstractStage stage) {} + + default void stageLeave(PlayerAccount acc, Player p, AbstractStage stage) {} + + default void stageLoad(AbstractStage stage) {} + + default void stageUnload(AbstractStage stage) {} + +} \ No newline at end of file diff --git a/core/src/main/java/fr/skytasul/quests/api/stages/StageType.java b/core/src/main/java/fr/skytasul/quests/api/stages/StageType.java index b9e338f4..d3c07897 100644 --- a/core/src/main/java/fr/skytasul/quests/api/stages/StageType.java +++ b/core/src/main/java/fr/skytasul/quests/api/stages/StageType.java @@ -6,23 +6,46 @@ import org.bukkit.configuration.ConfigurationSection; import org.bukkit.inventory.ItemStack; +import fr.skytasul.quests.BeautyQuests; +import fr.skytasul.quests.api.serializable.SerializableCreator; +import fr.skytasul.quests.api.serializable.SerializableRegistry; +import fr.skytasul.quests.api.stages.options.StageOption; import fr.skytasul.quests.gui.creation.stages.Line; import fr.skytasul.quests.structure.QuestBranch; public class StageType { - public final String id; - public final Class clazz; - public final String name; - public final StageLoader loader; - public final ItemStack item; - public final StageCreationSupplier creationSupplier; + private final String id; + private final Class clazz; + private final String name; + private final StageLoader loader; + private final ItemStack item; + private final StageCreationSupplier creationSupplier; + @Deprecated public final String[] dependencies; + private final SerializableRegistry, SerializableCreator>> optionsRegistry; + + /** + * Creates a stage type. + * + * @param id unique string id for this stage + * @param clazz class of this stage + * @param name proper name of this stage + * @param loader function which instanciates and loads values of a previously saved stage + * @param item item representing this stage in the Stages GUI + * @param creationSupplier function creating a stage creation context + */ + public StageType(String id, Class clazz, String name, StageLoader loader, ItemStack item, StageCreationSupplier creationSupplier) { + this(id, clazz, name, loader, item, creationSupplier, new String[0]); + } + + @Deprecated public StageType(String id, Class clazz, String name, StageDeserializationSupplier deserializationSupplier, ItemStack item, StageCreationSupplier creationSupplier, String... dependencies) { this(id, clazz, name, (StageLoader) deserializationSupplier, item, creationSupplier, dependencies); } + @Deprecated public StageType(String id, Class clazz, String name, StageLoader loader, ItemStack item, StageCreationSupplier creationSupplier, String... dependencies) { this.id = id; this.clazz = clazz; @@ -30,9 +53,42 @@ public StageType(String id, Class clazz, String name, StageLoader loader, this.item = item; this.loader = loader; this.creationSupplier = creationSupplier; + + this.optionsRegistry = new SerializableRegistry<>("stage-options-" + id); + this.dependencies = dependencies; + if (dependencies.length != 0) BeautyQuests.logger.warning("Nag author of the " + id + " stage type about its use of the deprecated \"dependencies\" feature."); + } + + public String getID() { + return id; + } + + public Class getStageClass() { + return clazz; } + public String getName() { + return name; + } + + public StageLoader getLoader() { + return loader; + } + + public ItemStack getItem() { + return item; + } + + public StageCreationSupplier getCreationSupplier() { + return creationSupplier; + } + + public SerializableRegistry, SerializableCreator>> getOptionsRegistry() { + return optionsRegistry; + } + + @Deprecated public boolean isValid() { for (String depend : dependencies) { if (!Bukkit.getPluginManager().isPluginEnabled(depend)) return false; diff --git a/core/src/main/java/fr/skytasul/quests/api/stages/StageTypeRegistry.java b/core/src/main/java/fr/skytasul/quests/api/stages/StageTypeRegistry.java new file mode 100644 index 00000000..4143f385 --- /dev/null +++ b/core/src/main/java/fr/skytasul/quests/api/stages/StageTypeRegistry.java @@ -0,0 +1,50 @@ +package fr.skytasul.quests.api.stages; + +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; + +import org.apache.commons.lang.Validate; + +import fr.skytasul.quests.utils.DebugUtils; + +public class StageTypeRegistry implements Iterable> { + + private List> types = new LinkedList<>(); + + /** + * Registers new stage type into the plugin. + * @param type StageType instance + */ + public void register(StageType type) { + Validate.notNull(type); + types.add(type); + DebugUtils.logMessage("Stage registered (" + type.getName() + ", " + (types.size() - 1) + ")"); + } + + public List> getTypes() { + return types; + } + + public Optional> getType(Class stageClass) { + return types + .stream() + .filter(type -> type.getStageClass() == stageClass) + .map(type -> (StageType) type) + .findAny(); + } + + public Optional> getType(String id) { + return types + .stream() + .filter(type -> type.getID().equals(id)) + .findAny(); + } + + @Override + public Iterator> iterator() { + return types.iterator(); + } + +} diff --git a/core/src/main/java/fr/skytasul/quests/api/stages/options/StageOption.java b/core/src/main/java/fr/skytasul/quests/api/stages/options/StageOption.java new file mode 100644 index 00000000..c132cd48 --- /dev/null +++ b/core/src/main/java/fr/skytasul/quests/api/stages/options/StageOption.java @@ -0,0 +1,32 @@ +package fr.skytasul.quests.api.stages.options; + +import fr.skytasul.quests.api.QuestsAPI; +import fr.skytasul.quests.api.serializable.SerializableObject; +import fr.skytasul.quests.api.stages.AbstractStage; +import fr.skytasul.quests.api.stages.StageCreation; +import fr.skytasul.quests.api.stages.StageHandler; + +public abstract class StageOption extends SerializableObject implements StageHandler { + + private final Class stageClass; + + protected StageOption(Class stageClass) { + super(QuestsAPI.getStages() + .getType(stageClass) + .orElseThrow(() -> new IllegalArgumentException(stageClass.getName() + "has not been registered as a stage type via the API.")) + .getOptionsRegistry()); + this.stageClass = stageClass; + } + + public Class getStageClass() { + return stageClass; + } + + @Override + public abstract StageOption clone(); + + public abstract void startEdition(StageCreation creation); + + public abstract boolean shouldSave(); + +} diff --git a/core/src/main/java/fr/skytasul/quests/api/stages/types/AbstractCountableBlockStage.java b/core/src/main/java/fr/skytasul/quests/api/stages/types/AbstractCountableBlockStage.java new file mode 100644 index 00000000..897be7e4 --- /dev/null +++ b/core/src/main/java/fr/skytasul/quests/api/stages/types/AbstractCountableBlockStage.java @@ -0,0 +1,91 @@ +package fr.skytasul.quests.api.stages.types; + +import java.util.Map; +import java.util.Map.Entry; + +import org.bukkit.block.Block; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import fr.skytasul.quests.api.stages.StageCreation; +import fr.skytasul.quests.gui.Inventories; +import fr.skytasul.quests.gui.ItemUtils; +import fr.skytasul.quests.gui.blocks.BlocksGUI; +import fr.skytasul.quests.gui.creation.stages.Line; +import fr.skytasul.quests.structure.QuestBranch; +import fr.skytasul.quests.utils.Lang; +import fr.skytasul.quests.utils.types.BQBlock; + +public abstract class AbstractCountableBlockStage extends AbstractCountableStage { + + protected AbstractCountableBlockStage(QuestBranch branch, Map> objects) { + super(branch, objects); + } + + @Override + protected boolean objectApplies(BQBlock object, Object other) { + if (other instanceof Block) return object.applies((Block) other); + return super.objectApplies(object, other); + } + + @Override + protected String getName(BQBlock object) { + return object.getName(); + } + + @Override + protected Object serialize(BQBlock object) { + return object.getAsString(); + } + + @Override + protected BQBlock deserialize(Object object) { + return BQBlock.fromString((String) object); + } + + public abstract static class AbstractCreator extends StageCreation { + + protected Map> blocks; + + protected AbstractCreator(Line line, boolean ending) { + super(line, ending); + + line.setItem(getBlocksSlot(), getBlocksItem(), (p, item) -> { + BlocksGUI blocksGUI = Inventories.create(p, new BlocksGUI()); + blocksGUI.setBlocksFromMap(blocks); + blocksGUI.run = obj -> { + setBlocks(obj); + reopenGUI(p, true); + }; + }); + } + + protected abstract ItemStack getBlocksItem(); + + protected int getBlocksSlot() { + return 7; + } + + public void setBlocks(Map> blocks) { + this.blocks = blocks; + line.editItem(getBlocksSlot(), ItemUtils.lore(line.getItem(getBlocksSlot()), Lang.optionValue.format(blocks.size() + " blocks"))); + } + + @Override + public void start(Player p) { + super.start(p); + Inventories.create(p, new BlocksGUI()).run = obj -> { + setBlocks(obj); + reopenGUI(p, true); + }; + } + + @Override + public void edit(T stage) { + super.edit(stage); + setBlocks(stage.cloneObjects()); + } + + } + +} \ No newline at end of file diff --git a/core/src/main/java/fr/skytasul/quests/api/stages/types/AbstractCountableStage.java b/core/src/main/java/fr/skytasul/quests/api/stages/types/AbstractCountableStage.java new file mode 100644 index 00000000..3a6b6395 --- /dev/null +++ b/core/src/main/java/fr/skytasul/quests/api/stages/types/AbstractCountableStage.java @@ -0,0 +1,293 @@ +package fr.skytasul.quests.api.stages.types; + +import java.util.AbstractMap; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.function.Supplier; + +import org.bukkit.Bukkit; +import org.bukkit.boss.BarColor; +import org.bukkit.boss.BarStyle; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.entity.Player; +import org.bukkit.scheduler.BukkitTask; + +import fr.skytasul.quests.BeautyQuests; +import fr.skytasul.quests.QuestsConfiguration; +import fr.skytasul.quests.api.QuestsAPI; +import fr.skytasul.quests.api.bossbar.BQBossBarManager.BQBossBar; +import fr.skytasul.quests.api.stages.AbstractStage; +import fr.skytasul.quests.players.PlayerAccount; +import fr.skytasul.quests.structure.QuestBranch; +import fr.skytasul.quests.structure.QuestBranch.Source; +import fr.skytasul.quests.utils.Lang; +import fr.skytasul.quests.utils.Utils; + +public abstract class AbstractCountableStage extends AbstractStage { + + protected Map> objects; + + protected Map bars = new HashMap<>(); + private boolean barsEnabled = false; + private int cachedSize = 0; + + public AbstractCountableStage(QuestBranch branch, Map> objects) { + super(branch); + this.objects = objects; + calculateSize(); + } + + public Map> getObjects() { + return objects; + } + + public Map> cloneObjects() { + Map> map = new HashMap<>(); + for (Entry> entry : objects.entrySet()) { + map.put(entry.getKey(), new AbstractMap.SimpleEntry<>(cloneObject(entry.getValue().getKey()), entry.getValue().getValue())); + } + return map; + } + + public Map getPlayerRemainings(PlayerAccount acc, boolean warnNull) { + Map remaining = getData(acc, "remaining"); + if (warnNull && remaining == null) + BeautyQuests.logger.severe("Cannot retrieve stage datas for " + acc.getNameAndID() + " on " + super.toString()); + return remaining; + } + + protected void calculateSize() { + cachedSize = 0; + for (Entry objectsEntry : objects.values()) { + cachedSize += objectsEntry.getValue(); + } + barsEnabled = QuestsConfiguration.showMobsProgressBar() && cachedSize > 0; + } + + @Override + protected String descriptionLine(PlayerAccount acc, Source source){ + return Utils.descriptionLines(source, buildRemainingArray(acc, source)); + } + + @Override + protected Supplier[] descriptionFormat(PlayerAccount acc, Source source) { + return new Supplier[] { () -> Utils.descriptionLines(source, buildRemainingArray(acc, source)) }; + } + + private String[] buildRemainingArray(PlayerAccount acc, Source source) { + Map playerAmounts = getPlayerRemainings(acc, true); + if (playerAmounts == null) return new String[] { "§4§lerror" }; + String[] elements = new String[playerAmounts.size()]; + int i = 0; + for (Entry obj : playerAmounts.entrySet()) { + Entry object = objects.get(obj.getKey()); + elements[i] = object == null ? "no object " + obj.getKey() : QuestsConfiguration.getItemNameColor() + Utils.getStringFromNameAndAmount(getName(object.getKey()), QuestsConfiguration.getItemAmountColor(), obj.getValue(), object.getValue(), QuestsConfiguration.showDescriptionItemsXOne(source)); + i++; + } + return elements; + } + + @Override + protected void initPlayerDatas(PlayerAccount acc, Map datas) { + super.initPlayerDatas(acc, datas); + Map amounts = new HashMap<>(); + for (Entry> entry : objects.entrySet()) { + amounts.put(entry.getKey(), entry.getValue().getValue()); + } + datas.put("remaining", amounts); + } + + /** + * When called, this will test the player datas for the passed object. + * If found, the remaining amount will be lowered. + * If no remaining items are found, the stage will complete. + * @param acc player account + * @param p player + * @param object object of the event + * @param amount amount completed + * @return false if there is no need to call this method again in the same game tick. + */ + public boolean event(PlayerAccount acc, Player p, Object object, int amount) { + if (amount < 0) throw new IllegalArgumentException("Event amount must be positive (" + amount + ")"); + if (!canUpdate(p)) return true; + for (Entry> entry : objects.entrySet()) { + int id = entry.getKey(); + if (objectApplies(entry.getValue().getKey(), object)) { + Map playerAmounts = getPlayerRemainings(acc, true); + if (playerAmounts == null) return true; + if (playerAmounts.containsKey(id)) { + int playerAmount = playerAmounts.get(id); + if (playerAmount <= amount) { + playerAmounts.remove(id); + }else playerAmounts.put(id, playerAmount -= amount); + } + + if (playerAmounts.isEmpty()) { + finishStage(p); + return true; + }else { + if (barsEnabled) { + BossBar bar = bars.get(p); + if (bar == null) { + BeautyQuests.logger.warning(p.getName() + " does not have boss bar for stage " + toString() + ". This is a bug!"); + }else bar.update(playerAmounts.values().stream().mapToInt(Integer::intValue).sum()); + } + updateObjective(acc, p, "remaining", playerAmounts); + return false; + } + } + } + return true; + } + + @Override + public void start(PlayerAccount acc) { + super.start(acc); + if (acc.isCurrent()) createBar(acc.getPlayer(), cachedSize); + } + + @Override + public void end(PlayerAccount acc) { + super.end(acc); + if (acc.isCurrent()) removeBar(acc.getPlayer()); + } + + @Override + public void unload() { + super.unload(); + bars.values().forEach(BossBar::remove); + } + + @Override + public void joins(PlayerAccount acc, Player p) { + super.joins(acc, p); + Map remainings = getPlayerRemainings(acc, true); + if (remainings == null) return; + createBar(p, remainings.values().stream().mapToInt(Integer::intValue).sum()); + } + + @Override + public void leaves(PlayerAccount acc, Player p) { + super.leaves(acc, p); + removeBar(p); + } + + protected void createBar(Player p, int amount) { + if (barsEnabled) { + if (bars.containsKey(p)) { + BeautyQuests.logger.warning("Trying to create an already existing bossbar for player " + p.getName()); + return; + } + bars.put(p, new BossBar(p, amount)); + } + } + + protected void removeBar(Player p) { + if (bars.containsKey(p)) bars.remove(p).remove(); + } + + protected boolean objectApplies(T object, Object other) { + return object.equals(other); + } + + protected T cloneObject(T object) { + return object; + } + + protected abstract String getName(T object); + + protected abstract Object serialize(T object); + + protected abstract T deserialize(Object object); + + /** + * @deprecated for removal, {@link #serialize(ConfigurationSection)} should be used instead. + */ + @Override + @Deprecated + protected void serialize(Map map) {} + + @Override + protected void serialize(ConfigurationSection section) { + ConfigurationSection objectsSection = section.createSection("objects"); + for (Entry> obj : objects.entrySet()) { + ConfigurationSection objectSection = objectsSection.createSection(Integer.toString(obj.getKey())); + objectSection.set("amount", obj.getValue().getValue()); + objectSection.set("object", serialize(obj.getValue().getKey())); + } + Map serialized = new HashMap<>(); + serialize(serialized); + Utils.setConfigurationSectionContent(section, serialized); + } + + /** + * @deprecated for removal, {@link #deserialize(ConfigurationSection)} should be used instead. + */ + @Deprecated + protected void deserialize(Map serializedDatas) { + deserialize(Utils.createConfigurationSection(serializedDatas)); + } + + protected void deserialize(ConfigurationSection section) { + ConfigurationSection objectsSection = section.getConfigurationSection("objects"); + if (objectsSection != null) { + for (String key : objectsSection.getKeys(false)) { + ConfigurationSection object = objectsSection.getConfigurationSection(key); + Object serialized = object.get("object"); + if (serialized instanceof ConfigurationSection) serialized = ((ConfigurationSection) serialized).getValues(false); + objects.put(Integer.parseInt(key), new AbstractMap.SimpleEntry<>(deserialize(serialized), object.getInt("amount"))); + } + } + + if (objects.isEmpty()) BeautyQuests.logger.warning("Stage with no content: " + toString()); + calculateSize(); + } + + class BossBar { + private Player p; + private BQBossBar bar; + private BukkitTask timer; + + public BossBar(Player p, int amount) { + this.p = p; + + BarStyle style = null; + if (cachedSize % 20 == 0) { + style = BarStyle.SEGMENTED_20; + }else if (cachedSize % 10 == 0) { + style = BarStyle.SEGMENTED_10; + }else if (cachedSize % 12 == 0) { + style = BarStyle.SEGMENTED_12; + }else if (cachedSize % 6 == 0) { + style = BarStyle.SEGMENTED_6; + }else style = BarStyle.SOLID; + bar = QuestsAPI.getBossBarManager().buildBossBar(Lang.MobsProgression.format(branch.getQuest().getName(), 100, 100), BarColor.YELLOW, style); + update(amount); + } + + public void remove() { + bar.removeAll(); + if (timer != null) timer.cancel(); + } + + public void update(int amount) { + if (amount >= 0 && amount <= cachedSize) { + bar.setProgress((double) (cachedSize - amount) / (double) cachedSize); + }else BeautyQuests.logger.warning("Amount of objects superior to max objects in " + AbstractCountableStage.this.toString() + " for player " + p.getName() + ": " + amount + " > " + cachedSize); + bar.setTitle(Lang.MobsProgression.format(branch.getQuest().getName(), cachedSize - amount, cachedSize)); + bar.addPlayer(p); + timer(); + } + + private void timer() { + if (QuestsConfiguration.getProgressBarTimeout() <= 0) return; + if (timer != null) timer.cancel(); + timer = Bukkit.getScheduler().runTaskLater(BeautyQuests.getInstance(), () -> { + bar.removePlayer(p); + timer = null; + }, QuestsConfiguration.getProgressBarTimeout() * 20L); + } + } + +} diff --git a/core/src/main/java/fr/skytasul/quests/api/stages/AbstractEntityStage.java b/core/src/main/java/fr/skytasul/quests/api/stages/types/AbstractEntityStage.java similarity index 73% rename from core/src/main/java/fr/skytasul/quests/api/stages/AbstractEntityStage.java rename to core/src/main/java/fr/skytasul/quests/api/stages/types/AbstractEntityStage.java index a9cceff2..2151eabf 100644 --- a/core/src/main/java/fr/skytasul/quests/api/stages/AbstractEntityStage.java +++ b/core/src/main/java/fr/skytasul/quests/api/stages/types/AbstractEntityStage.java @@ -1,6 +1,12 @@ -package fr.skytasul.quests.api.stages; +package fr.skytasul.quests.api.stages.types; +import java.util.AbstractMap; +import java.util.Comparator; import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Spliterator; +import java.util.Spliterators; import java.util.function.Supplier; import org.bukkit.configuration.ConfigurationSection; @@ -9,6 +15,10 @@ import fr.skytasul.quests.BeautyQuests; import fr.skytasul.quests.QuestsConfiguration; +import fr.skytasul.quests.api.stages.AbstractStage; +import fr.skytasul.quests.api.stages.StageCreation; +import fr.skytasul.quests.api.stages.types.Locatable.LocatableType; +import fr.skytasul.quests.api.stages.types.Locatable.LocatedType; import fr.skytasul.quests.editors.TextEditor; import fr.skytasul.quests.editors.checkers.NumberParser; import fr.skytasul.quests.gui.ItemUtils; @@ -23,7 +33,8 @@ import fr.skytasul.quests.utils.Utils; import fr.skytasul.quests.utils.XMaterial; -public abstract class AbstractEntityStage extends AbstractStage { +@LocatableType (types = LocatedType.ENTITY) +public abstract class AbstractEntityStage extends AbstractStage implements Locatable.MultipleLocatable { protected EntityType entity; protected int amount; @@ -40,7 +51,7 @@ protected void event(Player p, EntityType type) { if (entity == null || type.equals(entity)) { Integer playerAmount = getPlayerAmount(acc); if (playerAmount == null) { - BeautyQuests.logger.warning(p.getName() + " does not have object datas for stage " + debugName() + ". This is a bug!"); + BeautyQuests.logger.warning(p.getName() + " does not have object datas for stage " + toString() + ". This is a bug!"); }else if (playerAmount.intValue() <= 1) { finishStage(p); }else { @@ -78,6 +89,28 @@ protected Supplier[] descriptionFormat(PlayerAccount acc, Source source) return new Supplier[] { () -> getMobsLeft(acc) }; } + @Override + public boolean canBeFetchedAsynchronously() { + return false; + } + + @Override + public Spliterator getNearbyLocated(NearbyFetcher fetcher) { + if (!fetcher.isTargeting(LocatedType.ENTITY)) return Spliterators.emptySpliterator(); + return fetcher.getCenter().getWorld() + .getEntitiesByClass(entity.getEntityClass()) + .stream() + .map(x -> { + double ds = x.getLocation().distanceSquared(fetcher.getCenter()); + if (ds > fetcher.getMaxDistanceSquared()) return null; + return new AbstractMap.SimpleEntry<>(x, ds); + }) + .filter(Objects::nonNull) + .sorted(Comparator.comparing(Entry::getValue)) + .map(entry -> Located.LocatedEntity.create(entry.getKey())) + .spliterator(); + } + public abstract static class AbstractCreator extends StageCreation { protected EntityType entity = null; @@ -105,7 +138,7 @@ protected AbstractCreator(Line line, boolean ending) { public void setEntity(EntityType entity) { this.entity = entity; - line.editItem(6, ItemUtils.lore(line.getItem(6), entity == null ? Lang.EntityTypeAny.toString() : entity.name())); + line.editItem(6, ItemUtils.lore(line.getItem(6), Lang.optionValue.format(entity == null ? Lang.EntityTypeAny.toString() : entity.name()))); } public void setAmount(int amount) { diff --git a/core/src/main/java/fr/skytasul/quests/api/stages/AbstractItemStage.java b/core/src/main/java/fr/skytasul/quests/api/stages/types/AbstractItemStage.java similarity index 92% rename from core/src/main/java/fr/skytasul/quests/api/stages/AbstractItemStage.java rename to core/src/main/java/fr/skytasul/quests/api/stages/types/AbstractItemStage.java index 905cbdb1..21e5f81a 100644 --- a/core/src/main/java/fr/skytasul/quests/api/stages/AbstractItemStage.java +++ b/core/src/main/java/fr/skytasul/quests/api/stages/types/AbstractItemStage.java @@ -1,4 +1,4 @@ -package fr.skytasul.quests.api.stages; +package fr.skytasul.quests.api.stages.types; import java.util.AbstractMap; import java.util.Collections; @@ -13,6 +13,7 @@ import org.bukkit.inventory.ItemStack; import fr.skytasul.quests.api.comparison.ItemComparisonMap; +import fr.skytasul.quests.api.stages.StageCreation; import fr.skytasul.quests.gui.ItemUtils; import fr.skytasul.quests.gui.creation.ItemsGUI; import fr.skytasul.quests.gui.creation.stages.Line; @@ -35,7 +36,7 @@ protected AbstractItemStage(QuestBranch branch, ConfigurationSection section) { super(branch, new HashMap<>()); if (section.contains("itemComparisons")) { - comparisons = new ItemComparisonMap((Map) section.getConfigurationSection("itemComparisons").getValues(false)); + comparisons = new ItemComparisonMap(section.getConfigurationSection("itemComparisons")); }else comparisons = new ItemComparisonMap(); super.deserialize(section); @@ -100,12 +101,12 @@ protected Creator(Line line, boolean ending) { public void setItems(List items) { this.items = Utils.combineItems(items); - line.editItem(6, ItemUtils.lore(line.getItem(6), Lang.optionValue.format(this.items.size() + " item(s)"))); + line.editItem(6, ItemUtils.lore(line.getItem(6), Lang.optionValue.format(Lang.AmountItems.format(this.items.size())))); } public void setComparisons(ItemComparisonMap comparisons) { this.comparisons = comparisons; - line.editItem(7, ItemUtils.lore(line.getItem(7), Lang.optionValue.format(this.comparisons.getEffective().size() + " comparison(s)"))); + line.editItem(7, ItemUtils.lore(line.getItem(7), Lang.optionValue.format(Lang.AmountComparisons.format(this.comparisons.getEffective().size())))); } @Override diff --git a/core/src/main/java/fr/skytasul/quests/api/stages/Dialogable.java b/core/src/main/java/fr/skytasul/quests/api/stages/types/Dialogable.java similarity index 68% rename from core/src/main/java/fr/skytasul/quests/api/stages/Dialogable.java rename to core/src/main/java/fr/skytasul/quests/api/stages/types/Dialogable.java index 697a37cb..4e9bd201 100644 --- a/core/src/main/java/fr/skytasul/quests/api/stages/Dialogable.java +++ b/core/src/main/java/fr/skytasul/quests/api/stages/types/Dialogable.java @@ -1,4 +1,4 @@ -package fr.skytasul.quests.api.stages; +package fr.skytasul.quests.api.stages.types; import fr.skytasul.quests.api.npcs.BQNPC; import fr.skytasul.quests.utils.types.Dialog; @@ -13,7 +13,7 @@ public interface Dialogable { BQNPC getNPC(); default boolean hasDialog() { - return getDialog() != null; + return getNPC() != null && getDialog() != null && !getDialog().messages.isEmpty(); } } diff --git a/core/src/main/java/fr/skytasul/quests/api/stages/types/Locatable.java b/core/src/main/java/fr/skytasul/quests/api/stages/types/Locatable.java new file mode 100644 index 00000000..b3d7a6c1 --- /dev/null +++ b/core/src/main/java/fr/skytasul/quests/api/stages/types/Locatable.java @@ -0,0 +1,368 @@ +package fr.skytasul.quests.api.stages.types; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.Spliterator; +import org.bukkit.Location; +import org.bukkit.block.Block; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import fr.skytasul.quests.api.stages.types.Locatable.MultipleLocatable; +import fr.skytasul.quests.utils.DebugUtils; + +/** + * This interface indicates that an object can provide some locations on demand. + *

+ * Valid subinterfaces are {@link PreciseLocatable} and {@link MultipleLocatable}. + * A class must not directly implement this interface but one of those. + *

+ * Classes implementing one of the subinterfaces are also supposed to have the + * {@link LocatedType} annotation to indicate which types of located objects can be + * retrieved. If no annotation is attached, it will be assumed that all kinds of + * located objects can be retrieved. + */ +public interface Locatable { + + /** + * If no indication should be displayed to the player, + * then this method should return false. + * + * @param player Player to test for indications + * @return true if location indications should be displayed + * to the player, false otherwise. + */ + default boolean isShown(Player player) { + return true; + } + + /** + * Indicates if the Located instances gotten from {@link PreciseLocatable#getLocated()} + * and {@link MultipleLocatable#getNearbyLocated(MultipleLocatable.NearbyFetcher)} + * can be safely retrieved from an asynchronous thread. + * + * @return true only if the Located fetch operations can + * be done asynchronously, false otherwise. + */ + default boolean canBeFetchedAsynchronously() { + return true; + } + + /** + * This interface indicates that an object can provide a unique and precise location, + * no matter the player. + */ + interface PreciseLocatable extends Locatable { + + /** + * Gets the uniquely located object. + *

+ * The result should be consistent, which means that calling it twice without + * having something else changed in the game state would return the same value. + * @return the located object + */ + Located getLocated(); + + } + + /** + * This interface indicates that an object can provide multiple locations depending on + * factors such as a center and a maximum distance, detailed in {@link NearbyFetcher}. + */ + interface MultipleLocatable extends Locatable { + + /** + * Gets a {@link Spliterator} of all targets in the region specified by + * the {@link NearbyFetcher} parameter. + * @param fetcher describes the region from where the targets must be found + * @return a Spliterator which allows iterating through the targets + */ + Spliterator getNearbyLocated(NearbyFetcher fetcher); + + /** + * This POJO contains informations on the region from where + * the {@link MultipleLocatable} object has to find its targets. + */ + interface NearbyFetcher { + + Location getCenter(); + + double getMaxDistance(); + + default double getMaxDistanceSquared() { + return getMaxDistance() * getMaxDistance(); + } + + default boolean isTargeting(LocatedType type) { + return true; + } + + static NearbyFetcher create(Location location, double maxDistance) { + return new NearbyFetcherImpl(location, maxDistance, null); + } + + static NearbyFetcher create(Location location, double maxDistance, LocatedType targetType) { + return new NearbyFetcherImpl(location, maxDistance, targetType); + } + + class NearbyFetcherImpl implements NearbyFetcher { + private final Location center; + private final double maxDistance; + private final LocatedType targetType; + + public NearbyFetcherImpl(Location center, double maxDistance, LocatedType targetType) { + this.center = center; + this.maxDistance = maxDistance; + this.targetType = targetType; + } + + @Override + public Location getCenter() { + return center; + } + + @Override + public double getMaxDistance() { + return maxDistance; + } + + @Override + public boolean isTargeting(LocatedType type) { + return targetType == null || targetType == type; + } + + } + + } + + } + + /** + * This annotation indicates which types of {@link Located} objects can be retrieved from the attached class. + *

+ * It should only be attached to {@link Locatable} implementing classes. + */ + @Retention (RetentionPolicy.RUNTIME) + @Target (ElementType.TYPE) + @interface LocatableType { + /** + * @return an array of {@link LocatedType} that can be retrieved + * from the class attached to this annotation. + */ + LocatedType[] types() default { LocatedType.ENTITY, LocatedType.BLOCK, LocatedType.OTHER }; + } + + /** + * Represents something that is locatable on a world. + */ + interface Located { + + Location getLocation(); + + LocatedType getType(); + + static Located create(Location location) { + return new LocatedImpl(location); + } + + class LocatedImpl implements Located { + protected Location location; + + public LocatedImpl(Location location) { + this.location = location; + } + + @Override + public Location getLocation() { + return location.clone(); + } + + @Override + public LocatedType getType() { + return LocatedType.OTHER; + } + + @Override + public int hashCode() { + return location.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof LocatedImpl)) return false; + LocatedImpl other = (LocatedImpl) obj; + return other.location.equals(location); + } + + } + + interface LocatedEntity extends Located { + + Entity getEntity(); + + @Override + default Location getLocation() { + Entity entity = getEntity(); + return entity == null ? null : entity.getLocation(); + } + + @Override + default LocatedType getType() { + return LocatedType.ENTITY; + } + + static LocatedEntity create(Entity entity) { + return new LocatedEntityImpl(entity); + } + + class LocatedEntityImpl implements LocatedEntity { + private Entity entity; + + public LocatedEntityImpl(Entity entity) { + this.entity = entity; + } + + @Override + public Entity getEntity() { + return entity; + } + + @Override + public int hashCode() { + return entity.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof LocatedEntityImpl)) return false; + LocatedEntityImpl other = (LocatedEntityImpl) obj; + return other.entity.equals(entity); + } + + } + + } + + interface LocatedBlock extends Located { + + default Block getBlock() { + return getLocation().getBlock(); + } + + default Block getBlockNullable() { + Location location = getLocation(); + if (location == null || location.getWorld() == null) + return null; + return location.getBlock(); + } + + @Override + default LocatedType getType() { + return LocatedType.BLOCK; + } + + static LocatedBlock create(Block block) { + return new LocatedBlockImpl(block); + } + + static LocatedBlock create(Location location) { + return new LocatedBlockLocationImpl(location); + } + + class LocatedBlockLocationImpl extends LocatedImpl implements LocatedBlock { + public LocatedBlockLocationImpl(Location location) { + super(location); + } + + @Override + public Block getBlock() { + return location.getBlock(); + } + + @Override + public LocatedType getType() { + // As LocatedBlockImpl inherits getType() + // from both LocatedBlock AND LocatedImpl, + // we redefine the method correctly. + return LocatedType.BLOCK; + } + + } + + class LocatedBlockImpl implements LocatedBlock { + + private Block block; + + public LocatedBlockImpl(Block block) { + this.block = block; + } + + @Override + public Location getLocation() { + return block.getLocation(); + } + + @Override + public Block getBlock() { + return block; + } + + @Override + public Block getBlockNullable() { + return getBlock(); + } + + } + + } + + } + + enum LocatedType { + BLOCK, ENTITY, OTHER; + } + + /** + * Allows to check if some {@link LocatedType}s can be retrieved from a {@link Locatable} class. + *

+ * If the Class clazz does not have a {@link LocatableType} annotation attached, + * then this will return true as it is assumed that it can retrieve all kinds of + * located types. + * + * @param clazz Class implementing the {@link Locatable} interface for which the types + * will get tested. + * @param types Array of {@link LocatedType}s that must be checked they can be retrieved from + * the clazz. + * @return true if the clazz can retrieve all passed types + * OR if the clazz does not have a {@link LocatableType} annotation attached, + * false otherwise. + */ + static boolean hasLocatedTypes(Class clazz, LocatedType... types) { + Set toTest = new HashSet<>(Arrays.asList(types)); + boolean foundAnnotation = false; + + Class superclass = clazz; + do { + LocatableType annotation = superclass.getDeclaredAnnotation(Locatable.LocatableType.class); + if (annotation != null) { + foundAnnotation = true; + for (Locatable.LocatedType locatedType : annotation.types()) { + toTest.remove(locatedType); + if (toTest.isEmpty()) return true; + } + } + }while ((superclass = superclass.getSuperclass()) != null); + + if (!foundAnnotation) { + DebugUtils.logMessage("Class " + clazz.getName() + " does not have the @LocatableType annotation."); + return true; + } + + return false; + } + +} diff --git a/core/src/main/java/fr/skytasul/quests/commands/Cmd.java b/core/src/main/java/fr/skytasul/quests/commands/Cmd.java deleted file mode 100644 index 20e3d267..00000000 --- a/core/src/main/java/fr/skytasul/quests/commands/Cmd.java +++ /dev/null @@ -1,60 +0,0 @@ -package fr.skytasul.quests.commands; - -import static java.lang.annotation.ElementType.METHOD; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -import org.bukkit.entity.Player; - -import fr.skytasul.quests.api.npcs.BQNPC; -import fr.skytasul.quests.structure.Quest; - -@Retention(RUNTIME) -@Target(METHOD) -public @interface Cmd { - - /** - * If true, the command will not be executed if the executor is not a Player - * @return true if the command need to be executed by a player - */ - public boolean player() default false; - - /** - * Need {@link #player()} to be true - * @return if the player must not be in an inventory/editor system to execute the command - */ - public boolean noEditorInventory() default false; - - /** - * If arguments amount is lower than this value, the command will not be executed - * @return minimal amount of arguments for the command to be executed - */ - public int min() default 0; - - /** - * Available : - *

    - *
  • PLAYERS : list of players online - *
  • QUESTSID : list of all quests IDs - *
  • NPCSID : list of all NPCs IDs - *
  • xxx|yyy|zzz : available values, separated by a pipe (|) - *
- * In the case of PLAYERS QUESTSID and NPCSID, they will be directly replaced by - * an instance of {@link Player} / {@link Quest} / {@link BQNPC} when command executing - * (no need for String parsing) - * @return String array of possibles arguments - */ - public String[] args() default {}; - - /** - * Needed permission to execute this command (if empty, no permission will be required)
- * Final permission will be : beautyquests.command.XXXX - * @return name of the permission - */ - public String permission() default ""; - - public boolean hide() default false; - -} diff --git a/core/src/main/java/fr/skytasul/quests/commands/CommandContext.java b/core/src/main/java/fr/skytasul/quests/commands/CommandContext.java deleted file mode 100644 index 164d8739..00000000 --- a/core/src/main/java/fr/skytasul/quests/commands/CommandContext.java +++ /dev/null @@ -1,37 +0,0 @@ -package fr.skytasul.quests.commands; - -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; - -public class CommandContext { - - public final CommandsManager manager; - public final Object[] args; - public final String label; - public final CommandSender sender; - public final Player player; - - public CommandContext(CommandsManager manager, CommandSender sender, Object[] args, String label){ - this.manager = manager; - this.args = args; - this.label = label; - this.sender = sender; - if (sender instanceof Player){ - this.player = (Player) sender; - }else this.player = null; - } - - public boolean isPlayer(){ - return player != null; - } - - public T get(int arg) { - return get(arg, null); - } - - public T get(int arg, T def) { - if (args.length <= arg) return def; - return (T) args[arg]; - } - -} \ No newline at end of file diff --git a/core/src/main/java/fr/skytasul/quests/commands/Commands.java b/core/src/main/java/fr/skytasul/quests/commands/Commands.java deleted file mode 100644 index 5f3c69ad..00000000 --- a/core/src/main/java/fr/skytasul/quests/commands/Commands.java +++ /dev/null @@ -1,698 +0,0 @@ -package fr.skytasul.quests.commands; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.net.URL; -import java.nio.channels.Channels; -import java.nio.channels.ReadableByteChannel; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.Optional; - -import org.bukkit.Bukkit; -import org.bukkit.Material; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.FireworkMeta; -import org.bukkit.inventory.meta.ItemMeta; - -import fr.skytasul.quests.BeautyQuests; -import fr.skytasul.quests.api.QuestsAPI; -import fr.skytasul.quests.api.stages.AbstractStage; -import fr.skytasul.quests.api.stages.Dialogable; -import fr.skytasul.quests.editors.Editor; -import fr.skytasul.quests.editors.SelectNPC; -import fr.skytasul.quests.gui.Inventories; -import fr.skytasul.quests.gui.creation.QuestCreationSession; -import fr.skytasul.quests.gui.misc.ConfirmGUI; -import fr.skytasul.quests.gui.misc.ListBook; -import fr.skytasul.quests.gui.pools.PoolsManageGUI; -import fr.skytasul.quests.gui.quests.ChooseQuestGUI; -import fr.skytasul.quests.gui.quests.PlayerListGUI; -import fr.skytasul.quests.gui.quests.QuestsListGUI; -import fr.skytasul.quests.options.OptionStartDialog; -import fr.skytasul.quests.players.AdminMode; -import fr.skytasul.quests.players.PlayerAccount; -import fr.skytasul.quests.players.PlayerPoolDatas; -import fr.skytasul.quests.players.PlayerQuestDatas; -import fr.skytasul.quests.players.PlayersManager; -import fr.skytasul.quests.players.PlayersManagerDB; -import fr.skytasul.quests.players.PlayersManagerYAML; -import fr.skytasul.quests.rewards.CheckpointReward; -import fr.skytasul.quests.scoreboards.Scoreboard; -import fr.skytasul.quests.structure.BranchesManager; -import fr.skytasul.quests.structure.Quest; -import fr.skytasul.quests.structure.QuestBranch; -import fr.skytasul.quests.structure.pools.QuestPool; -import fr.skytasul.quests.utils.Database; -import fr.skytasul.quests.utils.Lang; -import fr.skytasul.quests.utils.MinecraftNames; -import fr.skytasul.quests.utils.Utils; -import fr.skytasul.quests.utils.nms.NMS; -import fr.skytasul.quests.utils.types.DialogRunner; - -public class Commands { - - @Cmd (permission = "create", player = true, noEditorInventory = true) - public void create(CommandContext cmd){ - QuestCreationSession session = new QuestCreationSession(); - if (cmd.args.length == 1) { - Integer id = Utils.parseInt(cmd.sender, cmd.get(0)); - if (id == null) return; - if (QuestsAPI.getQuests().getQuest(id) != null) { - Utils.sendMessage(cmd.sender, "Invalid quest ID: another quest exists with ID {0}", id); - return; - } - session.setCustomID(id); - } - session.openMainGUI(cmd.player); - } - - @Cmd (permission = "edit", args = "QUESTSID", player = true, noEditorInventory = true) - public void edit(CommandContext cmd){ - if (cmd.args.length >= 1) { - new QuestCreationSession(cmd.get(0)).openMainGUI(cmd.player); - return; - } - Lang.CHOOSE_NPC_STARTER.send(cmd.player); - new SelectNPC(cmd.player, () -> {}, npc -> { - if (npc == null) return; - if (!npc.getQuests().isEmpty()) { - Inventories.create(cmd.player, new ChooseQuestGUI(npc.getQuests(), quest -> { - if (quest == null) return; - new QuestCreationSession(quest).openMainGUI(cmd.player); - })); - }else { - Lang.NPC_NOT_QUEST.send(cmd.player); - } - }).enter(); - } - - @Cmd(permission = "remove", args = "QUESTSID") - public void remove(CommandContext cmd){ - if (cmd.args.length >= 1){ - try{ - remove(cmd.sender, (Quest) cmd.args[0]); - }catch (NumberFormatException ex){ - Lang.NUMBER_INVALID.send(cmd.sender, cmd.args[1]); - } - return; - }else if (!cmd.isPlayer()){ - Lang.INCORRECT_SYNTAX.send(cmd.sender); - return; - } - Lang.CHOOSE_NPC_STARTER.send(cmd.sender); - new SelectNPC(cmd.player, () -> {}, npc -> { - if (npc == null) return; - if (!npc.getQuests().isEmpty()) { - Inventories.create(cmd.player, new ChooseQuestGUI(npc.getQuests(), quest -> { - if (quest == null) return; - remove(cmd.sender, quest); - })); - }else { - Lang.NPC_NOT_QUEST.send(cmd.sender); - } - }).enter(); - } - - @Cmd (permission = "manage") - public void reload(CommandContext cmd){ - BeautyQuests.getInstance().performReload(cmd.sender); - } - - @Cmd(permission = "version") - public void version(CommandContext cmd){ - cmd.sender.sendMessage("§eBeautyQuests version : §6§l" + BeautyQuests.getInstance().getDescription().getVersion()); - } - - @Cmd (permission = "manage") - public void save(CommandContext cmd){ - try { - BeautyQuests.getInstance().saveAllConfig(false); - cmd.sender.sendMessage("§aDatas saved!"); - BeautyQuests.logger.info("Datas saved ~ manual save from " + cmd.sender.getName()); - } catch (Throwable e) { - e.printStackTrace(); - cmd.sender.sendMessage("Error while saving the data file."); - } - } - - @Cmd(permission = "finish", min = 1, args = "PLAYERS") - public void finishAll(CommandContext cmd){ - Player target = (Player) cmd.args[0]; - PlayerAccount acc = PlayersManager.getPlayerAccount(target); - int success = 0; - int errors = 0; - for (Quest q : QuestsAPI.getQuests().getQuestsStarted(acc)) { - try{ - q.finish(target); - success++; - }catch (Throwable ex){ - ex.printStackTrace(); - errors++; - continue; - } - } - Lang.LEAVE_ALL_RESULT.send(cmd.sender, success, errors); - } - - @Cmd(permission = "finish", min = 2, args = {"PLAYERS", "QUESTSID"}) - public void finish(CommandContext cmd){ - Player target = (Player) cmd.args[0]; - try{ - ((Quest) cmd.args[1]).finish(target); - Lang.LEAVE_ALL_RESULT.send(cmd.sender, 1, 0); - }catch (Throwable ex){ - ex.printStackTrace(); - Lang.LEAVE_ALL_RESULT.send(cmd.sender, 1, 1); - } - } - - @Cmd(permission = "setStage", min = 2, args = {"PLAYERS", "QUESTSID", "0|1|2|3|4|5|6|7|8|9|10|11|12|13|14", "0|1|2|3|4|5|6|7|8|9|10|11|12|13|14"}) - public void setStage(CommandContext cmd){ - Player target = (Player) cmd.args[0]; - Quest qu = (Quest) cmd.args[1]; - PlayerAccount acc = PlayersManager.getPlayerAccount(target); - BranchesManager manager = qu.getBranchesManager(); // syntax: no arg: next or start | 1 arg: start branch | 2 args: set branch stage - - PlayerQuestDatas datas = acc.getQuestDatasIfPresent(qu); - if (cmd.args.length < 3 && (datas == null || !datas.hasStarted())) { // start quest - qu.start(target); - Lang.START_QUEST.send(cmd.sender, qu.getName(), acc.debugName()); - return; - } - if (datas == null) datas = acc.getQuestDatas(qu); // creates quest datas - - QuestBranch currentBranch = manager.getBranch(datas.getBranch()); - - if (cmd.args.length < 3) { // next - if (!datas.isInEndingStages()) { - currentBranch.finishStage(target, currentBranch.getRegularStage(datas.getStage())); - Lang.COMMAND_SETSTAGE_NEXT.send(cmd.sender); - }else Lang.COMMAND_SETSTAGE_NEXT_UNAVAILABLE.send(cmd.sender); - }else { - Integer branchID = Utils.parseInt(cmd.sender, (String) cmd.args[2]); - if (branchID == null) return; - QuestBranch branch = manager.getBranch(branchID); - if (branch == null){ - Lang.COMMAND_SETSTAGE_BRANCH_DOESNTEXIST.send(cmd.sender, branchID); - return; - } - - Integer stageID = -1; - if (cmd.args.length > 3){ - stageID = Utils.parseInt(cmd.sender, (String) cmd.args[3]); - if (stageID == null) return; - if (stageID < 0) { - Lang.NUMBER_NEGATIVE.send(cmd.sender); - return; - } - if (currentBranch == null) { - Lang.ERROR_OCCURED.send(cmd.sender, "player " + acc.debugName() + " has not started quest"); - return; - } - if (currentBranch.getRegularStages().size() <= stageID) { - Lang.COMMAND_SETSTAGE_STAGE_DOESNTEXIST.send(cmd.sender, stageID); - return; - } - } - Lang.COMMAND_SETSTAGE_SET.send(cmd.sender, branchID); - if (currentBranch != null) { - if (datas.isInEndingStages()) { - for (AbstractStage stage : currentBranch.getEndingStages().keySet()) stage.end(acc); - }else { - currentBranch.getRegularStage(datas.getStage()).end(acc); - } - } - if (cmd.args.length == 3){ // start branch - branch.start(acc); - }else { // set stage in branch - datas.setBranch(branchID); - branch.setStage(acc, stageID); - } - QuestsAPI.propagateQuestsHandlers(handler -> handler.questUpdated(acc, target, qu)); - } - } - - @Cmd (permission = "setStage", min = 2, args = { "PLAYERS", "QUESTSID" }) - public void startDialog(CommandContext cmd) { - Player target = (Player) cmd.args[0]; - Quest qu = (Quest) cmd.args[1]; - PlayerAccount acc = PlayersManager.getPlayerAccount(target); - PlayerQuestDatas datas = acc.getQuestDatasIfPresent(qu); - - DialogRunner runner = null; - if (datas == null || !qu.hasStarted(acc)) { - if (qu.hasOption(OptionStartDialog.class)) { - runner = qu.getOption(OptionStartDialog.class).getDialogRunner(); - } - }else { - if (datas.isInEndingStages() || datas.isInQuestEnd()) { - Lang.COMMAND_STARTDIALOG_IMPOSSIBLE.send(cmd.sender); - return; - }else { - AbstractStage stage = qu.getBranchesManager().getBranch(datas.getBranch()).getRegularStage(datas.getStage()); - if (stage instanceof Dialogable) { - runner = ((Dialogable) stage).getDialogRunner(); - } - } - } - - if (runner == null) { - Lang.COMMAND_STARTDIALOG_NO.send(cmd.sender); - }else { - if (runner.isPlayerInDialog(target)) { - Lang.COMMAND_STARTDIALOG_ALREADY.send(cmd.sender); - }else { - runner.handleNext(target); - Lang.COMMAND_STARTDIALOG_SUCCESS.send(cmd.sender, target.getName(), qu.getID()); - } - } - } - - @Cmd(permission = "resetPlayer", min = 1, args = "PLAYERS") - public void resetPlayer(CommandContext cmd){ - Player target = (Player) cmd.args[0]; - PlayerAccount acc = PlayersManager.getPlayerAccount(target); - int quests = 0, pools = 0; - for (PlayerQuestDatas questDatas : new ArrayList<>(acc.getQuestsDatas())) { - Quest quest = questDatas.getQuest(); - if (quest != null) { - quest.resetPlayer(acc); - }else acc.removeQuestDatas(questDatas.getQuestID()); - quests++; - } - for (PlayerPoolDatas poolDatas : new ArrayList<>(acc.getPoolDatas())) { - QuestPool pool = poolDatas.getPool(); - if (pool != null) { - pool.resetPlayer(acc); - }else acc.removePoolDatas(poolDatas.getPoolID()); - pools++; - } - if (acc.isCurrent()) Lang.DATA_REMOVED.send(acc.getPlayer(), quests, cmd.sender.getName(), pools); - Lang.DATA_REMOVED_INFO.send(cmd.sender, quests, target.getName(), pools); - } - - @Cmd(permission = "resetPlayer", min = 1, args = {"PLAYERS", "QUESTSID"}) - public void resetPlayerQuest(CommandContext cmd){ - Player target = (Player) cmd.args[0]; - PlayerAccount acc = PlayersManager.getPlayerAccount(target); - if (cmd.args.length > 1){ - Quest qu = (Quest) cmd.args[1]; - reset(cmd.sender, target, acc, qu); - }else if (cmd.isPlayer()){ - QuestsListGUI gui = new QuestsListGUI((obj) -> { - reset(cmd.sender, target, acc, obj); - }, acc, true, false, true); - Inventories.create(cmd.player, gui); - }else Lang.INCORRECT_SYNTAX.sendWP(cmd.sender); - } - - @Cmd (permission = "resetPlayer", min = 2, args = { "PLAYERS", "POOLSID", "BOOLEAN" }) - public void resetPlayerPool(CommandContext cmd) { - Player target = (Player) cmd.args[0]; - PlayerAccount acc = PlayersManager.getPlayerAccount(target); - QuestPool pool = cmd.get(1); - if (Boolean.parseBoolean(cmd.get(2, "false"))) { // only timer - pool.resetPlayerTimer(acc); - Lang.POOL_RESET_TIMER.send(cmd.sender, pool.getID(), target.getName()); - }else { - pool.resetPlayer(acc); - Lang.POOL_RESET_FULL.send(cmd.sender, pool.getID(), target.getName()); - } - } - - @Cmd(permission = "seePlayer", player = true, min = 1, args = "PLAYERS") - public void seePlayer(CommandContext cmd){ - Player target = (Player) cmd.args[0]; - new PlayerListGUI(PlayersManager.getPlayerAccount(target), false).create(cmd.player); - } - - @Cmd(permission = "resetQuest", min = 1, args = {"QUESTSID"}) - public void resetQuest(CommandContext cmd) { - Quest qu = (Quest) cmd.args[0]; - int amount = 0; - for (Player p : Bukkit.getOnlinePlayers()) { - if (qu.resetPlayer(PlayersManager.getPlayerAccount(p))) amount++; - } - amount += PlayersManager.manager.removeQuestDatas(qu); - Lang.QUEST_PLAYERS_REMOVED.send(cmd.sender, amount); - } - - @Cmd (permission = "start", min = 1, args = { "PLAYERS", "QUESTSID", "BOOLEAN" }) - public void start(CommandContext cmd){ - Player target = (Player) cmd.args[0]; - boolean testRequirements = !(CommandsManager.hasPermission(cmd.sender, "start.other", false) && (cmd.args.length > 2 ? Boolean.parseBoolean(cmd.get(2)) : false)); - if (cmd.isPlayer()){ - if (target == cmd.player){ - if (!CommandsManager.hasPermission(cmd.player, "start", true)) return; - }else if (!CommandsManager.hasPermission(cmd.player, "start.other", true)) return; - } - PlayerAccount acc = PlayersManager.getPlayerAccount(target); - if (cmd.args.length < 2 && cmd.isPlayer()){ - QuestsListGUI gui = new QuestsListGUI((obj) -> { - Quest qu = obj; - if (testRequirements && !qu.isLauncheable(target, acc, true)) { - Lang.START_QUEST_NO_REQUIREMENT.send(cmd.sender, qu.getName()); - return; - } - qu.start(target); - Lang.START_QUEST.send(cmd.sender, qu.getName(), acc.abstractAcc.getIdentifier()); - }, acc, false, true, false); - Inventories.create(cmd.player, gui); - }else if (cmd.args.length >= 2){ - Quest qu = (Quest) cmd.args[1]; - if (testRequirements && !qu.isLauncheable(target, acc, true)) { - Lang.START_QUEST_NO_REQUIREMENT.send(cmd.sender, qu.getName()); - return; - } - qu.start(target); - Lang.START_QUEST.send(cmd.sender, qu.getName(), acc.abstractAcc.getIdentifier()); - }else { - Lang.INCORRECT_SYNTAX.send(cmd.sender); - } - } - - @Cmd (permission = "cancel", min = 1, args = { "PLAYERS", "QUESTSID" }) - public void cancel(CommandContext cmd){ - Player target = (Player) cmd.args[0]; - if (cmd.isPlayer()){ - if (target != cmd.player && !CommandsManager.hasPermission(cmd.player, "cancel.other", true)) return; - } - PlayerAccount acc = PlayersManager.getPlayerAccount(target); - if (acc == null) { - Lang.PLAYER_DATA_NOT_FOUND.send(cmd.sender, target.getName()); - return; - } - - if (cmd.args.length < 2 && cmd.isPlayer()){ - QuestsListGUI gui = new QuestsListGUI((obj) -> { - cancelQuest(cmd.sender, acc, obj); - }, acc, true, false, false); - Inventories.create(cmd.player, gui); - }else if (cmd.args.length >= 2){ - cancelQuest(cmd.sender, acc, (Quest) cmd.args[1]); - }else { - Lang.INCORRECT_SYNTAX.send(cmd.sender); - } - } - - @Cmd(permission = "setItem", player = true, min = 1, args = "talk|launch|nolaunch") - public void setItem(CommandContext cmd){ - String name = (String) cmd.args[0]; - if (!"talk".equalsIgnoreCase(name) && !"launch".equalsIgnoreCase(name) && !"nolaunch".equalsIgnoreCase(name)){ - Lang.INCORRECT_SYNTAX.send(cmd.sender); - return; - } - ItemStack item = cmd.player.getInventory().getItemInMainHand(); - if (item.getType() == Material.AIR) { - BeautyQuests.getInstance().getDataFile().set(name.toLowerCase() + "Item", null); - Lang.ITEM_REMOVED.send(cmd.sender); - return; - } - BeautyQuests.getInstance().getDataFile().set(name.toLowerCase() + "Item", item.serialize()); - Lang.ITEM_CHANGED.send(cmd.sender); - } - - @Cmd (permission = "setItem", player = true) - public void setFirework(CommandContext cmd) { - if ("none".equals(cmd.get(0, null))) { - BeautyQuests.getInstance().getDataFile().set("firework", "none"); - Lang.FIREWORK_REMOVED.send(cmd.sender); - Lang.RESTART_SERVER.send(cmd.sender); - }else { - ItemMeta meta = cmd.player.getInventory().getItemInMainHand().getItemMeta(); - if (meta instanceof FireworkMeta) { - BeautyQuests.getInstance().getDataFile().set("firework", meta); - Lang.FIREWORK_EDITED.send(cmd.sender); - Lang.RESTART_SERVER.send(cmd.sender); - }else { - Lang.FIREWORK_INVALID_HAND.send(cmd.sender); - } - } - } - - @Cmd (permission = "manage", min = 1, args = "save|force") - public void backup(CommandContext cmd){ - if (cmd.args[0].equals("save")){ - save(cmd); - }else if (!cmd.args[0].equals("force")){ - Lang.INCORRECT_SYNTAX.send(cmd.sender); - return; - } - - boolean success = true; - BeautyQuests.logger.info("Creating backup due to " + cmd.sender.getName() + "'s manual command."); - if (!BeautyQuests.getInstance().createFolderBackup()) { - Lang.BACKUP_QUESTS_FAILED.send(cmd.sender); - success = false; - } - if (!BeautyQuests.getInstance().createDataBackup()) { - Lang.BACKUP_PLAYERS_FAILED.send(cmd.sender); - success = false; - } - if (success) Lang.BACKUP_CREATED.send(cmd.sender); - } - - @Cmd(permission = "adminMode") - public void adminMode(CommandContext cmd){ - AdminMode.toggle(cmd.sender); - } - - @Cmd (player = true, hide = true) - public void exitEditor(CommandContext cmd){ - Editor.leave(cmd.player); - Inventories.closeAndExit(cmd.player); - } - - @Cmd (player = true, hide = true) - public void reopenInventory(CommandContext cmd){ - if (Inventories.isInSystem(cmd.player)){ - Inventories.openInventory(cmd.player); - } - } - - @Cmd(permission = "list", player = true) - public void list(CommandContext cmd){ - if (NMS.isValid()){ - ListBook.openQuestBook(cmd.player); - }else Utils.sendMessage(cmd.sender, "Version not supported"); - } - - @Cmd (args = { "PLAYERS", "setline|removeline|resetline|resetall|hide|show" }) - public void scoreboard(CommandContext cmd){ - if (cmd.args.length == 0) { - if (!cmd.isPlayer()) { - Lang.MUST_PLAYER.sendWP(cmd.sender); - return; - } - - if (!CommandsManager.hasPermission(cmd.sender, "scoreboard.toggle", true)) return; - Scoreboard board = BeautyQuests.getInstance().getScoreboardManager().getPlayerScoreboard(cmd.player); - if (board.isForceHidden()) { - board.show(true); - Lang.COMMAND_SCOREBOARD_OWN_SHOWN.send(cmd.player); - }else { - board.hide(true); - Lang.COMMAND_SCOREBOARD_OWN_HIDDEN.send(cmd.player); - } - }else { - if (!CommandsManager.hasPermission(cmd.sender, "scoreboard", true)) return; - if (cmd.args.length < 2) { - Lang.INCORRECT_SYNTAX.sendWP(cmd.sender); - return; - } - Player p = (Player) cmd.args[0]; - Scoreboard board = BeautyQuests.getInstance().getScoreboardManager().getPlayerScoreboard(p); - - switch (((String) cmd.args[1]).toLowerCase()) { - case "setline": - if (cmd.args.length < 4) { - Lang.INCORRECT_SYNTAX.send(cmd.sender); - break; - } - Integer id = Utils.parseInt(cmd.sender, (String) cmd.args[2]); - if (id == null) return; - board.setCustomLine(id, Utils.buildFromArray(cmd.args, 3, " ")); - Lang.COMMAND_SCOREBOARD_LINESET.send(cmd.sender, id); - break; - case "removeline": - if (cmd.args.length < 3) { - Lang.INCORRECT_SYNTAX.send(cmd.sender); - break; - } - id = Utils.parseInt(cmd.sender, (String) cmd.args[2]); - if (id == null) return; - if (board.removeLine(id)) { - Lang.COMMAND_SCOREBOARD_LINEREMOVE.send(cmd.sender, id); - }else Lang.COMMAND_SCOREBOARD_LINENOEXIST.send(cmd.sender, id); - break; - case "resetline": - if (cmd.args.length < 3) { - Lang.INCORRECT_SYNTAX.send(cmd.sender); - break; - } - id = Utils.parseInt(cmd.sender, (String) cmd.args[2]); - if (id == null) return; - if (board.resetLine(id)) { - Lang.COMMAND_SCOREBOARD_LINERESET.send(cmd.sender, id); - }else Lang.COMMAND_SCOREBOARD_LINENOEXIST.send(cmd.sender, id); - break; - case "resetall": - BeautyQuests.getInstance().getScoreboardManager().removePlayerScoreboard(p); - BeautyQuests.getInstance().getScoreboardManager().create(p); - Lang.COMMAND_SCOREBOARD_RESETALL.send(cmd.sender, p.getName()); - break; - case "hide": - board.hide(true); - Lang.COMMAND_SCOREBOARD_HIDDEN.send(cmd.sender, p.getName()); - break; - case "show": - board.show(true); - Lang.COMMAND_SCOREBOARD_SHOWN.send(cmd.sender, p.getName()); - break; - default: - Lang.INCORRECT_SYNTAX.send(cmd.sender); - break; - } - } - } - - @Cmd (player = true, permission = "pools") - public void pools(CommandContext cmd) { - PoolsManageGUI.get().create(cmd.player); - } - - @Cmd (player = true, args = "QUESTSID", min = 1) - public void checkpoint(CommandContext cmd) { - Quest quest = cmd.get(0); - PlayerAccount account = PlayersManager.getPlayerAccount(cmd.player); - if (account.hasQuestDatas(quest)) { - PlayerQuestDatas datas = account.getQuestDatas(quest); - QuestBranch branch = quest.getBranchesManager().getBranch(datas.getBranch()); - int max = datas.isInEndingStages() ? branch.getStageSize() : datas.getStage(); - for (int id = max - 1; id >= 0; id--) { - AbstractStage stage = branch.getRegularStage(id); - Optional optionalCheckpoint = stage.getRewards().stream().filter(CheckpointReward.class::isInstance).findAny().map(CheckpointReward.class::cast); - if (optionalCheckpoint.isPresent()) { - optionalCheckpoint.get().applies(cmd.player); - return; - } - } - Lang.COMMAND_CHECKPOINT_NO.send(cmd.sender, quest.getName()); - }else Lang.COMMAND_CHECKPOINT_NOT_STARTED.send(cmd.sender); - } - - @Cmd (permission = "manage") - public void downloadTranslations(CommandContext cmd) { - if (NMS.getMCVersion() < 13) { - Utils.sendMessage(cmd.sender, "§c" + Lang.VERSION_REQUIRED.toString(), "≥ 1.13"); - return; - } - if (cmd.args.length == 0) { - Lang.COMMAND_TRANSLATION_SYNTAX.send(cmd.sender); - return; - } - String lang = cmd.get(0).toLowerCase(); - String version = NMS.getVersionString(); - String url = MinecraftNames.LANG_DOWNLOAD_URL.replace("%version%", version).replace("%language%", lang); - - try { - File destination = new File(BeautyQuests.getInstance().getDataFolder(), lang + ".json"); - if (destination.isDirectory()) { - Lang.ERROR_OCCURED.send(cmd.sender, lang + ".json is a directory"); - return; - } - if (!(cmd.args.length > 1 && Boolean.parseBoolean(cmd.get(1))) && destination.exists()) { - Lang.COMMAND_TRANSLATION_EXISTS.send(cmd.sender, lang + ".json"); - return; - } - try (ReadableByteChannel channel = Channels.newChannel(new URL(url).openStream())) { - destination.createNewFile(); - try (FileOutputStream output = new FileOutputStream(destination)) { - output.getChannel().transferFrom(channel, 0, Long.MAX_VALUE); - Lang.COMMAND_TRANSLATION_DOWNLOADED.send(cmd.sender, lang); - } - }catch (FileNotFoundException ex) { - Lang.COMMAND_TRANSLATION_NOT_FOUND.send(cmd.sender, lang, version); - } - }catch (IOException e) { - Lang.ERROR_OCCURED.send(cmd.sender, "IO Exception when downloading translation."); - BeautyQuests.logger.severe("An error occurred while downloading translation.", e); - } - } - - @Cmd (permission = "manage") - public void migrateDatas(CommandContext cmd) { - if (!(PlayersManager.manager instanceof PlayersManagerYAML)) { - cmd.sender.sendMessage("§cYou can't migrate YAML datas to a DB system if you are already using the DB system."); - return; - } - Utils.runAsync(() -> { - cmd.sender.sendMessage("§aConnecting to the database."); - Database db = null; - try { - db = new Database(BeautyQuests.getInstance().getConfig().getConfigurationSection("database")); - db.testConnection(); - cmd.sender.sendMessage("§aConnection to database etablished."); - final Database fdb = db; - Utils.runSync(() -> { - cmd.sender.sendMessage("§aStarting migration..."); - try { - cmd.sender.sendMessage(PlayersManagerDB.migrate(fdb, (PlayersManagerYAML) PlayersManager.manager)); - }catch (Exception ex) { - cmd.sender.sendMessage("§cAn exception occured during migration. Process aborted. " + ex.getMessage()); - ex.printStackTrace(); - } - }); - }catch (SQLException ex) { - cmd.sender.sendMessage("§cConnection to database has failed. Aborting. " + ex.getMessage()); - BeautyQuests.logger.severe("An error occurred while connecting to the database for datas migration.", ex); - if (db != null) db.closeConnection(); - } - }); - } - - @Cmd(permission = "help") - public void help(CommandContext cmd){ - for (Lang l : Lang.values()){ - if (l.getPath().startsWith("msg.command.help.")){ - String command = l.getPath().substring(17); - if (command.equals("header")){ - l.sendWP(cmd.sender); - }else if (CommandsManager.hasPermission(cmd.sender, cmd.manager.commands.get(command.toLowerCase()).cmd.permission(), false)) l.sendWP(cmd.sender, cmd.label); - } - } - } - - private static void reset(CommandSender sender, Player target, PlayerAccount acc, Quest qu){ - qu.resetPlayer(acc); - if (acc.isCurrent()) Lang.DATA_QUEST_REMOVED.send(target, qu.getName(), sender.getName()); - Lang.DATA_QUEST_REMOVED_INFO.send(sender, target.getName(), qu.getName()); - } - - private static void remove(CommandSender sender, Quest quest){ - if (sender instanceof Player){ - Inventories.create((Player) sender, new ConfirmGUI(() -> { - quest.remove(true, true); - Lang.SUCCESFULLY_REMOVED.send(sender, quest.getName()); - }, ((Player) sender)::closeInventory, Lang.INDICATION_REMOVE.format(quest.getName()))); - }else { - quest.remove(true, true); - Lang.SUCCESFULLY_REMOVED.send(sender, quest.getName()); - } - } - - private static void cancelQuest(CommandSender sender, PlayerAccount acc, Quest qu){ - if (!sender.hasPermission("beautyquests.command.cancel.other") && !qu.isCancellable()){ - Lang.CANCEL_QUEST_UNAVAILABLE.send(sender, qu.getName()); - return; - } - qu.cancelPlayer(acc); - Lang.CANCEL_QUEST.send(sender, qu.getName()); - } - -} diff --git a/core/src/main/java/fr/skytasul/quests/commands/CommandsAdmin.java b/core/src/main/java/fr/skytasul/quests/commands/CommandsAdmin.java new file mode 100644 index 00000000..b0df6f61 --- /dev/null +++ b/core/src/main/java/fr/skytasul/quests/commands/CommandsAdmin.java @@ -0,0 +1,304 @@ +package fr.skytasul.quests.commands; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.net.URL; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; +import java.nio.file.Path; +import java.sql.SQLException; + +import org.bukkit.Material; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.FireworkMeta; +import org.bukkit.inventory.meta.ItemMeta; + +import fr.skytasul.quests.BeautyQuests; +import fr.skytasul.quests.api.QuestsAPI; +import fr.skytasul.quests.api.npcs.BQNPC; +import fr.skytasul.quests.editors.Editor; +import fr.skytasul.quests.editors.SelectNPC; +import fr.skytasul.quests.gui.Inventories; +import fr.skytasul.quests.gui.creation.QuestCreationSession; +import fr.skytasul.quests.gui.misc.ConfirmGUI; +import fr.skytasul.quests.gui.misc.ListBook; +import fr.skytasul.quests.gui.pools.PoolsManageGUI; +import fr.skytasul.quests.gui.quests.ChooseQuestGUI; +import fr.skytasul.quests.players.AdminMode; +import fr.skytasul.quests.players.PlayersManager; +import fr.skytasul.quests.players.PlayersManagerDB; +import fr.skytasul.quests.players.PlayersManagerYAML; +import fr.skytasul.quests.structure.Quest; +import fr.skytasul.quests.utils.Database; +import fr.skytasul.quests.utils.Lang; +import fr.skytasul.quests.utils.MinecraftNames; +import fr.skytasul.quests.utils.Utils; +import fr.skytasul.quests.utils.nms.NMS; + +import revxrsal.commands.annotation.Flag; +import revxrsal.commands.annotation.Optional; +import revxrsal.commands.annotation.SecretCommand; +import revxrsal.commands.annotation.Subcommand; +import revxrsal.commands.annotation.Switch; +import revxrsal.commands.bukkit.BukkitCommandActor; +import revxrsal.commands.bukkit.annotation.CommandPermission; +import revxrsal.commands.exception.CommandErrorException; +import revxrsal.commands.orphan.OrphanCommand; + +public class CommandsAdmin implements OrphanCommand { + + @Subcommand ("create") + @CommandPermission (value = "beautyquests.command.create") + @OutsideEditor + public void create(Player player, @Optional @Flag Integer id) { + QuestCreationSession session = new QuestCreationSession(); + if (id != null) { + if (id.intValue() < 0) throw new CommandErrorException(Lang.NUMBER_NEGATIVE.toString()); + if (QuestsAPI.getQuests().getQuest(id) != null) + throw new CommandErrorException("Invalid quest ID: another quest exists with ID {0}", id); + + session.setCustomID(id); + } + session.openMainGUI(player); + } + + @Subcommand ("edit") + @CommandPermission (value = "beautyquests.command.edit") + @OutsideEditor + public void edit(Player player, @Optional Quest quest) { + if (quest != null) { + new QuestCreationSession(quest).openMainGUI(player); + }else { + Lang.CHOOSE_NPC_STARTER.send(player); + new SelectNPC(player, () -> {}, npc -> { + if (npc == null) return; + if (!npc.getQuests().isEmpty()) { + new ChooseQuestGUI(npc.getQuests(), questClicked -> { + if (questClicked == null) return; + new QuestCreationSession(questClicked).openMainGUI(player); + }).create(player); + }else { + Lang.NPC_NOT_QUEST.send(player); + } + }).enter(); + } + } + + @Subcommand ("remove") + @CommandPermission (value = "beautyquests.command.remove") + @OutsideEditor + public void remove(BukkitCommandActor actor, @Optional Quest quest) { + if (quest != null) { + remove(actor.getSender(), quest); + }else { + Lang.CHOOSE_NPC_STARTER.send(actor.requirePlayer()); + new SelectNPC(actor.getAsPlayer(), () -> {}, npc -> { + if (npc == null) return; + if (!npc.getQuests().isEmpty()) { + new ChooseQuestGUI(npc.getQuests(), questClicked -> { + if (questClicked == null) return; + remove(actor.getSender(), questClicked); + }).create(actor.getAsPlayer()); + }else { + Lang.NPC_NOT_QUEST.send(actor.getAsPlayer()); + } + }).enter(); + } + } + + private void remove(CommandSender sender, Quest quest) { + if (sender instanceof Player) { + Inventories.create((Player) sender, new ConfirmGUI(() -> { + quest.remove(true, true); + Lang.SUCCESFULLY_REMOVED.send(sender, quest.getName()); + }, ((Player) sender)::closeInventory, Lang.INDICATION_REMOVE.format(quest.getName()))); + }else { + quest.remove(true, true); + Lang.SUCCESFULLY_REMOVED.send(sender, quest.getName()); + } + } + + @Subcommand ("pools") + @CommandPermission ("beautyquests.command.pools") + public void pools(Player player) { + PoolsManageGUI.get().create(player); + } + + @Subcommand ("reload") + @CommandPermission ("beautyquests.command.manage") + public void reload(BukkitCommandActor actor) { + BeautyQuests.getInstance().performReload(actor.getSender()); + } + + @Subcommand ("save") + @CommandPermission ("beautyquests.command.manage") + public void save(BukkitCommandActor actor) { + try { + BeautyQuests.getInstance().saveAllConfig(false); + actor.reply("§aDatas saved!"); + BeautyQuests.logger.info("Datas saved ~ manual save from " + actor.getName()); + }catch (Throwable e) { + e.printStackTrace(); + actor.error("Error while saving the data file."); + } + } + + @Subcommand ("backup") + @CommandPermission ("beautyquests.command.manage") + public void backup(BukkitCommandActor actor, @Switch boolean force) { + if (!force) save(actor); + + boolean success = true; + BeautyQuests.logger.info("Creating backup due to " + actor.getName() + "'s manual command."); + Path backup = BeautyQuests.getInstance().backupDir(); + if (!BeautyQuests.getInstance().createFolderBackup(backup)) { + Lang.BACKUP_QUESTS_FAILED.send(actor.getSender()); + success = false; + } + if (!BeautyQuests.getInstance().createDataBackup(backup)) { + Lang.BACKUP_PLAYERS_FAILED.send(actor.getSender()); + success = false; + } + if (success) Lang.BACKUP_CREATED.send(actor.getSender()); + } + + @Subcommand ("adminMode") + @CommandPermission ("beautyquests.command.adminMode") + public void adminMode(BukkitCommandActor actor) { + AdminMode.toggle(actor.getSender()); + } + + @Subcommand ("exitEditor") + @SecretCommand + public void exitEditor(Player player) { + Editor.leave(player); + Inventories.closeAndExit(player); + } + + @Subcommand ("reopenInventory") + @SecretCommand + public void reopenInventory(Player player) { + if (Inventories.isInSystem(player)) { + Inventories.openInventory(player); + } + } + + @Subcommand ("list") + @CommandPermission ("beautyquests.command.list") + public void list(Player player) { + if (NMS.isValid()) { + ListBook.openQuestBook(player); + }else Utils.sendMessage(player, "Version not supported"); + } + + @Subcommand ("downloadTranslations") + @CommandPermission ("beautyquests.command.manage") + public void downloadTranslations(BukkitCommandActor actor, @Optional String lang, @Switch boolean overwrite) { + if (NMS.getMCVersion() < 13) + throw new CommandErrorException(Lang.VERSION_REQUIRED.format("≥ 1.13")); + + if (lang == null) + throw new CommandErrorException(Lang.COMMAND_TRANSLATION_SYNTAX.toString()); + + String version = NMS.getVersionString(); + String url = MinecraftNames.LANG_DOWNLOAD_URL.replace("%version%", version).replace("%language%", lang); + + try { + File destination = new File(BeautyQuests.getInstance().getDataFolder(), lang + ".json"); + if (destination.isDirectory()) + throw new CommandErrorException(Lang.ERROR_OCCURED.format(lang + ".json is a directory")); + if (!overwrite && destination.exists()) + throw new CommandErrorException(Lang.COMMAND_TRANSLATION_EXISTS.format(lang + ".json")); + + try (ReadableByteChannel channel = Channels.newChannel(new URL(url).openStream())) { + destination.createNewFile(); + try (FileOutputStream output = new FileOutputStream(destination)) { + output.getChannel().transferFrom(channel, 0, Long.MAX_VALUE); + Lang.COMMAND_TRANSLATION_DOWNLOADED.send(actor.getSender(), lang); + } + }catch (FileNotFoundException ex) { + throw new CommandErrorException(Lang.COMMAND_TRANSLATION_NOT_FOUND.format(lang, version)); + } + }catch (IOException e) { + BeautyQuests.logger.severe("An error occurred while downloading translation.", e); + throw new CommandErrorException(Lang.ERROR_OCCURED.format("IO Exception when downloading translation.")); + } + } + + @Subcommand ("migrateDatas") + @CommandPermission ("beautyquests.command.manage") + public void migrateDatas(BukkitCommandActor actor) { + if (!(PlayersManager.manager instanceof PlayersManagerYAML)) + throw new CommandErrorException("§cYou can't migrate YAML datas to a DB system if you are already using the DB system."); + + Utils.runAsync(() -> { + actor.reply("§aConnecting to the database."); + try (Database db = new Database(BeautyQuests.getInstance().getConfig().getConfigurationSection("database"))) { + db.testConnection(); + actor.reply("§aConnection to database etablished."); + final Database fdb = db; + Utils.runSync(() -> { + actor.reply("§aStarting migration..."); + try { + actor.reply(PlayersManagerDB.migrate(fdb, (PlayersManagerYAML) PlayersManager.manager)); + }catch (Exception ex) { + actor.error("An exception occured during migration. Process aborted. " + ex.getMessage()); + BeautyQuests.logger.severe("Error during data migration", ex); + } + }); + }catch (SQLException ex) { + actor.error("§cConnection to database has failed. Aborting. " + ex.getMessage()); + BeautyQuests.logger.severe("An error occurred while connecting to the database for datas migration.", ex); + } + }); + } + + @Subcommand ("setItem") + @CommandPermission ("beautyquests.command.setItem") + public void setItem(Player player, ItemHologram position) { + ItemStack item = player.getInventory().getItemInMainHand(); + if (item.getType() == Material.AIR) { + BeautyQuests.getInstance().getDataFile().set(position.name().toLowerCase() + "Item", null); + Lang.ITEM_REMOVED.send(player); + return; + } + BeautyQuests.getInstance().getDataFile().set(position.name().toLowerCase() + "Item", item.serialize()); + Lang.ITEM_CHANGED.send(player); + } + + @Subcommand ("setFirework") + @CommandPermission ("beautyquests.command.setItem") + public void setFirework(Player player, @Switch boolean remove) { + if (remove) { + BeautyQuests.getInstance().getDataFile().set("firework", "none"); + Lang.FIREWORK_REMOVED.send(player); + Lang.RESTART_SERVER.send(player); + }else { + ItemMeta meta = player.getInventory().getItemInMainHand().getItemMeta(); + if (meta instanceof FireworkMeta) { + BeautyQuests.getInstance().getDataFile().set("firework", meta); + Lang.FIREWORK_EDITED.send(player); + Lang.RESTART_SERVER.send(player); + }else { + Lang.FIREWORK_INVALID_HAND.send(player); + } + } + } + + @Subcommand ("testNPC") + @CommandPermission (value = "beautyquests.command.create") + @SecretCommand + public void testNPC(BukkitCommandActor actor, BQNPC npc) { + Utils.sendMessage(actor.getSender(), npc.toString()); + npc.toggleDebug(); + } + + public enum ItemHologram { + TALK, LAUNCH, NOLAUNCH; + } + +} diff --git a/core/src/main/java/fr/skytasul/quests/commands/CommandsManager.java b/core/src/main/java/fr/skytasul/quests/commands/CommandsManager.java index 45151087..fb70b22e 100644 --- a/core/src/main/java/fr/skytasul/quests/commands/CommandsManager.java +++ b/core/src/main/java/fr/skytasul/quests/commands/CommandsManager.java @@ -1,218 +1,144 @@ - package fr.skytasul.quests.commands; - -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.function.Consumer; -import java.util.stream.Collectors; - -import org.bukkit.Bukkit; -import org.bukkit.command.Command; -import org.bukkit.command.CommandExecutor; -import org.bukkit.command.CommandSender; -import org.bukkit.command.TabCompleter; -import org.bukkit.entity.Player; - -import fr.skytasul.quests.BeautyQuests; -import fr.skytasul.quests.api.QuestsAPI; -import fr.skytasul.quests.api.npcs.BQNPC; -import fr.skytasul.quests.editors.Editor; -import fr.skytasul.quests.gui.Inventories; -import fr.skytasul.quests.structure.Quest; -import fr.skytasul.quests.structure.pools.QuestPool; -import fr.skytasul.quests.utils.DebugUtils; -import fr.skytasul.quests.utils.Lang; -import fr.skytasul.quests.utils.Utils; - -public class CommandsManager implements CommandExecutor, TabCompleter{ - - public final Map commands = new HashMap<>(); - private Consumer noArgs; - - /** - * @param noArgs RunnableObj(player) who'll be ran if the command is executed without any arguments (can be null) - */ - public CommandsManager(Consumer noArgs){ - this.noArgs = noArgs; - } - - /** - * Register all available commands from an instance of a Class - * @param commandsClassInstance Instance of the Class - */ - public void registerCommandsClass(Object commandsClassInstance){ - for(Method method : commandsClassInstance.getClass().getDeclaredMethods()){ - if (method.isAnnotationPresent(Cmd.class)){ - Cmd cmd = method.getDeclaredAnnotation(Cmd.class); - if (method.getParameterCount() == 1){ - if (method.getParameterTypes()[0] == CommandContext.class){ - this.commands.put(method.getName().toLowerCase(), new InternalCommand(cmd, method, commandsClassInstance)); - continue; - } - } - DebugUtils.logMessage("Error when loading command annotated method " + method.getName() + " in class " + commandsClassInstance.getClass().getName() + ". Needed argument: fr.skytasul.quests.commands.CommandContext"); - } - } - } - - @Override - public boolean onCommand(CommandSender sender, Command command, String label, String[] args){ - String commandString = "/" + label + " " + Utils.buildFromArray(args, 0, " "); - DebugUtils.logMessage(sender.getName() + " issued server command: " + commandString); - - if (args.length == 0){ - if (noArgs != null){ - noArgs.accept(sender); - }else Lang.INCORRECT_SYNTAX.sendWP(sender); - return false; - } - - InternalCommand internal = commands.get(args[0].toLowerCase()); - if (internal == null){ - Lang.COMMAND_DOESNT_EXIST.sendWP(sender); - return false; - } - - Cmd cmd = internal.cmd; - if (cmd.player() && !(sender instanceof Player)){ - Lang.MUST_PLAYER.sendWP(sender); - return false; - } - - if (!cmd.permission().isEmpty() && !hasPermission(sender, cmd.permission(), true)) return false; - - if (args.length - 1 < cmd.min()){ - Lang.INCORRECT_SYNTAX.sendWP(sender); - return false; - } - - if (cmd.player() && cmd.noEditorInventory() && (Inventories.isInSystem((Player) sender) || Editor.hasEditor((Player) sender))){ - Lang.ALREADY_EDITOR.send(sender); - return true; - } - - Object[] argsCmd = new Object[args.length - 1]; - for (int i = 1; i < args.length; i++){ - /*if (i > cmd.args().length){ - Lang.INCORRECT_SYNTAX.sendWP(sender); - return false; - }*/ - String arg = args[i]; - String type = i > cmd.args().length ? "" : cmd.args()[i-1]; - if (type.equals("PLAYERS")){ - Player target = Bukkit.getPlayerExact(arg); - if (target == null){ - Lang.PLAYER_NOT_ONLINE.send(sender, arg); - return false; - } - argsCmd[i-1] = target; - }else if (type.equals("QUESTSID")){ - Integer id = Utils.parseInt(sender, arg); - if (id == null) return false; - Quest qu = QuestsAPI.getQuests().getQuest(id); - if (qu == null){ - Lang.QUEST_INVALID.send(sender, id); - return false; - } - argsCmd[i-1] = qu; - }else if (type.equals("POOLSID")) { - Integer id = Utils.parseInt(sender, arg); - if (id == null) return false; - QuestPool pool = QuestsAPI.getQuestPools().getPool(id); - if (pool == null) { - Lang.POOL_INVALID.send(sender, id); - return false; - } - argsCmd[i - 1] = pool; - }else if (type.equals("NPCSID")){ - Integer id = Utils.parseInt(sender, arg); - if (id == null) return false; - BQNPC npc = QuestsAPI.getNPCsManager().getById(id); - if (npc == null){ - Lang.NPC_DOESNT_EXIST.send(sender, id); - return false; - } - argsCmd[i-1] = npc; - }else { - argsCmd[i-1] = arg; - } - } - - try { - DebugUtils.logMessage(sender.getName() + " invoked method \"" + internal.method.getName() + "\" from command: " + commandString); - internal.method.invoke(internal.commands, new CommandContext(this, sender, argsCmd, label)); - }catch (Exception e) { - Lang.ERROR_OCCURED.send(sender, e.getCause() == null ? e.getClass().getSimpleName() : e.getCause().getClass().getSimpleName()); - BeautyQuests.logger.severe("An exception occured during command execution:", e); - } - - return false; - } - - @Override - public List onTabComplete(CommandSender sender, Command command, String label, String[] args){ - List tmp = new ArrayList<>(); - List find = new ArrayList<>(); - String sel = args[0]; - - if (args.length == 1){ - for (Entry en : commands.entrySet()){ // PERMISSIONS - if (!en.getValue().cmd.hide() && hasPermission(sender, en.getValue().cmd.permission(), false)) find.add(en.getKey()); - } - }else if (args.length >= 2){ - int index = args.length-2; - if (!commands.containsKey(sel)) return tmp; - InternalCommand internal = commands.get(sel); - String[] needed = internal.cmd.args(); - if (needed.length <= index) return tmp; - if (!hasPermission(sender, internal.cmd.permission(), false)) return tmp; - sel = args[index + 1]; - String key = needed[index]; - if (key.equals("QUESTSID")){ - for (Quest quest : QuestsAPI.getQuests()) find.add(Integer.toString(quest.getID())); - }else if (key.equals("POOLSID")) { - for (QuestPool pool : QuestsAPI.getQuestPools().getPools()) find.add(Integer.toString(pool.getID())); - }else if (key.equals("PLAYERS")){ - return null; - }else if (key.equals("NPCSID")){ - find.addAll(QuestsAPI.getNPCsManager().getIDs().stream().map(String::valueOf).collect(Collectors.toList())); - }else if (key.equals("BOOLEAN")) { - find.add("false"); - find.add("true"); - }else { - find.addAll(Arrays.asList(key.split("\\|"))); - } - }else return tmp; - - for (String arg : find){ - if (arg.startsWith(sel)) tmp.add(arg); - } - return tmp; - } - - public static boolean hasPermission(CommandSender sender, String cmd, boolean message) { - if (cmd == null || cmd.isEmpty()) return true; - if (!sender.hasPermission(("beautyquests.command." + cmd))) { - if (message) Lang.PERMISSION_REQUIRED.sendWP(sender, "beautyquests.command." + cmd); - return false; - } - return true; - } - - class InternalCommand{ - Cmd cmd; - Method method; - Object commands; - - InternalCommand(Cmd cmd, Method method, Object commandsClass){ - this.cmd = cmd; - this.method = method; - this.commands = commandsClass; - } - } - -} \ No newline at end of file +package fr.skytasul.quests.commands; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Unmodifiable; + +import fr.skytasul.quests.BeautyQuests; +import fr.skytasul.quests.QuestsConfiguration; +import fr.skytasul.quests.api.QuestsAPI; +import fr.skytasul.quests.api.npcs.BQNPC; +import fr.skytasul.quests.editors.Editor; +import fr.skytasul.quests.gui.Inventories; +import fr.skytasul.quests.scoreboards.Scoreboard; +import fr.skytasul.quests.structure.Quest; +import fr.skytasul.quests.structure.pools.QuestPool; +import fr.skytasul.quests.utils.DebugUtils; +import fr.skytasul.quests.utils.Lang; + +import revxrsal.commands.autocomplete.SuggestionProvider; +import revxrsal.commands.bukkit.BukkitCommandActor; +import revxrsal.commands.bukkit.BukkitCommandHandler; +import revxrsal.commands.command.CommandActor; +import revxrsal.commands.command.ExecutableCommand; +import revxrsal.commands.exception.CommandErrorException; +import revxrsal.commands.orphan.OrphanCommand; +import revxrsal.commands.orphan.Orphans; + +public class CommandsManager { + + private static String[] COMMAND_ALIASES = { "quests", "quest", "bq", "beautyquests", "bquests" }; + + private BukkitCommandHandler handler; + private boolean locked = false; + + public CommandsManager() { + handler = BukkitCommandHandler.create(BeautyQuests.getInstance()); + handler.setMessagePrefix(QuestsConfiguration.getPrefix()); + handler.failOnTooManyArguments(); + + handler.registerValueResolver(Quest.class, context -> { + int id = context.popInt(); + Quest quest = QuestsAPI.getQuests().getQuest(id); + if (quest == null) + throw new CommandErrorException(Lang.QUEST_INVALID.format(id)); + return quest; + }); + handler.getAutoCompleter().registerParameterSuggestions(Quest.class, + SuggestionProvider.of(() -> QuestsAPI.getQuests().getQuests() + .stream() + .map(quest -> Integer.toString(quest.getID())) + .collect(Collectors.toList()))); + + handler.registerValueResolver(QuestPool.class, context -> { + int id = context.popInt(); + QuestPool pool = QuestsAPI.getQuestPools().getPool(id); + if (pool == null) + throw new CommandErrorException(Lang.POOL_INVALID.format(id)); + return pool; + }); + handler.getAutoCompleter().registerParameterSuggestions(QuestPool.class, + SuggestionProvider.of(() -> QuestsAPI.getQuestPools().getPools() + .stream() + .map(pool -> Integer.toString(pool.getID())) + .collect(Collectors.toList()))); + + handler.registerValueResolver(BQNPC.class, context -> { + int id = context.popInt(); + BQNPC npc = QuestsAPI.getNPCsManager().getById(id); + if (npc == null) + throw new CommandErrorException(Lang.NPC_DOESNT_EXIST.format(id)); + return npc; + }); + handler.getAutoCompleter().registerParameterSuggestions(BQNPC.class, + SuggestionProvider.of(() -> QuestsAPI.getNPCsManager().getIDs() + .stream() + .map(String::valueOf) + .collect(Collectors.toList()))); + + handler.registerCondition((@NotNull CommandActor actor, @NotNull ExecutableCommand command, @NotNull @Unmodifiable List arguments) -> { + if (command.hasAnnotation(OutsideEditor.class)) { + BukkitCommandActor bukkitActor = (BukkitCommandActor) actor; + if (bukkitActor.isPlayer() && (Inventories.isInSystem(bukkitActor.getAsPlayer()) || Editor.hasEditor(bukkitActor.getAsPlayer()))) + throw new CommandErrorException(Lang.ALREADY_EDITOR.toString()); + } + }); + + handler.setHelpWriter((command, actor) -> { + if (!command.hasPermission(actor)) return null; + for (Lang lang : Lang.values()) { + if (lang.getPath().startsWith("msg.command.help.")) { + String cmdKey = lang.getPath().substring(17); + if (cmdKey.equalsIgnoreCase(command.getName())) return lang.format(command.getPath().get(0)); + } + } + return null; + }); + + handler.registerContextResolver(Scoreboard.class, context -> { + return BeautyQuests.getInstance().getScoreboardManager().getPlayerScoreboard(context.getResolvedArgument(Player.class)); + }); + + handler.registerCondition((actor, command, arguments) -> { + DebugUtils.logMessage(actor.getName() + " executed command: " + command.getPath().toRealString() + " " + String.join(" ", arguments)); + }); + } + + public BukkitCommandHandler getHandler() { + return handler; + } + + public void initializeCommands() { + handler.register(new CommandsRoot()); + + registerCommands("", new CommandsAdmin(), new CommandsPlayer(), new CommandsPlayerManagement()); + registerCommands("scoreboard", new CommandsScoreboard()); + } + + public void registerCommands(String subpath, OrphanCommand... commands) { + Orphans path; + if (subpath == null || subpath.isEmpty()) { + path = Orphans.path(COMMAND_ALIASES); + }else { + path = Orphans.path(Arrays.stream(COMMAND_ALIASES).map(x -> x + " " + subpath).toArray(String[]::new)); + } + handler.register(Arrays.stream(commands).map(path::handler).toArray()); + if (locked) BeautyQuests.logger.warning("Registered commands after final locking."); + } + + public void lockCommands() { + if (locked) return; + locked = true; + handler.registerBrigadier(); + if (handler.isBrigadierSupported()) DebugUtils.logMessage("Brigadier supported!"); + } + + public void unload() { + handler.unregisterAllCommands(); + } + +} diff --git a/core/src/main/java/fr/skytasul/quests/commands/CommandsPlayer.java b/core/src/main/java/fr/skytasul/quests/commands/CommandsPlayer.java new file mode 100644 index 00000000..fcbca2e4 --- /dev/null +++ b/core/src/main/java/fr/skytasul/quests/commands/CommandsPlayer.java @@ -0,0 +1,59 @@ +package fr.skytasul.quests.commands; + +import java.util.Optional; + +import org.bukkit.entity.Player; + +import fr.skytasul.quests.BeautyQuests; +import fr.skytasul.quests.api.stages.AbstractStage; +import fr.skytasul.quests.gui.quests.PlayerListGUI; +import fr.skytasul.quests.players.PlayerAccount; +import fr.skytasul.quests.players.PlayerQuestDatas; +import fr.skytasul.quests.players.PlayersManager; +import fr.skytasul.quests.rewards.CheckpointReward; +import fr.skytasul.quests.structure.Quest; +import fr.skytasul.quests.structure.QuestBranch; +import fr.skytasul.quests.utils.Lang; + +import revxrsal.commands.annotation.Default; +import revxrsal.commands.annotation.Subcommand; +import revxrsal.commands.bukkit.BukkitCommandActor; +import revxrsal.commands.bukkit.annotation.CommandPermission; +import revxrsal.commands.command.ExecutableCommand; +import revxrsal.commands.exception.CommandErrorException; +import revxrsal.commands.orphan.OrphanCommand; + +public class CommandsPlayer implements OrphanCommand { + + @Default + @CommandPermission ("beautyquests.command.listPlayer") + public void menu(BukkitCommandActor actor, ExecutableCommand command, @revxrsal.commands.annotation.Optional String subcommand) { + if (subcommand != null) throw new revxrsal.commands.exception.InvalidSubcommandException(command.getPath(), subcommand); + PlayerAccount acc = PlayersManager.getPlayerAccount(actor.requirePlayer()); + if (acc == null) { + BeautyQuests.logger.severe("Player " + actor.getName() + " has got no account. This is a CRITICAL issue."); + throw new CommandErrorException("no player datas"); + }else new PlayerListGUI(acc).create(actor.getAsPlayer()); + } + + @Subcommand ("checkpoint") + @CommandPermission ("beautyquests.command.checkpoint") + public void checkpoint(Player player, Quest quest) { + PlayerAccount account = PlayersManager.getPlayerAccount(player); + if (account.hasQuestDatas(quest)) { + PlayerQuestDatas datas = account.getQuestDatas(quest); + QuestBranch branch = quest.getBranchesManager().getBranch(datas.getBranch()); + int max = datas.isInEndingStages() ? branch.getStageSize() : datas.getStage(); + for (int id = max - 1; id >= 0; id--) { + AbstractStage stage = branch.getRegularStage(id); + Optional optionalCheckpoint = stage.getRewards().stream().filter(CheckpointReward.class::isInstance).findAny().map(CheckpointReward.class::cast); + if (optionalCheckpoint.isPresent()) { + optionalCheckpoint.get().applies(player); + return; + } + } + Lang.COMMAND_CHECKPOINT_NO.send(player, quest.getName()); + }else Lang.COMMAND_CHECKPOINT_NOT_STARTED.send(player); + } + +} diff --git a/core/src/main/java/fr/skytasul/quests/commands/CommandsPlayerManagement.java b/core/src/main/java/fr/skytasul/quests/commands/CommandsPlayerManagement.java new file mode 100644 index 00000000..567bd920 --- /dev/null +++ b/core/src/main/java/fr/skytasul/quests/commands/CommandsPlayerManagement.java @@ -0,0 +1,316 @@ +package fr.skytasul.quests.commands; + +import java.util.ArrayList; +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.bukkit.permissions.Permission; +import fr.skytasul.quests.BeautyQuests; +import fr.skytasul.quests.api.QuestsAPI; +import fr.skytasul.quests.api.events.accounts.PlayerAccountResetEvent; +import fr.skytasul.quests.api.stages.AbstractStage; +import fr.skytasul.quests.api.stages.types.Dialogable; +import fr.skytasul.quests.gui.quests.PlayerListGUI; +import fr.skytasul.quests.gui.quests.QuestsListGUI; +import fr.skytasul.quests.options.OptionStartDialog; +import fr.skytasul.quests.players.PlayerAccount; +import fr.skytasul.quests.players.PlayerPoolDatas; +import fr.skytasul.quests.players.PlayerQuestDatas; +import fr.skytasul.quests.players.PlayersManager; +import fr.skytasul.quests.structure.BranchesManager; +import fr.skytasul.quests.structure.Quest; +import fr.skytasul.quests.structure.QuestBranch; +import fr.skytasul.quests.structure.pools.QuestPool; +import fr.skytasul.quests.utils.Lang; +import fr.skytasul.quests.utils.types.DialogRunner; +import fr.skytasul.quests.utils.types.DialogRunner.DialogNextReason; +import revxrsal.commands.annotation.Optional; +import revxrsal.commands.annotation.Range; +import revxrsal.commands.annotation.Subcommand; +import revxrsal.commands.annotation.Switch; +import revxrsal.commands.bukkit.BukkitCommandActor; +import revxrsal.commands.bukkit.BukkitCommandPermission; +import revxrsal.commands.bukkit.EntitySelector; +import revxrsal.commands.bukkit.annotation.CommandPermission; +import revxrsal.commands.command.ExecutableCommand; +import revxrsal.commands.exception.CommandErrorException; +import revxrsal.commands.exception.NoPermissionException; +import revxrsal.commands.orphan.OrphanCommand; + +public class CommandsPlayerManagement implements OrphanCommand { + + private BukkitCommandPermission startOtherPermission = new BukkitCommandPermission(new Permission("beautyquests.command.start.other")); + private BukkitCommandPermission cancelOtherPermission = new BukkitCommandPermission(new Permission("beautyquests.command.cancel.other")); + + @Subcommand ("finishAll") + @CommandPermission ("beautyquests.command.finish") + public void finishAll(BukkitCommandActor actor, EntitySelector players) { + for (Player player : players) { + PlayerAccount acc = PlayersManager.getPlayerAccount(player); + int success = 0; + int errors = 0; + for (Quest q : QuestsAPI.getQuests().getQuestsStarted(acc)) { + try { + q.finish(player); + success++; + }catch (Exception ex) { + BeautyQuests.logger.severe("An error occurred while finishing quest " + q.getID(), ex); + errors++; + } + } + Lang.LEAVE_ALL_RESULT.send(actor.getSender(), success, errors); + } + } + + @Subcommand ("finish") + @CommandPermission ("beautyquests.command.finish") + public void finish(BukkitCommandActor actor, EntitySelector players, Quest quest, @Switch boolean force) { + for (Player player : players) { + try { + if (force || quest.hasStarted(PlayersManager.getPlayerAccount(player))) { + quest.finish(player); + Lang.LEAVE_ALL_RESULT.send(actor.getSender(), 1, 0); + } + }catch (Exception ex) { + BeautyQuests.logger.severe("An error occurred while finishing quest " + quest.getID(), ex); + Lang.LEAVE_ALL_RESULT.send(actor.getSender(), 1, 1); + } + } + } + + @Subcommand ("setStage") + @CommandPermission ("beautyquests.command.setStage") + public void setStage( + BukkitCommandActor actor, + Player player, + Quest quest, + @Range (min = 0, max = 14) @Optional Integer branchID, + @Range (min = 0, max = 14) @Optional Integer stageID) { + PlayerAccount acc = PlayersManager.getPlayerAccount(player); + BranchesManager manager = quest.getBranchesManager(); // syntax: no arg: next or start | 1 arg: start branch | 2 args: set branch stage + + PlayerQuestDatas datas = acc.getQuestDatasIfPresent(quest); + if (branchID == null && (datas == null || !datas.hasStarted())) { // start quest + quest.start(player); + Lang.START_QUEST.send(actor.getSender(), quest.getName(), acc.debugName()); + return; + } + if (datas == null) datas = acc.getQuestDatas(quest); // creates quest datas + + QuestBranch currentBranch = manager.getBranch(datas.getBranch()); + + if (branchID == null) { // next + if (!datas.isInEndingStages()) { + currentBranch.finishStage(player, currentBranch.getRegularStage(datas.getStage())); + Lang.COMMAND_SETSTAGE_NEXT.send(actor.getSender()); + }else Lang.COMMAND_SETSTAGE_NEXT_UNAVAILABLE.send(actor.getSender()); + }else { + QuestBranch branch = manager.getBranch(branchID); + if (branch == null) + throw new CommandErrorException(Lang.COMMAND_SETSTAGE_BRANCH_DOESNTEXIST.format(branchID)); + + if (stageID != null) { + if (currentBranch == null) + throw new CommandErrorException(Lang.ERROR_OCCURED.format("player " + acc.debugName() + " has not started quest")); + if (branch.getRegularStages().size() <= stageID) + throw new CommandErrorException(Lang.COMMAND_SETSTAGE_STAGE_DOESNTEXIST.format(stageID)); + } + Lang.COMMAND_SETSTAGE_SET.send(actor.getSender(), stageID); + if (currentBranch != null) { + if (datas.isInEndingStages()) { + for (AbstractStage stage : currentBranch.getEndingStages().keySet()) stage.end(acc); + }else { + currentBranch.getRegularStage(datas.getStage()).end(acc); + } + } + if (stageID == null) { // start branch + branch.start(acc); + }else { // set stage in branch + datas.setBranch(branchID); + branch.setStage(acc, stageID); + } + QuestsAPI.propagateQuestsHandlers(handler -> handler.questUpdated(acc, player, quest)); + } + } + + @Subcommand ("startDialog") + @CommandPermission ("beautyquests.command.setStage") + public void startDialog(BukkitCommandActor actor, Player player, Quest quest) { + PlayerAccount acc = PlayersManager.getPlayerAccount(player); + PlayerQuestDatas datas = acc.getQuestDatasIfPresent(quest); + + DialogRunner runner = null; + if (datas == null || !quest.hasStarted(acc)) { + if (quest.hasOption(OptionStartDialog.class)) { + runner = quest.getOption(OptionStartDialog.class).getDialogRunner(); + } + }else { + if (datas.isInEndingStages() || datas.isInQuestEnd()) { + Lang.COMMAND_STARTDIALOG_IMPOSSIBLE.send(actor.getSender()); + return; + }else { + AbstractStage stage = quest.getBranchesManager().getBranch(datas.getBranch()).getRegularStage(datas.getStage()); + if (stage instanceof Dialogable) { + runner = ((Dialogable) stage).getDialogRunner(); + } + } + } + + if (runner == null) { + Lang.COMMAND_STARTDIALOG_NO.send(actor.getSender()); + }else { + if (runner.isPlayerInDialog(player)) { + Lang.COMMAND_STARTDIALOG_ALREADY.send(actor.getSender()); + }else { + runner.handleNext(player, DialogNextReason.COMMAND); + Lang.COMMAND_STARTDIALOG_SUCCESS.send(actor.getSender(), player.getName(), quest.getID()); + } + } + } + + @Subcommand ("resetPlayer") + @CommandPermission ("beautyquests.command.resetPlayer") + public void resetPlayer(BukkitCommandActor actor, EntitySelector players) { + for (Player player : players) { + PlayerAccount acc = PlayersManager.getPlayerAccount(player); + int quests = 0, pools = 0; + for (PlayerQuestDatas questDatas : new ArrayList<>(acc.getQuestsDatas())) { + Quest quest = questDatas.getQuest(); + if (quest != null) { + quest.resetPlayer(acc); + }else acc.removeQuestDatas(questDatas.getQuestID()); + quests++; + } + for (PlayerPoolDatas poolDatas : new ArrayList<>(acc.getPoolDatas())) { + QuestPool pool = poolDatas.getPool(); + if (pool != null) { + pool.resetPlayer(acc); + }else acc.removePoolDatas(poolDatas.getPoolID()); + pools++; + } + acc.resetDatas(); + Bukkit.getPluginManager().callEvent(new PlayerAccountResetEvent(player, acc)); + if (acc.isCurrent()) Lang.DATA_REMOVED.send(player, quests, actor.getName(), pools); + Lang.DATA_REMOVED_INFO.send(actor.getSender(), quests, player.getName(), pools); + } + } + + @Subcommand ("resetPlayerQuest") + @CommandPermission ("beautyquests.command.resetPlayer") + public void resetPlayerQuest(BukkitCommandActor actor, Player player, @Optional Quest quest) { + PlayerAccount acc = PlayersManager.getPlayerAccount(player); + if (quest != null) { + reset(actor.getSender(), player, acc, quest); + }else { + new QuestsListGUI(obj -> { + reset(actor.getSender(), player, acc, obj); + }, acc, true, false, true).create(actor.requirePlayer()); + } + } + + private void reset(CommandSender sender, Player target, PlayerAccount acc, Quest qu) { + qu.resetPlayer(acc); + if (acc.isCurrent()) Lang.DATA_QUEST_REMOVED.send(target, qu.getName(), sender.getName()); + Lang.DATA_QUEST_REMOVED_INFO.send(sender, target.getName(), qu.getName()); + } + + @Subcommand ("resetPlayerPool") + @CommandPermission ("beautyquests.command.resetPlayer") + public void resetPlayerPool(BukkitCommandActor actor, Player player, QuestPool pool, @Switch boolean timer) { + PlayerAccount acc = PlayersManager.getPlayerAccount(player); + if (timer) { + pool.resetPlayerTimer(acc); + Lang.POOL_RESET_TIMER.send(actor.getSender(), pool.getID(), player.getName()); + }else { + pool.resetPlayer(acc); + Lang.POOL_RESET_FULL.send(actor.getSender(), pool.getID(), player.getName()); + } + } + + @Subcommand ("resetQuest") + @CommandPermission ("beautyquests.command.resetQuest") + public void resetQuest(BukkitCommandActor actor, Quest quest) { + int amount = 0; + for (Player p : Bukkit.getOnlinePlayers()) { + if (quest.resetPlayer(PlayersManager.getPlayerAccount(p))) amount++; + } + amount += PlayersManager.manager.removeQuestDatas(quest); + Lang.QUEST_PLAYERS_REMOVED.send(actor.getSender(), amount); + } + + @Subcommand ("seePlayer") + @CommandPermission ("beautyquests.command.seePlayer") + public void seePlayer(Player actor, Player player) { + new PlayerListGUI(PlayersManager.getPlayerAccount(player), false).create(actor); + } + + @Subcommand ("start") + @CommandPermission ("beautyquests.command.start") + public void start(BukkitCommandActor actor, ExecutableCommand command, EntitySelector players, @Optional Quest quest, @CommandPermission ("beautyquests.command.start.other") @Switch boolean overrideRequirements) { + if (actor.isPlayer() && !startOtherPermission.canExecute(actor)) { + if (players.isEmpty() || players.size() > 1 || (players.get(0) != actor.getAsPlayer())) + throw new NoPermissionException(command, startOtherPermission); + } + + for (Player player : players) { + PlayerAccount acc = PlayersManager.getPlayerAccount(player); + + if (quest == null) { + new QuestsListGUI(obj -> { + start(actor.getSender(), player, acc, obj, overrideRequirements); + }, acc, false, true, false).create(actor.requirePlayer()); + }else { + start(actor.getSender(), player, acc, quest, overrideRequirements); + } + } + } + + private void start(CommandSender sender, Player player, PlayerAccount acc, Quest quest, boolean overrideRequirements) { + if (!overrideRequirements && !(quest.isLauncheable(player, acc, true) && quest.testTimer(acc, true))) { + Lang.START_QUEST_NO_REQUIREMENT.send(sender, quest.getName()); + return; + } + quest.start(player); + Lang.START_QUEST.send(sender, quest.getName(), acc.abstractAcc.getIdentifier()); + } + + @Subcommand ("cancel") + @CommandPermission ("beautyquests.command.cancel") + public void cancel(BukkitCommandActor actor, ExecutableCommand command, EntitySelector players, @Optional Quest quest) { + if (actor.isPlayer() && !cancelOtherPermission.canExecute(actor)) { + if (players.isEmpty() || players.size() > 1 || (players.get(0) != actor.getAsPlayer())) + throw new NoPermissionException(command, cancelOtherPermission); + } + + for (Player player : players) { + PlayerAccount acc = PlayersManager.getPlayerAccount(player); + + if (quest == null) { + new QuestsListGUI(obj -> { + cancel(actor.getSender(), acc, obj); + }, acc, true, false, false).create(actor.requirePlayer()); + }else { + cancel(actor.getSender(), acc, quest); + } + } + } + + private void cancel(CommandSender sender, PlayerAccount acc, Quest quest) { + if (!quest.isCancellable()) { + Lang.CANCEL_QUEST_UNAVAILABLE.send(sender, quest.getName()); + return; + } + + if (quest.cancelPlayer(acc)) { + Lang.CANCEL_QUEST.send(sender, quest.getName()); + } else { + if (sender.equals(acc.getPlayer())) { + Lang.QUEST_NOT_STARTED.send(sender); + } else { + Lang.ERROR_OCCURED.send(sender, + "Player " + acc.getName() + " does not have the quest " + quest.getID() + " started."); + } + } + } + +} diff --git a/core/src/main/java/fr/skytasul/quests/commands/CommandsRoot.java b/core/src/main/java/fr/skytasul/quests/commands/CommandsRoot.java new file mode 100644 index 00000000..554586f2 --- /dev/null +++ b/core/src/main/java/fr/skytasul/quests/commands/CommandsRoot.java @@ -0,0 +1,31 @@ +package fr.skytasul.quests.commands; + +import fr.skytasul.quests.BeautyQuests; +import fr.skytasul.quests.utils.Lang; +import fr.skytasul.quests.utils.Utils; + +import revxrsal.commands.annotation.Command; +import revxrsal.commands.annotation.Description; +import revxrsal.commands.annotation.Subcommand; +import revxrsal.commands.bukkit.BukkitCommandActor; +import revxrsal.commands.bukkit.annotation.CommandPermission; +import revxrsal.commands.help.CommandHelp; + +@Command ({ "quests", "quest", "bq", "beautyquests", "bquests" }) +@Description ("Main command for quests") +@CommandPermission ("beautyquests.command") +public class CommandsRoot { + + @Subcommand ("help") + public void help(BukkitCommandActor actor, CommandHelp helpEntries) { + Lang.COMMAND_HELP.sendWP(actor.getSender()); + helpEntries.forEach(help -> Utils.sendMessageWP(actor.getSender(), help)); + } + + @Subcommand ("version") + @CommandPermission ("beautyquests.command.version") + public void version(BukkitCommandActor actor) { + actor.reply("§eBeautyQuests version : §6§l" + BeautyQuests.getInstance().getDescription().getVersion()); + } + +} diff --git a/core/src/main/java/fr/skytasul/quests/commands/CommandsScoreboard.java b/core/src/main/java/fr/skytasul/quests/commands/CommandsScoreboard.java new file mode 100644 index 00000000..06e03afc --- /dev/null +++ b/core/src/main/java/fr/skytasul/quests/commands/CommandsScoreboard.java @@ -0,0 +1,78 @@ +package fr.skytasul.quests.commands; + +import org.bukkit.entity.Player; + +import fr.skytasul.quests.BeautyQuests; +import fr.skytasul.quests.scoreboards.Scoreboard; +import fr.skytasul.quests.utils.Lang; + +import revxrsal.commands.annotation.Default; +import revxrsal.commands.annotation.Optional; +import revxrsal.commands.annotation.Range; +import revxrsal.commands.annotation.Subcommand; +import revxrsal.commands.bukkit.BukkitCommandActor; +import revxrsal.commands.bukkit.annotation.CommandPermission; +import revxrsal.commands.command.ExecutableCommand; +import revxrsal.commands.orphan.OrphanCommand; + +public class CommandsScoreboard implements OrphanCommand { + + @Default + @CommandPermission ("beautyquests.command.scoreboard.toggle") + public void scoreboardToggle(Player player, ExecutableCommand command, Scoreboard scoreboard, @Optional String subcommand) { + if (subcommand != null) throw new revxrsal.commands.exception.InvalidSubcommandException(command.getPath(), subcommand); + if (scoreboard.isForceHidden()) { + scoreboard.show(true); + Lang.COMMAND_SCOREBOARD_OWN_SHOWN.send(player); + }else { + scoreboard.hide(true); + Lang.COMMAND_SCOREBOARD_OWN_HIDDEN.send(player); + } + } + + @Subcommand ("setline") + @CommandPermission ("beautyquests.command.scoreboard") + public void setline(BukkitCommandActor actor, Player player, Scoreboard scoreboard, @Range (min = 0) int line, String text) { + scoreboard.setCustomLine(line, text); + Lang.COMMAND_SCOREBOARD_LINESET.send(actor.getSender(), line); + } + + @Subcommand ("removeline") + @CommandPermission ("beautyquests.command.scoreboard") + public void removeline(BukkitCommandActor actor, Player player, Scoreboard scoreboard, @Range (min = 0) int line) { + if (scoreboard.removeLine(line)) { + Lang.COMMAND_SCOREBOARD_LINEREMOVE.send(actor.getSender(), line); + }else Lang.COMMAND_SCOREBOARD_LINENOEXIST.send(actor.getSender(), line); + } + + @Subcommand ("resetline") + @CommandPermission ("beautyquests.command.scoreboard") + public void resetline(BukkitCommandActor actor, Player player, Scoreboard scoreboard, @Range (min = 0) int line) { + if (scoreboard.resetLine(line)) { + Lang.COMMAND_SCOREBOARD_LINERESET.send(actor.getSender(), line); + }else Lang.COMMAND_SCOREBOARD_LINENOEXIST.send(actor.getSender(), line); + } + + @Subcommand ("resetall") + @CommandPermission ("beautyquests.command.scoreboard") + public void resetall(BukkitCommandActor actor, Player player) { + BeautyQuests.getInstance().getScoreboardManager().removePlayerScoreboard(player); + BeautyQuests.getInstance().getScoreboardManager().create(player); + Lang.COMMAND_SCOREBOARD_RESETALL.send(actor.getSender(), player.getName()); + } + + @Subcommand ("show") + @CommandPermission ("beautyquests.command.scoreboard") + public void show(BukkitCommandActor actor, Player player, Scoreboard scoreboard) { + scoreboard.show(true); + Lang.COMMAND_SCOREBOARD_SHOWN.send(actor.getSender(), player.getName()); + } + + @Subcommand ("hide") + @CommandPermission ("beautyquests.command.scoreboard") + public void hide(BukkitCommandActor actor, Player player, Scoreboard scoreboard) { + scoreboard.hide(true); + Lang.COMMAND_SCOREBOARD_HIDDEN.send(actor.getSender(), player.getName()); + } + +} \ No newline at end of file diff --git a/core/src/main/java/fr/skytasul/quests/commands/OutsideEditor.java b/core/src/main/java/fr/skytasul/quests/commands/OutsideEditor.java new file mode 100644 index 00000000..d8e8f7fc --- /dev/null +++ b/core/src/main/java/fr/skytasul/quests/commands/OutsideEditor.java @@ -0,0 +1,11 @@ +package fr.skytasul.quests.commands; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Retention (RUNTIME) +@Target (METHOD) +public @interface OutsideEditor {} diff --git a/core/src/main/java/fr/skytasul/quests/editors/DialogEditor.java b/core/src/main/java/fr/skytasul/quests/editors/DialogEditor.java index 220e772a..b1a6427b 100644 --- a/core/src/main/java/fr/skytasul/quests/editors/DialogEditor.java +++ b/core/src/main/java/fr/skytasul/quests/editors/DialogEditor.java @@ -67,7 +67,7 @@ public boolean chat(String coloredMessage, String strippedMessage){ case LIST: for (int i = 0; i < d.messages.size(); i++) { Message dmsg = d.messages.get(i); - Utils.IsendMessage(p, "§6{0}: §7 \"{1}§7\"§e by §l{2}", false, i, dmsg.text, dmsg.sender.name().toLowerCase()); + Utils.IsendMessage(p, "§6{0}: §7\"{1}§7\"§e by §l{2}", false, i, dmsg.text, dmsg.sender.name().toLowerCase()); } break; @@ -95,15 +95,13 @@ public boolean chat(String coloredMessage, String strippedMessage){ } try{ Message message = d.messages.get(Integer.parseInt(args[1])); - if (message == null) { - Lang.OUT_OF_BOUNDS.send(p, args[1], 0, d.messages.size()); - }else { - msg = Utils.buildFromArray(argsColored, 2, " "); - message.text = msg; - Lang.DIALOG_MSG_EDITED.send(p, msg); - } + msg = Utils.buildFromArray(argsColored, 2, " "); + message.text = msg; + Lang.DIALOG_MSG_EDITED.send(p, msg); }catch (IllegalArgumentException ex){ Utils.sendMessage(p, Lang.NUMBER_INVALID.toString()); + }catch (IndexOutOfBoundsException ex) { + Lang.OBJECT_DOESNT_EXIST.send(p, args[1]); } break; @@ -114,13 +112,11 @@ public boolean chat(String coloredMessage, String strippedMessage){ } try{ Message imsg = d.messages.get(Integer.parseInt(args[1])); - if (imsg == null){ - Lang.OBJECT_DOESNT_EXIST.send(p, args[1]); - break; - } Lang.DIALOG_SOUND_ADDED.send(p, imsg.sound = args[2], args[1]); }catch (IllegalArgumentException ex){ Utils.sendMessage(p, Lang.NUMBER_INVALID.toString()); + }catch (IndexOutOfBoundsException ex) { + Lang.OBJECT_DOESNT_EXIST.send(p, args[1]); } break; @@ -131,10 +127,6 @@ public boolean chat(String coloredMessage, String strippedMessage){ } try { Message imsg = d.messages.get(Integer.parseInt(args[1])); - if (imsg == null) { - Lang.OBJECT_DOESNT_EXIST.send(p, args[1]); - break; - } int time = Integer.parseInt(args[2]); if (time < 0) { imsg.wait = -1; @@ -145,6 +137,8 @@ public boolean chat(String coloredMessage, String strippedMessage){ } }catch (IllegalArgumentException ex) { Utils.sendMessage(p, Lang.NUMBER_INVALID.toString()); + }catch (IndexOutOfBoundsException ex) { + Lang.OBJECT_DOESNT_EXIST.send(p, args[1]); } break; diff --git a/core/src/main/java/fr/skytasul/quests/editors/TextEditor.java b/core/src/main/java/fr/skytasul/quests/editors/TextEditor.java index 22304197..62c3d015 100644 --- a/core/src/main/java/fr/skytasul/quests/editors/TextEditor.java +++ b/core/src/main/java/fr/skytasul/quests/editors/TextEditor.java @@ -89,4 +89,10 @@ public boolean chat(String msg, String strippedMessage){ return false; } + @Override + protected void begin() { + super.begin(); + if (parser != null) parser.sendIndication(p); + } + } diff --git a/core/src/main/java/fr/skytasul/quests/editors/TextListEditor.java b/core/src/main/java/fr/skytasul/quests/editors/TextListEditor.java index 75ee65ff..a6f24252 100644 --- a/core/src/main/java/fr/skytasul/quests/editors/TextListEditor.java +++ b/core/src/main/java/fr/skytasul/quests/editors/TextListEditor.java @@ -1,9 +1,9 @@ package fr.skytasul.quests.editors; import java.util.List; +import java.util.StringJoiner; import java.util.function.Consumer; import java.util.function.Predicate; -import java.util.stream.Collectors; import org.apache.commons.lang.Validate; import org.bukkit.entity.Player; @@ -74,10 +74,11 @@ public boolean chat(String coloredMessage, String strippedMessage){ break; case LIST: - p.sendMessage(texts - .stream() - .map(text -> "§7- §r" + text) - .collect(Collectors.joining("\n", "§6§lList:\n", ""))); + StringJoiner joiner = new StringJoiner("\n", "§6§lList:\n", ""); + for (int i = 0; i < texts.size(); i++) { + joiner.add("§6" + i + ": §r" + texts.get(i)); + } + p.sendMessage(joiner.toString()); break; case HELP: diff --git a/core/src/main/java/fr/skytasul/quests/editors/checkers/AbstractParser.java b/core/src/main/java/fr/skytasul/quests/editors/checkers/AbstractParser.java index b03c392b..68e4b172 100644 --- a/core/src/main/java/fr/skytasul/quests/editors/checkers/AbstractParser.java +++ b/core/src/main/java/fr/skytasul/quests/editors/checkers/AbstractParser.java @@ -6,4 +6,6 @@ public abstract interface AbstractParser { public abstract T parse(Player p, String msg) throws Throwable; + public default void sendIndication(Player p) {} + } diff --git a/core/src/main/java/fr/skytasul/quests/editors/checkers/CollectionParser.java b/core/src/main/java/fr/skytasul/quests/editors/checkers/CollectionParser.java new file mode 100644 index 00000000..dd280557 --- /dev/null +++ b/core/src/main/java/fr/skytasul/quests/editors/checkers/CollectionParser.java @@ -0,0 +1,42 @@ +package fr.skytasul.quests.editors.checkers; + +import java.util.Collection; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.bukkit.entity.Player; + +import fr.skytasul.quests.utils.Lang; + +public class CollectionParser implements AbstractParser { + + protected Map names; + protected String namesString; + + public CollectionParser(Collection collection, Function namer) { + names = collection.stream().collect(Collectors.toMap(namer, Function.identity())); + namesString = String.join(", ", names.keySet()); + } + + @Override + public T parse(Player p, String msg) throws Throwable { + T obj = names.get(processName(msg)); + if (obj == null) Lang.NO_SUCH_ELEMENT.send(p, namesString); + return obj; + } + + protected String processName(String msg) { + return msg; + } + + @Override + public void sendIndication(Player p) { + Lang.AVAILABLE_ELEMENTS.send(p, namesString); + } + + public String getNames() { + return namesString; + } + +} \ No newline at end of file diff --git a/core/src/main/java/fr/skytasul/quests/editors/checkers/ColorParser.java b/core/src/main/java/fr/skytasul/quests/editors/checkers/ColorParser.java new file mode 100644 index 00000000..18c877c0 --- /dev/null +++ b/core/src/main/java/fr/skytasul/quests/editors/checkers/ColorParser.java @@ -0,0 +1,53 @@ +package fr.skytasul.quests.editors.checkers; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.bukkit.ChatColor; +import org.bukkit.Color; +import org.bukkit.entity.Player; + +import fr.skytasul.quests.utils.Lang; + +public class ColorParser implements AbstractParser { + + public static final ColorParser PARSER = new ColorParser(); + + private final Pattern hexPattern = Pattern.compile("^#([a-fA-F0-9]{6})$"); + private final Pattern rgbPattern = Pattern.compile("^\\b([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\b,? ?\\b([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\b,? ?\\b([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\b$"); + + private ColorParser() {} + + @Override + public Color parse(Player p, String msg) throws Throwable { + int red, green, blue; + + Matcher hexMatcher = hexPattern.matcher(msg); + if (hexMatcher.matches()) { + String hex = hexMatcher.group(1); + red = Integer.parseInt(hex.substring(0, 2), 16); + green = Integer.parseInt(hex.substring(2, 4), 16); + blue = Integer.parseInt(hex.substring(4, 6), 16); + }else { + Matcher rgbMatcher = rgbPattern.matcher(msg); + if (rgbMatcher.matches()) { + red = Integer.parseInt(rgbMatcher.group(1)); + green = Integer.parseInt(rgbMatcher.group(2)); + blue = Integer.parseInt(rgbMatcher.group(3)); + }else { + try { + // just in case the user has entered a named color + java.awt.Color awtColor = ChatColor.valueOf(msg.toUpperCase().replace(' ', '_')).asBungee().getColor(); + red = awtColor.getRed(); + green = awtColor.getGreen(); + blue = awtColor.getBlue(); + }catch (IllegalArgumentException | NullPointerException ex) { + Lang.INVALID_COLOR.send(p); + return null; + } + } + } + return Color.fromRGB(red, green, blue); + } + +} diff --git a/core/src/main/java/fr/skytasul/quests/editors/checkers/EnumParser.java b/core/src/main/java/fr/skytasul/quests/editors/checkers/EnumParser.java index e26b1407..7fd8014c 100644 --- a/core/src/main/java/fr/skytasul/quests/editors/checkers/EnumParser.java +++ b/core/src/main/java/fr/skytasul/quests/editors/checkers/EnumParser.java @@ -1,45 +1,28 @@ package fr.skytasul.quests.editors.checkers; -import java.util.HashMap; -import java.util.Map; +import java.util.Arrays; +import java.util.function.Predicate; import java.util.regex.Pattern; +import java.util.stream.Collectors; -import org.bukkit.entity.Player; - -import fr.skytasul.quests.utils.Lang; - -public class EnumParser> implements AbstractParser { +public class EnumParser> extends CollectionParser { private static final Pattern FORMAT = Pattern.compile("[ _]"); - private Map names; - private String namesString; - public EnumParser(Class enumClass) { - try { - T[] values = (T[]) enumClass.getDeclaredMethod("values").invoke(null); - names = new HashMap<>(values.length + 1, 1); - for (T value : values) { - names.put(proceed(value.name()), value); - } - namesString = String.join(", ", names.keySet()); - }catch (ReflectiveOperationException ex) { - ex.printStackTrace(); - } + super(Arrays.asList(enumClass.getEnumConstants()), constant -> processConstantName(constant.name())); } - - @Override - public T parse(Player p, String msg) throws Throwable { - T obj = names.get(proceed(msg)); - if (obj == null) Lang.NO_SUCH_ELEMENT.send(p, namesString); - return obj; + + public EnumParser(Class enumClass, Predicate filter) { + super(Arrays.stream(enumClass.getEnumConstants()).filter(filter).collect(Collectors.toList()), constant -> processConstantName(constant.name())); } - - public String getNames() { - return namesString; + + @Override + protected String processName(String msg) { + return processConstantName(msg); } - - private String proceed(String key) { + + static String processConstantName(String key) { return FORMAT.matcher(key.toLowerCase()).replaceAll(""); } diff --git a/core/src/main/java/fr/skytasul/quests/editors/checkers/MaterialParser.java b/core/src/main/java/fr/skytasul/quests/editors/checkers/MaterialParser.java index 65830cf1..7d9fa47c 100644 --- a/core/src/main/java/fr/skytasul/quests/editors/checkers/MaterialParser.java +++ b/core/src/main/java/fr/skytasul/quests/editors/checkers/MaterialParser.java @@ -10,13 +10,19 @@ public class MaterialParser implements AbstractParser { + public static final MaterialParser ITEM_PARSER = new MaterialParser(true, false); + public static final MaterialParser BLOCK_PARSER = new MaterialParser(false, true); + public static final MaterialParser ANY_PARSER = new MaterialParser(false, false); + private boolean item, block; + @Deprecated public MaterialParser(boolean item, boolean block) { this.item = item; this.block = block; } + @Override public XMaterial parse(Player p, String msg) throws Throwable { XMaterial tmp = XMaterial.matchXMaterial(msg).orElse(null); if (tmp == null){ @@ -26,8 +32,10 @@ public XMaterial parse(Player p, String msg) throws Throwable { if (block) { Lang.UNKNOWN_BLOCK_TYPE.send(p); }else Lang.UNKNOWN_ITEM_TYPE.send(p); + return null; } - }else if (item) { + } + if (item) { if (NMS.getMCVersion() >= 13 && !Post1_13.isItem(tmp.parseMaterial())) { Lang.INVALID_ITEM_TYPE.send(p); return null; diff --git a/core/src/main/java/fr/skytasul/quests/editors/checkers/NumberParser.java b/core/src/main/java/fr/skytasul/quests/editors/checkers/NumberParser.java index 1844b649..5ac85b15 100644 --- a/core/src/main/java/fr/skytasul/quests/editors/checkers/NumberParser.java +++ b/core/src/main/java/fr/skytasul/quests/editors/checkers/NumberParser.java @@ -11,10 +11,13 @@ public class NumberParser implements AbstractParser { public static final NumberParser INTEGER_PARSER = new NumberParser<>(Integer.class, false, false); public static final NumberParser INTEGER_PARSER_POSITIVE = new NumberParser<>(Integer.class, true, false); public static final NumberParser INTEGER_PARSER_STRICT_POSITIVE = new NumberParser<>(Integer.class, true, true); + public static final NumberParser DOUBLE_PARSER_STRICT_POSITIVE = new NumberParser<>(Double.class, true, true); private Class numberType; private boolean positive; private boolean noZero; + private BigDecimal min; + private BigDecimal max; public NumberParser(Class numberType, boolean positive) { this(numberType, positive, false); @@ -26,6 +29,13 @@ public NumberParser(Class numberType, boolean positive, boolean noZero) { this.noZero = noZero; } + public NumberParser(Class numberType, T min, T max) { + this.numberType = numberType; + this.min = new BigDecimal(min.doubleValue()); + this.max = new BigDecimal(max.doubleValue()); + } + + @Override public T parse(Player p, String msg) { try{ String tname = numberType != Integer.class ? numberType.getSimpleName() : "Int"; @@ -40,6 +50,13 @@ public T parse(Player p, String msg) { return null; } } + if (min != null || max != null) { + BigDecimal bd = new BigDecimal(msg); + if ((min != null && bd.compareTo(min) < 0) || (max != null && bd.compareTo(max) > 0)) { + Lang.NUMBER_NOT_IN_BOUNDS.send(p, min, max); + return null; + } + } return number; }catch (Exception ex) {} Lang.NUMBER_INVALID.send(p, msg); diff --git a/core/src/main/java/fr/skytasul/quests/gui/Inventories.java b/core/src/main/java/fr/skytasul/quests/gui/Inventories.java index c81b09b3..5ccfe3e6 100644 --- a/core/src/main/java/fr/skytasul/quests/gui/Inventories.java +++ b/core/src/main/java/fr/skytasul/quests/gui/Inventories.java @@ -42,10 +42,16 @@ public static Inventory createGetInv(Player p, CustomInventory inv){ */ public static T create(Player p, T inv) { closeWithoutExit(p); - Inventory tinv = inv.open(p); - if (tinv == null) return inv; - put(p, inv, tinv); - return inv; + try { + Inventory tinv = inv.open(p); + if (tinv == null) return inv; + put(p, inv, tinv); + return inv; + }catch (Throwable ex) { + BeautyQuests.logger.severe("Cannot open inventory " + inv.getClass().getSimpleName() + " to player " + p.getName(), ex); + closeAndExit(p); + return null; + } } public static void onClick(InventoryClickEvent e) { diff --git a/core/src/main/java/fr/skytasul/quests/gui/ItemUtils.java b/core/src/main/java/fr/skytasul/quests/gui/ItemUtils.java index 9ad0e189..e24f0a1f 100644 --- a/core/src/main/java/fr/skytasul/quests/gui/ItemUtils.java +++ b/core/src/main/java/fr/skytasul/quests/gui/ItemUtils.java @@ -30,7 +30,7 @@ public class ItemUtils { private static final int LORE_LINE_LENGTH = 40; - private static final int LORE_LINE_LENGTH_CRITICAL = 100; + private static final int LORE_LINE_LENGTH_CRITICAL = 1000; /** * Create an ItemStack instance from a generic XMaterial @@ -70,7 +70,7 @@ public static ItemStack item(XMaterial type, String name, List lore) { * @return the ItemStack instance */ public static ItemStack skull(String name, String skull, String... lore) { - ItemStack is = XMaterial.playerSkullItem(); + ItemStack is = XMaterial.PLAYER_HEAD.parseItem(); SkullMeta im = (SkullMeta) is.getItemMeta(); if (skull != null) im.setOwner(skull); is.setItemMeta(applyMeta(im, name, lore)); diff --git a/core/src/main/java/fr/skytasul/quests/gui/blocks/SelectBlockGUI.java b/core/src/main/java/fr/skytasul/quests/gui/blocks/SelectBlockGUI.java index 943ce1c3..07b35ff3 100644 --- a/core/src/main/java/fr/skytasul/quests/gui/blocks/SelectBlockGUI.java +++ b/core/src/main/java/fr/skytasul/quests/gui/blocks/SelectBlockGUI.java @@ -73,7 +73,7 @@ public Inventory open(Player p){ private void updateTypeItem() { inv.setItem(TYPE_SLOT, item(type, Lang.materialName.format(type.name()))); - if (inv.getItem(TYPE_SLOT) == null || inv.getItem(3).getType() == Material.AIR) { // means that the material cannot be treated as an inventory item (ex: fire) + if (inv.getItem(TYPE_SLOT) == null || inv.getItem(TYPE_SLOT).getType() == Material.AIR) { // means that the material cannot be treated as an inventory item (ex: fire) inv.setItem(TYPE_SLOT, item(XMaterial.STONE, Lang.materialName.format(type.name()), QuestOption.formatDescription(Lang.materialNotItemLore.format(type.name())))); } if (tag == null) ItemUtils.addEnchant(inv.getItem(TYPE_SLOT), Enchantment.DURABILITY, 1); @@ -126,7 +126,7 @@ public boolean onClick(Player p, Inventory inv, ItemStack current, int slot, Cli resetTag(); updateTypeItem(); openLastInv(p); - }, new MaterialParser(false, true)).enter(); + }, MaterialParser.BLOCK_PARSER).enter(); break; case DATA_SLOT: diff --git a/core/src/main/java/fr/skytasul/quests/gui/creation/FinishGUI.java b/core/src/main/java/fr/skytasul/quests/gui/creation/FinishGUI.java index f3b6f11f..a774e3ca 100644 --- a/core/src/main/java/fr/skytasul/quests/gui/creation/FinishGUI.java +++ b/core/src/main/java/fr/skytasul/quests/gui/creation/FinishGUI.java @@ -1,6 +1,7 @@ package fr.skytasul.quests.gui.creation; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; @@ -119,7 +120,7 @@ public void update() { XMaterial type = enabled ? XMaterial.GOLD_INGOT : XMaterial.NETHER_BRICK; String itemName = (enabled ? ChatColor.GOLD : ChatColor.DARK_PURPLE).toString() + (session.isEdition() ? Lang.edit : Lang.create).toString(); String itemLore = QuestOption.formatDescription(Lang.createLore.toString()) + (enabled ? " §a✔" : " §c✖"); - String[] lore = Boolean.TRUE.equals(keepPlayerDatas) ? new String[] { itemLore } : new String[] { itemLore, "", Lang.resetLore.toString() }; + String[] lore = keepPlayerDatas == null || keepPlayerDatas.booleanValue() ? new String[] { itemLore } : new String[] { itemLore, "", Lang.resetLore.toString() }; ItemStack item = inv.getItem(slot); @@ -319,18 +320,18 @@ public static void initialize(){ QuestsAPI.registerQuestOption(new QuestOptionCreator<>("confirmMessage", 15, OptionConfirmMessage.class, OptionConfirmMessage::new, null)); QuestsAPI.registerQuestOption(new QuestOptionCreator<>("hologramText", 17, OptionHologramText.class, OptionHologramText::new, Lang.HologramText.toString())); QuestsAPI.registerQuestOption(new QuestOptionCreator<>("bypassLimit", 18, OptionBypassLimit.class, OptionBypassLimit::new, false)); - QuestsAPI.registerQuestOption(new QuestOptionCreator<>("hideNoRequirements", 19, OptionHideNoRequirements.class, OptionHideNoRequirements::new, false)); - QuestsAPI.registerQuestOption(new QuestOptionCreator<>("startableFromGUI", 20, OptionStartable.class, OptionStartable::new, false)); - QuestsAPI.registerQuestOption(new QuestOptionCreator<>("failOnDeath", 21, OptionFailOnDeath.class, OptionFailOnDeath::new, false)); - QuestsAPI.registerQuestOption(new QuestOptionCreator<>("cancellable", 22, OptionCancellable.class, OptionCancellable::new, true)); - QuestsAPI.registerQuestOption(new QuestOptionCreator<>("cancelActions", 23, OptionCancelRewards.class, OptionCancelRewards::new, new ArrayList<>())); + QuestsAPI.registerQuestOption(new QuestOptionCreator<>("startableFromGUI", 19, OptionStartable.class, OptionStartable::new, false)); + QuestsAPI.registerQuestOption(new QuestOptionCreator<>("failOnDeath", 20, OptionFailOnDeath.class, OptionFailOnDeath::new, false)); + QuestsAPI.registerQuestOption(new QuestOptionCreator<>("cancellable", 21, OptionCancellable.class, OptionCancellable::new, true)); + QuestsAPI.registerQuestOption(new QuestOptionCreator<>("cancelActions", 22, OptionCancelRewards.class, OptionCancelRewards::new, new ArrayList<>())); QuestsAPI.registerQuestOption(new QuestOptionCreator<>("hologramLaunch", 25, OptionHologramLaunch.class, OptionHologramLaunch::new, QuestsConfiguration.getHoloLaunchItem())); QuestsAPI.registerQuestOption(new QuestOptionCreator<>("hologramLaunchNo", 26, OptionHologramLaunchNo.class, OptionHologramLaunchNo::new, QuestsConfiguration.getHoloLaunchNoItem())); QuestsAPI.registerQuestOption(new QuestOptionCreator<>("scoreboard", 27, OptionScoreboardEnabled.class, OptionScoreboardEnabled::new, true)); - QuestsAPI.registerQuestOption(new QuestOptionCreator<>("hide", 28, OptionHide.class, OptionHide::new, false, "hid")); + QuestsAPI.registerQuestOption(new QuestOptionCreator<>("hideNoRequirements", 28, OptionHideNoRequirements.class, OptionHideNoRequirements::new, false)); QuestsAPI.registerQuestOption(new QuestOptionCreator<>("auto", 29, OptionAutoQuest.class, OptionAutoQuest::new, false)); QuestsAPI.registerQuestOption(new QuestOptionCreator<>("repeatable", 30, OptionRepeatable.class, OptionRepeatable::new, false, "multiple")); QuestsAPI.registerQuestOption(new QuestOptionCreator<>("timer", 31, OptionTimer.class, OptionTimer::new, QuestsConfiguration.getTimeBetween())); + QuestsAPI.registerQuestOption(new QuestOptionCreator<>("visibility", 32, OptionVisibility.class, OptionVisibility::new, Arrays.asList(OptionVisibility.VisibilityLocation.values()), "hid", "hide")); QuestsAPI.registerQuestOption(new QuestOptionCreator<>("endSound", 34, OptionEndSound.class, OptionEndSound::new, QuestsConfiguration.getFinishSound())); QuestsAPI.registerQuestOption(new QuestOptionCreator<>("firework", 35, OptionFirework.class, OptionFirework::new, QuestsConfiguration.getDefaultFirework())); QuestsAPI.registerQuestOption(new QuestOptionCreator<>("requirements", 36, OptionRequirements.class, OptionRequirements::new, new ArrayList<>())); diff --git a/core/src/main/java/fr/skytasul/quests/gui/creation/QuestCreationSession.java b/core/src/main/java/fr/skytasul/quests/gui/creation/QuestCreationSession.java index bb28ec34..78414bf8 100644 --- a/core/src/main/java/fr/skytasul/quests/gui/creation/QuestCreationSession.java +++ b/core/src/main/java/fr/skytasul/quests/gui/creation/QuestCreationSession.java @@ -49,7 +49,7 @@ public void setStagesEdited() { } public boolean areStagesEdited() { - return stagesEdited; + return isEdition() && stagesEdited; } public StagesGUI getMainGUI() { diff --git a/core/src/main/java/fr/skytasul/quests/gui/creation/QuestObjectGUI.java b/core/src/main/java/fr/skytasul/quests/gui/creation/QuestObjectGUI.java index bff7a2fc..95e8276c 100644 --- a/core/src/main/java/fr/skytasul/quests/gui/creation/QuestObjectGUI.java +++ b/core/src/main/java/fr/skytasul/quests/gui/creation/QuestObjectGUI.java @@ -22,6 +22,7 @@ import fr.skytasul.quests.gui.ItemUtils; import fr.skytasul.quests.gui.templates.ListGUI; import fr.skytasul.quests.gui.templates.PagedGUI; +import fr.skytasul.quests.requirements.EquipmentRequirement; import fr.skytasul.quests.requirements.LevelRequirement; import fr.skytasul.quests.requirements.PermissionsRequirement; import fr.skytasul.quests.requirements.QuestRequirement; @@ -32,6 +33,7 @@ import fr.skytasul.quests.utils.Lang; import fr.skytasul.quests.utils.Utils; import fr.skytasul.quests.utils.XMaterial; +import fr.skytasul.quests.utils.nms.NMS; public class QuestObjectGUI extends ListGUI { @@ -42,28 +44,21 @@ public class QuestObjectGUI extends ListGUI { public QuestObjectGUI(String name, QuestObjectLocation objectLocation, Collection> creators, Consumer> end, List objects) { super(name, DyeColor.CYAN, (List) objects.stream().map(QuestObject::clone).collect(Collectors.toList())); this.name = name; - this.creators = creators.stream().filter(creator -> creator.isAllowed(objectLocation) && (creator.multiple || !objects.stream().anyMatch(object -> object.getCreator() == creator))).collect(Collectors.toList()); + this.creators = creators.stream() + .filter(creator -> creator.isAllowed(objectLocation)) + .filter(creator -> creator.canBeMultiple() || objects.stream().noneMatch(object -> object.getCreator() == creator)) + .collect(Collectors.toList()); this.end = end; } - - @Deprecated - public void reopen(Player p) { - super.reopen(); - } @Override public ItemStack getObjectItemStack(QuestObject object) { return object.getItemStack(); } - @Override - public boolean remove(QuestObject object) { - return super.remove((T) object); - } - @Override protected void removed(T object) { - if (!object.getCreator().multiple) creators.add(object.getCreator()); + if (!object.getCreator().canBeMultiple()) creators.add(object.getCreator()); } @Override @@ -72,14 +67,14 @@ public void createObject(Function callback) { @Override public ItemStack getItemStack(QuestObjectCreator object) { - return object.item; + return object.getItem(); } @Override public void click(QuestObjectCreator existing, ItemStack item, ClickType clickType) { - T object = existing.newObjectSupplier.get(); - if (!existing.multiple) creators.remove(existing); - object.itemClick(new QuestObjectClickEvent(p, QuestObjectGUI.this, callback.apply(object), clickType, true)); + T object = existing.newObject(); + if (!existing.canBeMultiple()) creators.remove(existing); + object.itemClick(new QuestObjectClickEvent(p, QuestObjectGUI.this, callback.apply(object), clickType, true, object)); } @Override @@ -93,7 +88,7 @@ public CloseBehavior onClose(Player p, Inventory inv) { @Override public void clickObject(QuestObject existing, ItemStack item, ClickType clickType) { - existing.itemClick(new QuestObjectClickEvent(p, this, item, clickType, false)); + existing.itemClick(new QuestObjectClickEvent(p, this, item, clickType, false, existing)); } @Override @@ -124,6 +119,8 @@ public static void initialize(){ QuestsAPI.getRequirements().register(new RequirementCreator("levelRequired", LevelRequirement.class, ItemUtils.item(XMaterial.EXPERIENCE_BOTTLE, Lang.RLevel.toString()), LevelRequirement::new)); QuestsAPI.getRequirements().register(new RequirementCreator("permissionRequired", PermissionsRequirement.class, ItemUtils.item(XMaterial.PAPER, Lang.RPermissions.toString()), PermissionsRequirement::new)); QuestsAPI.getRequirements().register(new RequirementCreator("scoreboardRequired", ScoreboardRequirement.class, ItemUtils.item(XMaterial.COMMAND_BLOCK, Lang.RScoreboard.toString()), ScoreboardRequirement::new)); + if (NMS.getMCVersion() >= 9) + QuestsAPI.getRequirements().register(new RequirementCreator("equipmentRequired", EquipmentRequirement.class, ItemUtils.item(XMaterial.CHAINMAIL_HELMET, Lang.REquipment.toString()), EquipmentRequirement::new)); } } \ No newline at end of file diff --git a/core/src/main/java/fr/skytasul/quests/gui/creation/stages/Line.java b/core/src/main/java/fr/skytasul/quests/gui/creation/stages/Line.java index d0766821..f766b088 100644 --- a/core/src/main/java/fr/skytasul/quests/gui/creation/stages/Line.java +++ b/core/src/main/java/fr/skytasul/quests/gui/creation/stages/Line.java @@ -37,8 +37,8 @@ public boolean isEmpty() { * @param is the item * @param click the action when a click on the item (if null nothing happens) (runnable parameter is the player) */ - public void setItem(int slot, ItemStack is, StageRunnable click){ - setItem(slot, is, click, false, true); + public int setItem(int slot, ItemStack is, StageRunnable click) { + return setItem(slot, is, click, false, true); } /** @@ -49,13 +49,13 @@ public void setItem(int slot, ItemStack is, StageRunnable click){ * @param override override if any other item is on the selected slot deprecated * @param refresh refresh all items (display this item) */ - public void setItem(int slot, ItemStack is, StageRunnable click, boolean override, boolean refresh){ + public int setItem(int slot, ItemStack is, StageRunnable click, boolean override, boolean refresh) { Pair en = new Pair<>(is, click); if (override){ items.set(slot, en); }else { if (items.get(slot) != null){ - items.add(en); + slot = items.add(en); }else items.set(slot, en); } if (items.getLast() <= 8) { @@ -65,6 +65,7 @@ public void setItem(int slot, ItemStack is, StageRunnable click, boolean overrid activePage = 0; setItems(activePage); } + return slot; } /** @@ -73,9 +74,16 @@ public void setItem(int slot, ItemStack is, StageRunnable click, boolean overrid * @param newItem the new item inserted */ public void editItem(int slot, ItemStack newItem){ + editItem(slot, newItem, false); + } + + public void editItem(int slot, ItemStack newItem, boolean refresh) { Pair last = items.get(slot); if (last == null) return; - items.set(slot, new Pair(newItem, last.getValue())); + items.set(slot, new Pair<>(newItem, last.getValue())); + if (refresh) { + setItems(activePage); + } } /** diff --git a/core/src/main/java/fr/skytasul/quests/gui/creation/stages/StagesGUI.java b/core/src/main/java/fr/skytasul/quests/gui/creation/stages/StagesGUI.java index a3532425..e3633b7e 100644 --- a/core/src/main/java/fr/skytasul/quests/gui/creation/stages/StagesGUI.java +++ b/core/src/main/java/fr/skytasul/quests/gui/creation/stages/StagesGUI.java @@ -104,10 +104,10 @@ private void setStageCreate(Line line, boolean branches){ line.setItem(0, stageCreate.clone(), (p, item) -> { line.setItem(0, null, null, true, false); int i = 0; - for (StageType creator : QuestsAPI.stages) { - if (creator.isValid()) { - line.setItem(++i, creator.item, (p1, item1) -> { - runClick(line, creator, branches).start(p1); + for (StageType type : QuestsAPI.getStages()) { + if (type.isValid()) { + line.setItem(++i, type.getItem(), (p1, item1) -> { + runClick(line, type, branches).start(p1); }, true, false); } } @@ -127,15 +127,16 @@ private void updateLineManageLore(Line line) { line.editItem(0, ItemUtils.lore(line.getItem(0), getLineManageLore(line.getLine()))); } - private StageCreation runClick(Line line, StageType creator, boolean branches) { + private StageCreation runClick(Line line, StageType type, boolean branches) { line.removeItems(); - StageCreation creation = creator.creationSupplier.supply(line, branches); + StageCreation creation = type.getCreationSupplier().supply(line, branches); line.creation = creation; + creation.setup((StageType) type); inv.setItem(SLOT_FINISH, ItemUtils.itemDone); int maxStages = branches ? 20 : 15; - ItemStack manageItem = ItemUtils.item(XMaterial.BARRIER, Lang.stageType.format(creator.name), getLineManageLore(line.getLine())); + ItemStack manageItem = ItemUtils.item(XMaterial.BARRIER, Lang.stageType.format(type.getName()), getLineManageLore(line.getLine())); line.setItem(0, manageItem, new StageRunnableClick() { @Override public void run(Player p, ItemStack item, ClickType click) { @@ -311,7 +312,6 @@ private void editBranch(QuestBranch branch){ private static final ItemStack stageNPC = ItemUtils.item(XMaterial.OAK_SIGN, Lang.stageNPC.toString()); private static final ItemStack stageItems = ItemUtils.item(XMaterial.CHEST, Lang.stageBring.toString()); - private static final ItemStack stageArea = ItemUtils.item(XMaterial.WOODEN_AXE, Lang.stageGoTo.toString()); private static final ItemStack stageMobs = ItemUtils.item(XMaterial.WOODEN_SWORD, Lang.stageMobs.toString()); private static final ItemStack stageMine = ItemUtils.item(XMaterial.WOODEN_PICKAXE, Lang.stageMine.toString()); private static final ItemStack stagePlace = ItemUtils.item(XMaterial.OAK_STAIRS, Lang.stagePlace.toString()); @@ -326,26 +326,31 @@ private void editBranch(QuestBranch branch){ private static final ItemStack stagePlayTime = ItemUtils.item(XMaterial.CLOCK, Lang.stagePlayTime.toString()); private static final ItemStack stageBreed = ItemUtils.item(XMaterial.WHEAT, Lang.stageBreedAnimals.toString()); private static final ItemStack stageTame = ItemUtils.item(XMaterial.CARROT, Lang.stageTameAnimals.toString()); + private static final ItemStack stageDeath = ItemUtils.item(XMaterial.SKELETON_SKULL, Lang.stageDeath.toString()); + private static final ItemStack stageDealDamage = ItemUtils.item(XMaterial.REDSTONE, Lang.stageDealDamage.toString()); + private static final ItemStack stageEatDrink = ItemUtils.item(XMaterial.COOKED_PORKCHOP, Lang.stageEatDrink.toString()); public static void initialize(){ DebugUtils.logMessage("Initlializing default stage types."); - QuestsAPI.registerStage(new StageType("REGION", StageArea.class, Lang.Find.name(), StageArea::deserialize, stageArea, StageArea.Creator::new, "WorldGuard")); - QuestsAPI.registerStage(new StageType<>("NPC", StageNPC.class, Lang.Talk.name(), StageNPC::deserialize, stageNPC, StageNPC.Creator::new)); - QuestsAPI.registerStage(new StageType<>("ITEMS", StageBringBack.class, Lang.Items.name(), StageBringBack::deserialize, stageItems, StageBringBack.Creator::new)); - QuestsAPI.registerStage(new StageType<>("MOBS", StageMobs.class, Lang.Mobs.name(), StageMobs::deserialize, stageMobs, StageMobs.Creator::new)); - QuestsAPI.registerStage(new StageType<>("MINE", StageMine.class, Lang.Mine.name(), StageMine::deserialize, stageMine, StageMine.Creator::new)); - QuestsAPI.registerStage(new StageType<>("PLACE_BLOCKS", StagePlaceBlocks.class, Lang.Place.name(), StagePlaceBlocks::deserialize, stagePlace, StagePlaceBlocks.Creator::new)); - QuestsAPI.registerStage(new StageType<>("CHAT", StageChat.class, Lang.Chat.name(), StageChat::deserialize, stageChat, StageChat.Creator::new)); - QuestsAPI.registerStage(new StageType<>("INTERACT", StageInteract.class, Lang.Interact.name(), StageInteract::deserialize, stageInteract, StageInteract.Creator::new)); - QuestsAPI.registerStage(new StageType<>("FISH", StageFish.class, Lang.Fish.name(), StageFish::deserialize, stageFish, StageFish.Creator::new)); - QuestsAPI.registerStage(new StageType<>("MELT", StageMelt.class, Lang.Melt.name(), StageMelt::deserialize, stageMelt, StageMelt.Creator::new)); - QuestsAPI.registerStage(new StageType<>("ENCHANT", StageEnchant.class, Lang.Enchant.name(), StageEnchant::deserialize, stageEnchant, StageEnchant.Creator::new)); - QuestsAPI.registerStage(new StageType<>("CRAFT", StageCraft.class, Lang.Craft.name(), StageCraft::deserialize, stageCraft, StageCraft.Creator::new)); - QuestsAPI.registerStage(new StageType<>("BUCKET", StageBucket.class, Lang.Bucket.name(), StageBucket::deserialize, stageBucket, StageBucket.Creator::new)); - QuestsAPI.registerStage(new StageType<>("LOCATION", StageLocation.class, Lang.Location.name(), StageLocation::deserialize, stageLocation, StageLocation.Creator::new)); - QuestsAPI.registerStage(new StageType<>("PLAY_TIME", StagePlayTime.class, Lang.PlayTime.name(), StagePlayTime::deserialize, stagePlayTime, StagePlayTime.Creator::new)); - QuestsAPI.registerStage(new StageType<>("BREED", StageBreed.class, Lang.Breed.name(), StageBreed::deserialize, stageBreed, StageBreed.Creator::new)); - QuestsAPI.registerStage(new StageType<>("TAME", StageTame.class, Lang.Tame.name(), StageTame::deserialize, stageTame, StageTame.Creator::new)); + QuestsAPI.getStages().register(new StageType<>("NPC", StageNPC.class, Lang.Talk.name(), StageNPC::deserialize, stageNPC, StageNPC.Creator::new)); + QuestsAPI.getStages().register(new StageType<>("ITEMS", StageBringBack.class, Lang.Items.name(), StageBringBack::deserialize, stageItems, StageBringBack.Creator::new)); + QuestsAPI.getStages().register(new StageType<>("MOBS", StageMobs.class, Lang.Mobs.name(), StageMobs::deserialize, stageMobs, StageMobs.Creator::new)); + QuestsAPI.getStages().register(new StageType<>("MINE", StageMine.class, Lang.Mine.name(), StageMine::deserialize, stageMine, StageMine.Creator::new)); + QuestsAPI.getStages().register(new StageType<>("PLACE_BLOCKS", StagePlaceBlocks.class, Lang.Place.name(), StagePlaceBlocks::deserialize, stagePlace, StagePlaceBlocks.Creator::new)); + QuestsAPI.getStages().register(new StageType<>("CHAT", StageChat.class, Lang.Chat.name(), StageChat::deserialize, stageChat, StageChat.Creator::new)); + QuestsAPI.getStages().register(new StageType<>("INTERACT", StageInteract.class, Lang.Interact.name(), StageInteract::deserialize, stageInteract, StageInteract.Creator::new)); + QuestsAPI.getStages().register(new StageType<>("FISH", StageFish.class, Lang.Fish.name(), StageFish::deserialize, stageFish, StageFish.Creator::new)); + QuestsAPI.getStages().register(new StageType<>("MELT", StageMelt.class, Lang.Melt.name(), StageMelt::deserialize, stageMelt, StageMelt.Creator::new)); + QuestsAPI.getStages().register(new StageType<>("ENCHANT", StageEnchant.class, Lang.Enchant.name(), StageEnchant::deserialize, stageEnchant, StageEnchant.Creator::new)); + QuestsAPI.getStages().register(new StageType<>("CRAFT", StageCraft.class, Lang.Craft.name(), StageCraft::deserialize, stageCraft, StageCraft.Creator::new)); + QuestsAPI.getStages().register(new StageType<>("BUCKET", StageBucket.class, Lang.Bucket.name(), StageBucket::deserialize, stageBucket, StageBucket.Creator::new)); + QuestsAPI.getStages().register(new StageType<>("LOCATION", StageLocation.class, Lang.Location.name(), StageLocation::deserialize, stageLocation, StageLocation.Creator::new)); + QuestsAPI.getStages().register(new StageType<>("PLAY_TIME", StagePlayTime.class, Lang.PlayTime.name(), StagePlayTime::deserialize, stagePlayTime, StagePlayTime.Creator::new)); + QuestsAPI.getStages().register(new StageType<>("BREED", StageBreed.class, Lang.Breed.name(), StageBreed::deserialize, stageBreed, StageBreed.Creator::new)); + QuestsAPI.getStages().register(new StageType<>("TAME", StageTame.class, Lang.Tame.name(), StageTame::deserialize, stageTame, StageTame.Creator::new)); + QuestsAPI.getStages().register(new StageType<>("DEATH", StageDeath.class, Lang.Death.name(), StageDeath::deserialize, stageDeath, StageDeath.Creator::new)); + QuestsAPI.getStages().register(new StageType<>("DEAL_DAMAGE", StageDealDamage.class, Lang.DealDamage.name(), StageDealDamage::deserialize, stageDealDamage, StageDealDamage.Creator::new)); + QuestsAPI.getStages().register(new StageType<>("EAT_DRINK", StageEatDrink.class, Lang.EatDrink.name(), StageEatDrink::new, stageEatDrink, StageEatDrink.Creator::new)); } } \ No newline at end of file diff --git a/core/src/main/java/fr/skytasul/quests/gui/misc/DamageCausesGUI.java b/core/src/main/java/fr/skytasul/quests/gui/misc/DamageCausesGUI.java new file mode 100644 index 00000000..795cb533 --- /dev/null +++ b/core/src/main/java/fr/skytasul/quests/gui/misc/DamageCausesGUI.java @@ -0,0 +1,63 @@ +package fr.skytasul.quests.gui.misc; + +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; + +import org.bukkit.DyeColor; +import org.bukkit.event.entity.EntityDamageEvent.DamageCause; +import org.bukkit.inventory.ItemStack; + +import fr.skytasul.quests.gui.ItemUtils; +import fr.skytasul.quests.gui.templates.ListGUI; +import fr.skytasul.quests.gui.templates.StaticPagedGUI; +import fr.skytasul.quests.utils.Lang; +import fr.skytasul.quests.utils.XMaterial; + +public class DamageCausesGUI extends ListGUI { + + private static final Map MAPPED_ITEMS; + + static { + MAPPED_ITEMS = new EnumMap<>(DamageCause.class); + for (DamageCause cause : DamageCause.values()) { + XMaterial type; + if (cause.name().length() <= 6) { + type = XMaterial.CREEPER_HEAD; + }else if (cause.name().length() < 10) { + type = XMaterial.ZOMBIE_HEAD; + }else if (cause.name().length() < 14) { + type = XMaterial.SKELETON_SKULL; + }else { + type = XMaterial.WITHER_SKELETON_SKULL; + } + // this is simply not to have a fully uniform list + MAPPED_ITEMS.put(cause, ItemUtils.item(type, "§7" + cause.name())); + } + } + + private final Consumer> end; + + public DamageCausesGUI(List causes, Consumer> end) { + super(Lang.INVENTORY_DAMAGE_CAUSE.toString(), DyeColor.RED, causes); + this.end = end; + } + + @Override + public void finish(List objects) { + end.accept(objects); + } + + @Override + public ItemStack getObjectItemStack(DamageCause object) { + return MAPPED_ITEMS.get(object); + } + + @Override + public void createObject(Function callback) { + new StaticPagedGUI(Lang.INVENTORY_DAMAGE_CAUSES_LIST.toString(), DyeColor.ORANGE, MAPPED_ITEMS, cause -> callback.apply(cause), DamageCause::name).create(p); + } + +} diff --git a/core/src/main/java/fr/skytasul/quests/gui/misc/ItemComparisonGUI.java b/core/src/main/java/fr/skytasul/quests/gui/misc/ItemComparisonGUI.java index 7e0cf126..5b522a3e 100644 --- a/core/src/main/java/fr/skytasul/quests/gui/misc/ItemComparisonGUI.java +++ b/core/src/main/java/fr/skytasul/quests/gui/misc/ItemComparisonGUI.java @@ -21,7 +21,7 @@ public class ItemComparisonGUI extends PagedGUI { private ItemComparisonMap comparisons; public ItemComparisonGUI(ItemComparisonMap comparisons, Runnable validate) { - super(Lang.INVENTORY_ITEM_COMPARISONS.toString(), DyeColor.LIME, QuestsAPI.itemComparisons, x -> validate.run(), null); + super(Lang.INVENTORY_ITEM_COMPARISONS.toString(), DyeColor.LIME, QuestsAPI.getItemComparisons(), x -> validate.run(), null); this.comparisons = comparisons; } diff --git a/core/src/main/java/fr/skytasul/quests/gui/misc/ItemCreatorGUI.java b/core/src/main/java/fr/skytasul/quests/gui/misc/ItemCreatorGUI.java index de1a1921..e3b3624d 100644 --- a/core/src/main/java/fr/skytasul/quests/gui/misc/ItemCreatorGUI.java +++ b/core/src/main/java/fr/skytasul/quests/gui/misc/ItemCreatorGUI.java @@ -48,7 +48,7 @@ public Inventory open(Player p) { this.p = p; inv = Bukkit.createInventory(null, 18, Lang.INVENTORY_CREATOR.toString()); - inv.setItem(0, ItemUtils.item(XMaterial.ARROW, Lang.itemType.toString())); + inv.setItem(0, ItemUtils.item(XMaterial.GRASS_BLOCK, Lang.itemType.toString())); inv.setItem(1, ItemUtils.item(XMaterial.REDSTONE, Lang.Amount.format(1))); inv.setItem(2, ItemUtils.itemSwitch(Lang.itemFlags.toString(), false)); inv.setItem(3, ItemUtils.item(XMaterial.NAME_TAG, Lang.itemName.toString())); @@ -82,7 +82,7 @@ public boolean onClick(Player p, Inventory inv, ItemStack current, int slot, Cli new TextEditor<>(p, () -> reopen(), obj -> { type = obj; reopen(); - }, new MaterialParser(true, false)).enter(); + }, MaterialParser.ITEM_PARSER).enter(); break; case 1: diff --git a/core/src/main/java/fr/skytasul/quests/gui/misc/ItemGUI.java b/core/src/main/java/fr/skytasul/quests/gui/misc/ItemGUI.java index 36b22a9c..659050c6 100644 --- a/core/src/main/java/fr/skytasul/quests/gui/misc/ItemGUI.java +++ b/core/src/main/java/fr/skytasul/quests/gui/misc/ItemGUI.java @@ -26,6 +26,7 @@ public ItemGUI(Consumer end, Runnable cancel) { this.cancel = cancel; } + @Override public Inventory open(Player p){ Inventory inv = Bukkit.createInventory(null, InventoryType.DROPPER, Lang.INVENTORY_ITEM.toString()); @@ -39,6 +40,7 @@ public Inventory open(Player p){ return p.openInventory(inv).getTopInventory(); } + @Override public boolean onClick(Player p, Inventory inv, ItemStack current, int slot, ClickType click){ if (slot != 4) return true; new ItemCreatorGUI((obj) -> { @@ -47,7 +49,9 @@ public boolean onClick(Player p, Inventory inv, ItemStack current, int slot, Cli return true; } + @Override public boolean onClickCursor(Player p, Inventory inv, ItemStack current, ItemStack cursor, int slot){ + if (slot != 4) return true; p.setItemOnCursor(null); end.accept(cursor); return false; diff --git a/core/src/main/java/fr/skytasul/quests/gui/mobs/EntityTypeGUI.java b/core/src/main/java/fr/skytasul/quests/gui/mobs/EntityTypeGUI.java index 6fa543c3..5be127a7 100644 --- a/core/src/main/java/fr/skytasul/quests/gui/mobs/EntityTypeGUI.java +++ b/core/src/main/java/fr/skytasul/quests/gui/mobs/EntityTypeGUI.java @@ -17,6 +17,7 @@ import fr.skytasul.quests.gui.ItemUtils; import fr.skytasul.quests.gui.templates.PagedGUI; import fr.skytasul.quests.utils.Lang; +import fr.skytasul.quests.utils.Utils; import fr.skytasul.quests.utils.XMaterial; public class EntityTypeGUI extends PagedGUI{ @@ -26,7 +27,7 @@ public class EntityTypeGUI extends PagedGUI{ for (EntityType en : EntityType.values()){ if (!en.isAlive()) continue; if (en == EntityType.PLAYER) continue; - XMaterial mat = XMaterial.mobItem(en); + XMaterial mat = Utils.mobItem(en); if (mat == null) continue; entities.put(en, ItemUtils.item(mat, en.getName())); } @@ -37,7 +38,11 @@ public class EntityTypeGUI extends PagedGUI{ private Consumer run; public EntityTypeGUI(Consumer run, Predicate typeFilter) { - super(Lang.INVENTORY_TYPE.toString(), DyeColor.PURPLE, entities.keySet().stream().filter(typeFilter).collect(Collectors.toList()), null, EntityTypeGUI::getName); + super(Lang.INVENTORY_TYPE.toString(), DyeColor.PURPLE, entities + .keySet() + .stream() + .filter(typeFilter == null ? __ -> true : typeFilter) + .collect(Collectors.toList()), null, EntityTypeGUI::getName); sortValues(EntityType::getName); this.run = run; } diff --git a/core/src/main/java/fr/skytasul/quests/gui/mobs/MobSelectionGUI.java b/core/src/main/java/fr/skytasul/quests/gui/mobs/MobSelectionGUI.java new file mode 100644 index 00000000..b9e9b655 --- /dev/null +++ b/core/src/main/java/fr/skytasul/quests/gui/mobs/MobSelectionGUI.java @@ -0,0 +1,44 @@ +package fr.skytasul.quests.gui.mobs; + +import java.util.function.Consumer; + +import org.bukkit.DyeColor; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.ClickType; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; + +import fr.skytasul.quests.api.mobs.Mob; +import fr.skytasul.quests.api.mobs.MobFactory; +import fr.skytasul.quests.gui.templates.PagedGUI; +import fr.skytasul.quests.utils.Lang; +import fr.skytasul.quests.utils.Utils; + +public class MobSelectionGUI extends PagedGUI> { + + private Consumer> end; + + public MobSelectionGUI(Consumer> end) { + super(Lang.INVENTORY_MOBSELECT.toString(), DyeColor.LIME, MobFactory.factories); + this.end = end; + } + + @Override + public ItemStack getItemStack(MobFactory object) { + return object.getFactoryItem(); + } + + @Override + public void click(MobFactory existing, ItemStack item, ClickType clickType) { + existing.itemClick(p, mobData -> { + end.accept(mobData == null ? null : new Mob(existing, mobData)); + }); + } + + @Override + public CloseBehavior onClose(Player p, Inventory inv) { + Utils.runSync(() -> end.accept(null)); + return CloseBehavior.REMOVE; + } + +} diff --git a/core/src/main/java/fr/skytasul/quests/gui/mobs/MobsListGUI.java b/core/src/main/java/fr/skytasul/quests/gui/mobs/MobsListGUI.java index c1a3ca4e..75d4a201 100644 --- a/core/src/main/java/fr/skytasul/quests/gui/mobs/MobsListGUI.java +++ b/core/src/main/java/fr/skytasul/quests/gui/mobs/MobsListGUI.java @@ -1,27 +1,26 @@ package fr.skytasul.quests.gui.mobs; import java.util.AbstractMap; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.function.Consumer; - import org.bukkit.Bukkit; -import org.bukkit.DyeColor; import org.bukkit.entity.Player; import org.bukkit.event.inventory.ClickType; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemStack; - +import fr.skytasul.quests.api.mobs.LeveledMobFactory; import fr.skytasul.quests.api.mobs.Mob; -import fr.skytasul.quests.api.mobs.MobFactory; import fr.skytasul.quests.editors.TextEditor; import fr.skytasul.quests.editors.checkers.NumberParser; import fr.skytasul.quests.gui.CustomInventory; import fr.skytasul.quests.gui.Inventories; import fr.skytasul.quests.gui.ItemUtils; -import fr.skytasul.quests.gui.templates.PagedGUI; import fr.skytasul.quests.utils.Lang; +import fr.skytasul.quests.utils.Utils; import fr.skytasul.quests.utils.XMaterial; public class MobsListGUI implements CustomInventory{ @@ -38,6 +37,7 @@ public CustomInventory openLastInv(Player p) { return this; } + @Override public Inventory open(Player p){ inv = Bukkit.createInventory(null, 9, Lang.INVENTORY_MOBS.toString()); @@ -53,10 +53,11 @@ public void setMobsFromMap(Map, Integer>> map) { int id = entry.getKey(); Entry, Integer> mobEntry = entry.getValue(); mobs.put(id, mobEntry); - inv.setItem(id, mobEntry.getKey().createItemStack(mobEntry.getValue())); + inv.setItem(id, createItemStack(mobEntry.getKey(), mobEntry.getValue())); } } + @Override public boolean onClick(Player p, Inventory inv, ItemStack current, int slot, ClickType click){ if (slot == 8){ Inventories.closeAndExit(p); @@ -65,45 +66,61 @@ public boolean onClick(Player p, Inventory inv, ItemStack current, int slot, Cli } Entry, Integer> mobEntry = mobs.get(slot); if (mobEntry == null) { - new PagedGUI>(Lang.INVENTORY_MOBSELECT.toString(), DyeColor.LIME, MobFactory.factories) { - public ItemStack getItemStack(MobFactory object) { - return object.getFactoryItem(); - } - - @SuppressWarnings ("rawtypes") - public void click(MobFactory existing, ItemStack item, ClickType clickType) { - existing.itemClick(p, (obj) -> { - Inventories.put(p, openLastInv(p), MobsListGUI.this.inv); - if (obj == null) return; - Mob mob = new Mob(existing, obj); - MobsListGUI.this.inv.setItem(slot, mob.createItemStack(1)); - mobs.put(slot, new AbstractMap.SimpleEntry<>(mob, 1)); - }); + new MobSelectionGUI(mob -> { + if (mob != null) { + inv.setItem(slot, createItemStack(mob, 1)); + mobs.put(slot, new AbstractMap.SimpleEntry<>(mob, 1)); } - }.create(p); + Inventories.put(p, openLastInv(p), MobsListGUI.this.inv); + }).create(p); }else { if (click == ClickType.SHIFT_LEFT) { Lang.MOB_NAME.send(p); new TextEditor<>(p, () -> openLastInv(p), name -> { mobEntry.getKey().setCustomName((String) name); - inv.setItem(slot, mobEntry.getKey().createItemStack(mobEntry.getValue())); + inv.setItem(slot, createItemStack(mobEntry.getKey(), mobEntry.getValue())); openLastInv(p); }).passNullIntoEndConsumer().enter(); }else if (click == ClickType.LEFT) { Lang.MOB_AMOUNT.send(p); new TextEditor<>(p, () -> openLastInv(p), amount -> { mobEntry.setValue(amount); - inv.setItem(slot, mobEntry.getKey().createItemStack(amount)); + inv.setItem(slot, createItemStack(mobEntry.getKey(), amount)); openLastInv(p); }, NumberParser.INTEGER_PARSER_STRICT_POSITIVE).enter(); - }else if (click.isRightClick()) { + } else if (click == ClickType.SHIFT_RIGHT) { + if (mobEntry.getKey().getFactory() instanceof LeveledMobFactory) { + new TextEditor<>(p, () -> openLastInv(p), level -> { + mobEntry.getKey().setMinLevel(level); + inv.setItem(slot, createItemStack(mobEntry.getKey(), mobEntry.getValue())); + openLastInv(p); + }, new NumberParser<>(Double.class, true, false)).enter(); + } else { + Utils.playPluginSound(p.getLocation(), "ENTITY_VILLAGER_NO", 0.6f); + } + } else if (click == ClickType.RIGHT) { mobs.remove(slot); inv.setItem(slot, none.clone()); - return true; } } return true; } + + private ItemStack createItemStack(Mob mob, int amount) { + List lore = new ArrayList<>(); + lore.add(Lang.Amount.format(amount)); + lore.addAll(mob.getFactory().getDescriptiveLore(mob.getData())); + lore.add(""); + lore.add(Lang.click.toString()); + if (mob.getFactory() instanceof LeveledMobFactory) { + lore.add("§7" + Lang.ClickShiftRight + " > §e" + Lang.setLevel); + } else { + lore.add("§8§n" + Lang.ClickShiftRight + " > " + Lang.setLevel); + } + ItemStack item = ItemUtils.item(mob.getMobItem(), mob.getName(), lore); + item.setAmount(Math.min(amount, 64)); + return item; + } @Override public CloseBehavior onClose(Player p, Inventory inv) { diff --git a/core/src/main/java/fr/skytasul/quests/gui/npc/NPCGUI.java b/core/src/main/java/fr/skytasul/quests/gui/npc/NPCGUI.java index 5e0e7203..5a07ef14 100644 --- a/core/src/main/java/fr/skytasul/quests/gui/npc/NPCGUI.java +++ b/core/src/main/java/fr/skytasul/quests/gui/npc/NPCGUI.java @@ -70,7 +70,7 @@ private void setType(EntityType type) { this.en = type; if (en == EntityType.PLAYER) { inv.setItem(5, ItemUtils.skull(Lang.type.toString(), null, Lang.optionValue.format("player"))); - }else inv.setItem(5, ItemUtils.item(XMaterial.mobItem(en), Lang.type.toString(), Lang.optionValue.format(en.getName()))); + }else inv.setItem(5, ItemUtils.item(Utils.mobItem(en), Lang.type.toString(), Lang.optionValue.format(en.getName()))); } private void setSkin(String skin) { diff --git a/core/src/main/java/fr/skytasul/quests/gui/particles/ParticleEffectGUI.java b/core/src/main/java/fr/skytasul/quests/gui/particles/ParticleEffectGUI.java new file mode 100644 index 00000000..132d675e --- /dev/null +++ b/core/src/main/java/fr/skytasul/quests/gui/particles/ParticleEffectGUI.java @@ -0,0 +1,144 @@ +package fr.skytasul.quests.gui.particles; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import org.bukkit.Bukkit; +import org.bukkit.Color; +import org.bukkit.Particle; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.ClickType; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; + +import fr.skytasul.quests.editors.TextEditor; +import fr.skytasul.quests.editors.checkers.ColorParser; +import fr.skytasul.quests.gui.CustomInventory; +import fr.skytasul.quests.gui.ItemUtils; +import fr.skytasul.quests.utils.Lang; +import fr.skytasul.quests.utils.ParticleEffect; +import fr.skytasul.quests.utils.ParticleEffect.ParticleShape; +import fr.skytasul.quests.utils.Utils; +import fr.skytasul.quests.utils.XMaterial; +import fr.skytasul.quests.utils.compatibility.Post1_13; +import fr.skytasul.quests.utils.nms.NMS; + +public class ParticleEffectGUI implements CustomInventory { + + private static final int SLOT_SHAPE = 1; + private static final int SLOT_PARTICLE = 3; + private static final int SLOT_COLOR = 4; + private static final int SLOT_CANCEL = 7; + private static final int SLOT_FINISH = 8; + + static final List PARTICLES = Arrays.stream(Particle.values()).filter(particle -> { + if (particle.getDataType() == Void.class) return true; + if (NMS.getMCVersion() >= 13) return particle.getDataType() == Post1_13.getDustOptionClass(); + return false; + }).collect(Collectors.toList()); + + private final Consumer end; + + private Particle particle; + private ParticleShape shape; + private Color color; + + private Inventory inv; + + public ParticleEffectGUI(Consumer end) { + this(end, Particle.FLAME, ParticleShape.POINT, Color.AQUA); + } + + public ParticleEffectGUI(Consumer end, ParticleEffect effect) { + this(end, effect.getParticle(), effect.getShape(), effect.getColor()); + } + + public ParticleEffectGUI(Consumer end, Particle particle, ParticleShape shape, Color color) { + this.end = end; + this.particle = particle; + this.shape = shape; + this.color = color; + } + + @Override + public Inventory open(Player p) { + if (inv == null) { + inv = Bukkit.createInventory(null, 9, Lang.INVENTORY_PARTICLE_EFFECT.toString()); + + inv.setItem(SLOT_SHAPE, ItemUtils.item(XMaterial.FIREWORK_STAR, Lang.particle_shape.toString(), Lang.optionValue.format(shape))); + inv.setItem(SLOT_PARTICLE, ItemUtils.item(XMaterial.PAPER, Lang.particle_type.toString(), Lang.optionValue.format(particle))); + if (ParticleEffect.canHaveColor(particle)) setColorItem(); + + inv.setItem(SLOT_CANCEL, ItemUtils.itemCancel); + inv.setItem(SLOT_FINISH, ItemUtils.itemDone); + } + return inv = p.openInventory(inv).getTopInventory(); + } + + @Override + public CloseBehavior onClose(Player p, Inventory inv) { + Utils.runSync(() -> end.accept(null)); + return CloseBehavior.REMOVE; + } + + @Override + public boolean onClick(Player p, Inventory inv, ItemStack current, int slot, ClickType click) { + switch (slot) { + + case SLOT_SHAPE: + List shapes = Arrays.asList(ParticleShape.values()); + int index = shapes.indexOf(shape); + shape = shapes.get(index == shapes.size() - 1 ? 0 : (index + 1)); + ItemUtils.lore(current, Lang.optionValue.format(shape)); + break; + + case SLOT_PARTICLE: + new ParticleListGUI(existing -> { + if (existing != null) { + particle = existing; + ItemUtils.lore(current, Lang.optionValue.format(particle)); + if (ParticleEffect.canHaveColor(existing)) { + setColorItem(); + }else { + inv.setItem(SLOT_COLOR, null); + } + } + ParticleEffectGUI.this.create(p); + }).allowCancel().create(p); + break; + + case SLOT_COLOR: + if (ParticleEffect.canHaveColor(particle)) { + Runnable reopen = () -> open(p); + Lang.COLOR_EDITOR.send(p); + new TextEditor<>(p, reopen, newColor -> { + color = newColor; + ItemUtils.lore(current, getColorLore()); + reopen.run(); + }, ColorParser.PARSER).enter(); + } + break; + + case SLOT_CANCEL: + end.accept(null); + break; + + case SLOT_FINISH: + end.accept(new ParticleEffect(particle, shape, color)); + break; + } + return true; + } + + private void setColorItem() { + if (color == null) color = Color.RED; + inv.setItem(SLOT_COLOR, ItemUtils.item(XMaterial.MAGENTA_DYE, Lang.particle_color.toString(), getColorLore())); + } + + private String[] getColorLore() { + return new String[] { Lang.optionValue.format("RGB: " + color.getRed() + " " + color.getGreen() + " " + color.getBlue()) }; + } + +} diff --git a/core/src/main/java/fr/skytasul/quests/gui/particles/ParticleListGUI.java b/core/src/main/java/fr/skytasul/quests/gui/particles/ParticleListGUI.java new file mode 100644 index 00000000..23aa122b --- /dev/null +++ b/core/src/main/java/fr/skytasul/quests/gui/particles/ParticleListGUI.java @@ -0,0 +1,33 @@ +package fr.skytasul.quests.gui.particles; + +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.bukkit.DyeColor; +import org.bukkit.Particle; +import org.bukkit.inventory.ItemStack; + +import fr.skytasul.quests.api.options.QuestOption; +import fr.skytasul.quests.gui.ItemUtils; +import fr.skytasul.quests.gui.templates.StaticPagedGUI; +import fr.skytasul.quests.utils.Lang; +import fr.skytasul.quests.utils.ParticleEffect; +import fr.skytasul.quests.utils.XMaterial; + +public class ParticleListGUI extends StaticPagedGUI { + + private static final Map PARTICLES = ParticleEffectGUI.PARTICLES + .stream().collect(Collectors.toMap(Function.identity(), particle -> { + boolean colorable = ParticleEffect.canHaveColor(particle); + String[] lore = colorable ? new String[] { QuestOption.formatDescription(Lang.particle_colored.toString()) } : new String[0]; + return ItemUtils.item(colorable ? XMaterial.MAP : XMaterial.PAPER, "§e" + particle.name(), lore); + })); + + public ParticleListGUI(Consumer end) { + super(Lang.INVENTORY_PARTICLE_LIST.toString(), DyeColor.MAGENTA, PARTICLES, end, Particle::name); + sortValuesByName(); + } + +} \ No newline at end of file diff --git a/core/src/main/java/fr/skytasul/quests/gui/quests/DialogHistoryGUI.java b/core/src/main/java/fr/skytasul/quests/gui/quests/DialogHistoryGUI.java index 1ba1d779..ade56677 100644 --- a/core/src/main/java/fr/skytasul/quests/gui/quests/DialogHistoryGUI.java +++ b/core/src/main/java/fr/skytasul/quests/gui/quests/DialogHistoryGUI.java @@ -16,7 +16,7 @@ import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; -import fr.skytasul.quests.api.stages.Dialogable; +import fr.skytasul.quests.api.stages.types.Dialogable; import fr.skytasul.quests.gui.quests.DialogHistoryGUI.WrappedDialogable; import fr.skytasul.quests.gui.templates.PagedGUI; import fr.skytasul.quests.options.OptionStartDialog; diff --git a/core/src/main/java/fr/skytasul/quests/gui/quests/PlayerListGUI.java b/core/src/main/java/fr/skytasul/quests/gui/quests/PlayerListGUI.java index 4b141d26..49b57960 100644 --- a/core/src/main/java/fr/skytasul/quests/gui/quests/PlayerListGUI.java +++ b/core/src/main/java/fr/skytasul/quests/gui/quests/PlayerListGUI.java @@ -1,7 +1,7 @@ package fr.skytasul.quests.gui.quests; -import java.util.ArrayList; import java.util.List; +import java.util.function.Function; import java.util.stream.Collectors; import org.bukkit.Bukkit; @@ -16,6 +16,7 @@ import fr.skytasul.quests.BeautyQuests; import fr.skytasul.quests.QuestsConfiguration; import fr.skytasul.quests.api.QuestsAPI; +import fr.skytasul.quests.api.options.description.QuestDescriptionContext; import fr.skytasul.quests.gui.CustomInventory; import fr.skytasul.quests.gui.Inventories; import fr.skytasul.quests.gui.ItemUtils; @@ -96,67 +97,35 @@ private void setItems(){ switch (cat){ case FINISHED: - setQuests(QuestsAPI.getQuests().getQuestsFinished(acc, hide)); - for (int i = page * 35; i < quests.size(); i++){ - if (i == (page + 1) * 35) break; - Quest qu = quests.get(i); - List lore = new ArrayList<>(4); - if (qu.isRepeatable()){ - if (qu.testTimer(acc, false)) { - lore.add(Lang.canRedo.toString()); - }else { - lore.add(Lang.timeWait.format(qu.getTimeLeft(acc))); - } - lore.add(null); - lore.add(Lang.timesFinished.format(acc.getQuestDatas(qu).getTimesFinished())); - } + displayQuests(QuestsAPI.getQuests().getQuestsFinished(acc, hide), qu -> { + List lore = new QuestDescriptionContext(QuestsConfiguration.getQuestDescription(), qu, acc, cat, Source.MENU).formatDescription(); if (QuestsConfiguration.getDialogsConfig().isHistoryEnabled() && acc.getQuestDatas(qu).hasFlowDialogs()) { if (!lore.isEmpty()) lore.add(null); lore.add("§8" + Lang.ClickRight + " §8> " + Lang.dialogsHistoryLore); } - setMainItem(i - page * 35, createQuestItem(qu, lore)); - } + return createQuestItem(qu, lore); + }); break; case IN_PROGRESS: - setQuests(QuestsAPI.getQuests().getQuestsStarted(acc)); - for (int i = page * 35; i < quests.size(); i++){ - if (i == (page + 1) * 35) break; - Quest qu = quests.get(i); - ItemStack item; - try { - String desc = qu.getDescriptionLine(acc, Source.MENU); - List lore = new ArrayList<>(4); - if (desc != null && !desc.isEmpty()) lore.add(desc); - boolean hasDialogs = QuestsConfiguration.getDialogsConfig().isHistoryEnabled() && acc.getQuestDatas(qu).hasFlowDialogs(); - boolean cancellable = QuestsConfiguration.getMenuConfig().allowPlayerCancelQuest() && qu.isCancellable(); - if (cancellable || hasDialogs) { - if (!lore.isEmpty()) lore.add(null); - if (cancellable) lore.add("§8" + Lang.ClickLeft + " §8> " + Lang.cancelLore); - if (hasDialogs) lore.add("§8" + Lang.ClickRight + " §8> " + Lang.dialogsHistoryLore); - } - item = createQuestItem(qu, lore); - }catch (Exception ex) { - item = ItemUtils.item(XMaterial.BARRIER, "§cError - Quest #" + qu.getID()); - BeautyQuests.logger.severe("An error ocurred when creating item of quest " + qu.getID() + " for account " + acc.abstractAcc.getIdentifier(), ex); + displayQuests(QuestsAPI.getQuests().getQuestsStarted(acc, true, false), qu -> { + List lore = new QuestDescriptionContext(QuestsConfiguration.getQuestDescription(), qu, acc, cat, Source.MENU).formatDescription(); + + boolean hasDialogs = QuestsConfiguration.getDialogsConfig().isHistoryEnabled() && acc.getQuestDatas(qu).hasFlowDialogs(); + boolean cancellable = QuestsConfiguration.getMenuConfig().allowPlayerCancelQuest() && qu.isCancellable(); + if (cancellable || hasDialogs) { + if (!lore.isEmpty()) lore.add(null); + if (cancellable) lore.add("§8" + Lang.ClickLeft + " §8> " + Lang.cancelLore); + if (hasDialogs) lore.add("§8" + Lang.ClickRight + " §8> " + Lang.dialogsHistoryLore); } - setMainItem(i - page * 35, item); - } + return createQuestItem(qu, lore); + }); break; case NOT_STARTED: - setQuests(QuestsAPI.getQuests().getQuestsNotStarted(acc, hide, true).stream().filter(quest -> !quest.isHiddenWhenRequirementsNotMet() || quest.isLauncheable(acc.getPlayer(), acc, false)).collect(Collectors.toList())); - for (int i = page * 35; i < quests.size(); i++){ - if (i == (page + 1) * 35) break; - Quest qu = quests.get(i); - List lore = new ArrayList<>(5); - lore.addAll(QuestsConfiguration.getQuestDescription().formatDescription(qu, acc.getPlayer())); - if (qu.getOptionValueOrDef(OptionStartable.class) && acc.isCurrent()) { - lore.add(""); - lore.add(qu.isLauncheable(acc.getPlayer(), acc, false) ? Lang.startLore.toString() : Lang.startImpossibleLore.toString()); - } - setMainItem(i - page * 35, createQuestItem(qu, lore)); - } + displayQuests(QuestsAPI.getQuests().getQuestsNotStarted(acc, hide, true).stream().filter(quest -> !quest.isHiddenWhenRequirementsNotMet() || quest.isLauncheable(acc.getPlayer(), acc, false)).collect(Collectors.toList()), qu -> { + return createQuestItem(qu, new QuestDescriptionContext(QuestsConfiguration.getQuestDescription(), qu, acc, cat, Source.MENU).formatDescription()); + }); break; default: @@ -164,6 +133,22 @@ private void setItems(){ } } + private void displayQuests(List quests, Function itemProvider) { + setQuests(quests); + for (int i = page * 35; i < quests.size(); i++) { + if (i == (page + 1) * 35) break; + Quest qu = quests.get(i); + ItemStack item; + try { + item = itemProvider.apply(qu); + }catch (Exception ex) { + item = ItemUtils.item(XMaterial.BARRIER, "§cError - Quest #" + qu.getID()); + BeautyQuests.logger.severe("An error ocurred when creating item of quest " + qu.getID() + " for account " + acc.abstractAcc.getIdentifier(), ex); + } + setMainItem(i - page * 35, item); + } + } + private int setMainItem(int mainSlot, ItemStack is){ int line = (int) Math.floor(mainSlot * 1.0 / 7.0); int slot = mainSlot + (2 * line); diff --git a/core/src/main/java/fr/skytasul/quests/gui/quests/QuestsListGUI.java b/core/src/main/java/fr/skytasul/quests/gui/quests/QuestsListGUI.java index ebfd2e1c..e82204a1 100644 --- a/core/src/main/java/fr/skytasul/quests/gui/quests/QuestsListGUI.java +++ b/core/src/main/java/fr/skytasul/quests/gui/quests/QuestsListGUI.java @@ -22,10 +22,10 @@ public class QuestsListGUI extends PagedGUI { public Consumer run; public QuestsListGUI(Consumer run, PlayerAccount acc, boolean started, boolean notStarted, boolean finished){ - super(Lang.INVENTORY_QUESTS_LIST.toString(), DyeColor.CYAN, new ArrayList<>(), null, x -> x.getName()); + super(Lang.INVENTORY_QUESTS_LIST.toString(), DyeColor.CYAN, new ArrayList<>(), null, Quest::getName); if (acc != null){ if (started) super.objects.addAll(QuestsAPI.getQuests().getQuestsStarted(acc)); - if (notStarted) super.objects.addAll(QuestsAPI.getQuests().getQuestsNotStarted(acc, false, false)); + if (notStarted) super.objects.addAll(QuestsAPI.getQuests().getQuestsNotStarted(acc, false, true)); if (finished) super.objects.addAll(QuestsAPI.getQuests().getQuestsFinished(acc, false)); }else super.objects.addAll(QuestsAPI.getQuests().getQuests()); this.run = run; diff --git a/core/src/main/java/fr/skytasul/quests/gui/templates/PagedGUI.java b/core/src/main/java/fr/skytasul/quests/gui/templates/PagedGUI.java index a390a11b..46029b92 100644 --- a/core/src/main/java/fr/skytasul/quests/gui/templates/PagedGUI.java +++ b/core/src/main/java/fr/skytasul/quests/gui/templates/PagedGUI.java @@ -6,6 +6,7 @@ import java.util.function.Consumer; import java.util.function.Function; +import org.apache.commons.lang.Validate; import org.bukkit.Bukkit; import org.bukkit.DyeColor; import org.bukkit.entity.Player; @@ -41,7 +42,7 @@ public abstract class PagedGUI implements CustomInventory { protected List objects; protected Consumer> validate; private ItemStack validationItem = ItemUtils.itemDone; - private LevenshteinComparator comparator; + protected LevenshteinComparator comparator; protected PagedGUI(String name, DyeColor color, Collection objects) { this(name, color, objects, null, null); @@ -84,6 +85,12 @@ public PagedGUI setValidate(Consumer> validate, ItemStack validationI return this; } + public PagedGUI sortValuesByName() { + Validate.notNull(comparator); + sortValues(comparator.getFunction()); + return this; + } + public > PagedGUI sortValues(Function mapper) { objects.sort((o1, o2) -> { C map1; diff --git a/core/src/main/java/fr/skytasul/quests/gui/templates/StaticPagedGUI.java b/core/src/main/java/fr/skytasul/quests/gui/templates/StaticPagedGUI.java new file mode 100644 index 00000000..5129fb80 --- /dev/null +++ b/core/src/main/java/fr/skytasul/quests/gui/templates/StaticPagedGUI.java @@ -0,0 +1,51 @@ +package fr.skytasul.quests.gui.templates; + +import java.util.Map; +import java.util.Map.Entry; +import java.util.function.Consumer; +import java.util.function.Function; + +import org.bukkit.DyeColor; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.ClickType; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; + +import fr.skytasul.quests.utils.Utils; + +public class StaticPagedGUI extends PagedGUI> { + + protected final Consumer clicked; + private boolean cancelAllowed = false; + + public StaticPagedGUI(String name, DyeColor color, Map objects, Consumer clicked, Function nameMapper) { + super(name, color, objects.entrySet(), null, nameMapper == null ? null : entry -> nameMapper.apply(entry.getKey())); + this.clicked = clicked; + } + + public StaticPagedGUI allowCancel() { + cancelAllowed = true; + return this; + } + + @Override + public ItemStack getItemStack(Entry object) { + return object.getValue(); + } + + @Override + public void click(Entry existing, ItemStack item, ClickType clickType) { + clicked.accept(existing.getKey()); + } + + @Override + public CloseBehavior onClose(Player p, Inventory inv) { + if (cancelAllowed) { + Utils.runSync(() -> clicked.accept(null)); + return CloseBehavior.NOTHING; + }else { + return CloseBehavior.REOPEN; + } + } + +} diff --git a/core/src/main/java/fr/skytasul/quests/options/OptionDescription.java b/core/src/main/java/fr/skytasul/quests/options/OptionDescription.java index 63c0d52d..cc541e45 100644 --- a/core/src/main/java/fr/skytasul/quests/options/OptionDescription.java +++ b/core/src/main/java/fr/skytasul/quests/options/OptionDescription.java @@ -1,12 +1,23 @@ package fr.skytasul.quests.options; +import java.util.Arrays; +import java.util.List; import org.bukkit.entity.Player; - import fr.skytasul.quests.api.options.QuestOptionString; +import fr.skytasul.quests.api.options.description.QuestDescriptionContext; +import fr.skytasul.quests.api.options.description.QuestDescriptionProvider; import fr.skytasul.quests.utils.Lang; import fr.skytasul.quests.utils.XMaterial; -public class OptionDescription extends QuestOptionString { +public class OptionDescription extends QuestOptionString implements QuestDescriptionProvider { + + private List cachedDescription; + + @Override + public void setValue(String value) { + super.setValue(value); + cachedDescription = null; + } @Override public void sendIndication(Player p) { @@ -33,4 +44,20 @@ public boolean isMultiline() { return true; } + @Override + public List provideDescription(QuestDescriptionContext context) { + if (cachedDescription == null) cachedDescription = Arrays.asList("§7" + getValue()); + return cachedDescription; + } + + @Override + public String getDescriptionId() { + return "description"; + } + + @Override + public double getDescriptionPriority() { + return 0; + } + } diff --git a/core/src/main/java/fr/skytasul/quests/options/OptionEndRewards.java b/core/src/main/java/fr/skytasul/quests/options/OptionEndRewards.java index 53338c90..3c266836 100644 --- a/core/src/main/java/fr/skytasul/quests/options/OptionEndRewards.java +++ b/core/src/main/java/fr/skytasul/quests/options/OptionEndRewards.java @@ -1,11 +1,21 @@ package fr.skytasul.quests.options; +import java.util.List; +import java.util.Objects; +import java.util.regex.Pattern; +import java.util.stream.Collectors; import fr.skytasul.quests.api.options.QuestOptionRewards; +import fr.skytasul.quests.api.options.description.QuestDescriptionContext; +import fr.skytasul.quests.api.options.description.QuestDescriptionProvider; import fr.skytasul.quests.api.rewards.AbstractReward; +import fr.skytasul.quests.gui.quests.PlayerListGUI.Category; import fr.skytasul.quests.utils.Lang; +import fr.skytasul.quests.utils.Utils; import fr.skytasul.quests.utils.XMaterial; -public class OptionEndRewards extends QuestOptionRewards { +public class OptionEndRewards extends QuestOptionRewards implements QuestDescriptionProvider { + + private static final Pattern SPLIT_PATTERN = Pattern.compile("\\{JOIN\\}"); @Override protected void attachedAsyncReward(AbstractReward reward) { @@ -26,5 +36,34 @@ public String getItemName() { public String getItemDescription() { return Lang.rewardsLore.toString(); } + + @Override + public List provideDescription(QuestDescriptionContext context) { + if (!context.getPlayerAccount().isCurrent()) return null; + if (!context.getDescriptionOptions().showRewards()) return null; + if (context.getCategory() == Category.FINISHED) return null; + + List rewards = getValue().stream() + .map(x -> x.getDescription(context.getPlayerAccount().getPlayer())) + .filter(Objects::nonNull) + .flatMap(SPLIT_PATTERN::splitAsStream) + .filter(x -> !x.isEmpty()) + .map(x -> Utils.format(context.getDescriptionOptions().getRewardsFormat(), x)) + .collect(Collectors.toList()); + if (rewards.isEmpty()) return null; + + rewards.add(0, Lang.RWDTitle.toString()); + return rewards; + } + + @Override + public String getDescriptionId() { + return "rewards"; + } + + @Override + public double getDescriptionPriority() { + return 10; + } } diff --git a/core/src/main/java/fr/skytasul/quests/options/OptionFailOnDeath.java b/core/src/main/java/fr/skytasul/quests/options/OptionFailOnDeath.java index 45046482..4b9320d9 100644 --- a/core/src/main/java/fr/skytasul/quests/options/OptionFailOnDeath.java +++ b/core/src/main/java/fr/skytasul/quests/options/OptionFailOnDeath.java @@ -1,16 +1,12 @@ package fr.skytasul.quests.options; -import org.bukkit.Bukkit; import org.bukkit.event.EventHandler; -import org.bukkit.event.HandlerList; import org.bukkit.event.Listener; import org.bukkit.event.entity.PlayerDeathEvent; -import fr.skytasul.quests.BeautyQuests; import fr.skytasul.quests.api.options.QuestOptionBoolean; import fr.skytasul.quests.players.PlayerAccount; import fr.skytasul.quests.players.PlayersManager; -import fr.skytasul.quests.structure.Quest; import fr.skytasul.quests.utils.Lang; public class OptionFailOnDeath extends QuestOptionBoolean implements Listener { @@ -25,18 +21,6 @@ public String getDescription() { return Lang.failOnDeathLore.toString(); } - @Override - public void attach(Quest quest) { - super.attach(quest); - Bukkit.getPluginManager().registerEvents(this, BeautyQuests.getInstance()); - } - - @Override - public void detach() { - super.detach(); - HandlerList.unregisterAll(this); - } - @EventHandler public void onDeath(PlayerDeathEvent e) { PlayerAccount acc = PlayersManager.getPlayerAccount(e.getEntity()); diff --git a/core/src/main/java/fr/skytasul/quests/options/OptionHide.java b/core/src/main/java/fr/skytasul/quests/options/OptionHide.java deleted file mode 100644 index 07ef87fb..00000000 --- a/core/src/main/java/fr/skytasul/quests/options/OptionHide.java +++ /dev/null @@ -1,18 +0,0 @@ -package fr.skytasul.quests.options; - -import fr.skytasul.quests.api.options.QuestOptionBoolean; -import fr.skytasul.quests.utils.Lang; - -public class OptionHide extends QuestOptionBoolean { - - @Override - public String getName() { - return Lang.hide.toString(); - } - - @Override - public String getDescription() { - return Lang.hideLore.toString(); - } - -} diff --git a/core/src/main/java/fr/skytasul/quests/options/OptionQuestItem.java b/core/src/main/java/fr/skytasul/quests/options/OptionQuestItem.java index 1202b6ad..bee8df4f 100644 --- a/core/src/main/java/fr/skytasul/quests/options/OptionQuestItem.java +++ b/core/src/main/java/fr/skytasul/quests/options/OptionQuestItem.java @@ -64,8 +64,15 @@ public void click(FinishGUI gui, Player p, ItemStack item, int slot, ClickType c setValue(obj.parseItem()); } gui.inv.setItem(slot, ItemUtils.nameAndLore(getValue().clone(), Lang.customMaterial.toString(), getLore())); + ItemStack setItem = gui.inv.getItem(slot); + if (setItem == null || setItem.getType() == Material.AIR) { + // means that the material cannot be treated as an inventory item (ex: fire) + resetValue(); + Lang.INVALID_ITEM_TYPE.send(p); + gui.inv.setItem(slot, ItemUtils.nameAndLore(getValue().clone(), Lang.customMaterial.toString(), getLore())); + } gui.reopen(p); - }, new MaterialParser(false, false)).passNullIntoEndConsumer().enter(); + }, MaterialParser.ANY_PARSER).passNullIntoEndConsumer().enter(); } @Override diff --git a/core/src/main/java/fr/skytasul/quests/options/OptionQuestPool.java b/core/src/main/java/fr/skytasul/quests/options/OptionQuestPool.java index 594f57ec..d4b2e6cd 100644 --- a/core/src/main/java/fr/skytasul/quests/options/OptionQuestPool.java +++ b/core/src/main/java/fr/skytasul/quests/options/OptionQuestPool.java @@ -1,5 +1,8 @@ package fr.skytasul.quests.options; +import java.util.ArrayList; +import java.util.List; + import org.bukkit.Bukkit; import org.bukkit.DyeColor; import org.bukkit.configuration.ConfigurationSection; @@ -48,8 +51,16 @@ public QuestPool cloneValue(QuestPool value) { return value; } - private String[] getLore() { - return new String[] { formatDescription(Lang.questPoolLore.toString()), "", formatValue(getValue() == null ? null : "#" + getValue().getID()) }; + private List getLore() { + List lore = new ArrayList<>(5); + lore.add(formatDescription(Lang.questPoolLore.toString())); + lore.add(""); + lore.add(formatValue(getValue() == null ? null : "#" + getValue().getID())); + if (hasCustomValue()) { + lore.add(""); + lore.add("§8" + Lang.ClickShiftRight.toString() + " > §d" + Lang.Reset.toString()); + } + return lore; } @Override @@ -59,26 +70,32 @@ public ItemStack getItemStack(OptionSet options) { @Override public void click(FinishGUI gui, Player p, ItemStack item, int slot, ClickType click) { - new PagedGUI(Lang.INVENTORY_POOLS_LIST.toString(), DyeColor.CYAN, BeautyQuests.getInstance().getPoolsManager().getPools(), list -> gui.reopen(p), null) { - - @Override - public ItemStack getItemStack(QuestPool object) { - return object.getItemStack(Lang.poolChoose.toString()); - } - - @Override - public void click(QuestPool existing, ItemStack poolItem, ClickType click) { - setValue(existing); - ItemUtils.lore(item, getLore()); - gui.reopen(p); - } - - @Override - public CloseBehavior onClose(Player p, Inventory inv) { - Bukkit.getScheduler().runTask(BeautyQuests.getInstance(), () -> gui.reopen(p)); - return CloseBehavior.NOTHING; - } - }.create(p); + if (click == ClickType.SHIFT_RIGHT) { + setValue(null); + ItemUtils.lore(item, getLore()); + gui.reopen(p); + }else { + new PagedGUI(Lang.INVENTORY_POOLS_LIST.toString(), DyeColor.CYAN, BeautyQuests.getInstance().getPoolsManager().getPools(), list -> gui.reopen(p), null) { + + @Override + public ItemStack getItemStack(QuestPool object) { + return object.getItemStack(Lang.poolChoose.toString()); + } + + @Override + public void click(QuestPool existing, ItemStack poolItem, ClickType click) { + setValue(existing); + ItemUtils.lore(item, getLore()); + gui.reopen(p); + } + + @Override + public CloseBehavior onClose(Player p, Inventory inv) { + Bukkit.getScheduler().runTask(BeautyQuests.getInstance(), () -> gui.reopen(p)); + return CloseBehavior.NOTHING; + } + }.create(p); + } } } diff --git a/core/src/main/java/fr/skytasul/quests/options/OptionRepeatable.java b/core/src/main/java/fr/skytasul/quests/options/OptionRepeatable.java index 24c5f234..5fea8a23 100644 --- a/core/src/main/java/fr/skytasul/quests/options/OptionRepeatable.java +++ b/core/src/main/java/fr/skytasul/quests/options/OptionRepeatable.java @@ -1,9 +1,14 @@ package fr.skytasul.quests.options; +import java.util.ArrayList; +import java.util.List; import fr.skytasul.quests.api.options.QuestOptionBoolean; +import fr.skytasul.quests.api.options.description.QuestDescriptionContext; +import fr.skytasul.quests.api.options.description.QuestDescriptionProvider; +import fr.skytasul.quests.gui.quests.PlayerListGUI.Category; import fr.skytasul.quests.utils.Lang; -public class OptionRepeatable extends QuestOptionBoolean { +public class OptionRepeatable extends QuestOptionBoolean implements QuestDescriptionProvider { @Override public String getName() { @@ -15,4 +20,29 @@ public String getDescription() { return Lang.multipleLore.toString(); } + @Override + public List provideDescription(QuestDescriptionContext context) { + if (context.getCategory() != Category.FINISHED) return null; + + List lore = new ArrayList<>(4); + if (context.getQuest().testTimer(context.getPlayerAccount(), false)) { + lore.add(Lang.canRedo.toString()); + }else { + lore.add(Lang.timeWait.format(context.getQuest().getTimeLeft(context.getPlayerAccount()))); + } + lore.add(null); + lore.add(Lang.timesFinished.format(context.getQuestDatas().getTimesFinished())); + return lore; + } + + @Override + public String getDescriptionId() { + return "repeatable"; + } + + @Override + public double getDescriptionPriority() { + return 100; + } + } diff --git a/core/src/main/java/fr/skytasul/quests/options/OptionRequirements.java b/core/src/main/java/fr/skytasul/quests/options/OptionRequirements.java index df538d01..92e0a81b 100644 --- a/core/src/main/java/fr/skytasul/quests/options/OptionRequirements.java +++ b/core/src/main/java/fr/skytasul/quests/options/OptionRequirements.java @@ -1,22 +1,22 @@ package fr.skytasul.quests.options; +import java.util.List; import java.util.Map; -import java.util.function.Function; - +import java.util.Objects; +import java.util.stream.Collectors; import fr.skytasul.quests.api.QuestsAPI; import fr.skytasul.quests.api.objects.QuestObjectsRegistry; import fr.skytasul.quests.api.options.QuestOptionObject; +import fr.skytasul.quests.api.options.description.QuestDescriptionContext; +import fr.skytasul.quests.api.options.description.QuestDescriptionProvider; import fr.skytasul.quests.api.requirements.AbstractRequirement; import fr.skytasul.quests.api.requirements.RequirementCreator; +import fr.skytasul.quests.gui.quests.PlayerListGUI.Category; import fr.skytasul.quests.utils.Lang; +import fr.skytasul.quests.utils.Utils; import fr.skytasul.quests.utils.XMaterial; -public class OptionRequirements extends QuestOptionObject { - - @Override - protected Function> getSerializeFunction() { - return AbstractRequirement::serialize; - } +public class OptionRequirements extends QuestOptionObject implements QuestDescriptionProvider { @Override protected AbstractRequirement deserialize(Map map) { @@ -48,4 +48,34 @@ public String getItemDescription() { return Lang.editRequirementsLore.toString(); } + @Override + public List provideDescription(QuestDescriptionContext context) { + if (!context.getPlayerAccount().isCurrent()) return null; + if (!context.getDescriptionOptions().showRequirements()) return null; + if (context.getCategory() != Category.NOT_STARTED) return null; + + List requirements = getValue().stream() + .map(x -> { + String description = x.getDescription(context.getPlayerAccount().getPlayer()); + if (description != null) description = Utils.format(x.test(context.getPlayerAccount().getPlayer()) ? context.getDescriptionOptions().getRequirementsValid() : context.getDescriptionOptions().getRequirementsInvalid(), description); + return description; + }) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + if (requirements.isEmpty()) return null; + + requirements.add(0, Lang.RDTitle.toString()); + return requirements; + } + + @Override + public String getDescriptionId() { + return "requirements"; + } + + @Override + public double getDescriptionPriority() { + return 30; + } + } diff --git a/core/src/main/java/fr/skytasul/quests/options/OptionStartDialog.java b/core/src/main/java/fr/skytasul/quests/options/OptionStartDialog.java index b664be02..3aba0cd1 100644 --- a/core/src/main/java/fr/skytasul/quests/options/OptionStartDialog.java +++ b/core/src/main/java/fr/skytasul/quests/options/OptionStartDialog.java @@ -9,7 +9,7 @@ import fr.skytasul.quests.api.npcs.BQNPC; import fr.skytasul.quests.api.options.OptionSet; import fr.skytasul.quests.api.options.QuestOption; -import fr.skytasul.quests.api.stages.Dialogable; +import fr.skytasul.quests.api.stages.types.Dialogable; import fr.skytasul.quests.editors.DialogEditor; import fr.skytasul.quests.gui.ItemUtils; import fr.skytasul.quests.gui.creation.FinishGUI; @@ -50,7 +50,7 @@ public boolean shouldDisplay(OptionSet options) { } private String[] getLore() { - return new String[] { formatDescription(Lang.startDialogLore.toString()), "", getValue() == null ? Lang.NotSet.toString() : "§7" + getValue().messages.size() + " line(s)" }; + return new String[] { formatDescription(Lang.startDialogLore.toString()), "", getValue() == null ? Lang.NotSet.toString() : "§7" + Lang.AmountDialogLines.format(getValue().messages.size()) }; } @Override diff --git a/core/src/main/java/fr/skytasul/quests/options/OptionStartable.java b/core/src/main/java/fr/skytasul/quests/options/OptionStartable.java index bd434a6a..835a2229 100644 --- a/core/src/main/java/fr/skytasul/quests/options/OptionStartable.java +++ b/core/src/main/java/fr/skytasul/quests/options/OptionStartable.java @@ -1,9 +1,17 @@ package fr.skytasul.quests.options; +import java.util.Arrays; +import java.util.List; import fr.skytasul.quests.api.options.QuestOptionBoolean; +import fr.skytasul.quests.api.options.description.QuestDescriptionContext; +import fr.skytasul.quests.api.options.description.QuestDescriptionProvider; +import fr.skytasul.quests.gui.quests.PlayerListGUI.Category; import fr.skytasul.quests.utils.Lang; -public class OptionStartable extends QuestOptionBoolean { +public class OptionStartable extends QuestOptionBoolean implements QuestDescriptionProvider { + + private static final List STARTABLE = Arrays.asList(Lang.startLore.toString()); + private static final List NOT_STARTABLE = Arrays.asList(Lang.startImpossibleLore.toString()); @Override public String getName() { @@ -15,4 +23,20 @@ public String getDescription() { return Lang.startableFromGUILore.toString(); } + @Override + public List provideDescription(QuestDescriptionContext context) { + if (context.getCategory() != Category.NOT_STARTED || !context.getPlayerAccount().isCurrent()) return null; + return context.getQuest().isLauncheable(context.getPlayerAccount().getPlayer(), context.getPlayerAccount(), false) ? STARTABLE : NOT_STARTABLE; + } + + @Override + public String getDescriptionId() { + return "startable_from_gui"; + } + + @Override + public double getDescriptionPriority() { + return 100; + } + } diff --git a/core/src/main/java/fr/skytasul/quests/options/OptionVisibility.java b/core/src/main/java/fr/skytasul/quests/options/OptionVisibility.java new file mode 100644 index 00000000..4d95e0ff --- /dev/null +++ b/core/src/main/java/fr/skytasul/quests/options/OptionVisibility.java @@ -0,0 +1,127 @@ +package fr.skytasul.quests.options; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumMap; +import java.util.List; +import java.util.Map.Entry; +import java.util.stream.Collectors; + +import org.bukkit.Bukkit; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.ClickType; +import org.bukkit.event.inventory.InventoryType; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; + +import fr.skytasul.quests.api.options.OptionSet; +import fr.skytasul.quests.api.options.QuestOption; +import fr.skytasul.quests.gui.CustomInventory; +import fr.skytasul.quests.gui.ItemUtils; +import fr.skytasul.quests.gui.creation.FinishGUI; +import fr.skytasul.quests.options.OptionVisibility.VisibilityLocation; +import fr.skytasul.quests.utils.Lang; +import fr.skytasul.quests.utils.Utils; +import fr.skytasul.quests.utils.XMaterial; + +public class OptionVisibility extends QuestOption> { + + @Override + public Object save() { + return getValue().stream().map(VisibilityLocation::name).collect(Collectors.toList()); + } + + @Override + public void load(ConfigurationSection config, String key) { + if (config.isBoolean(key)) { + setValue(Collections.emptyList()); // migration from before 0.20, where it was the "hide" option + }else { + setValue(config.getStringList(key).stream().map(VisibilityLocation::valueOf).collect(Collectors.toList())); + } + } + + @Override + public List cloneValue(List value) { + return new ArrayList<>(value); + } + + private String[] getLore() { + return new String[] { formatDescription(Lang.optionVisibilityLore.toString()), "", formatValue(getValue().stream().map(VisibilityLocation::getName).collect(Collectors.joining(", "))) }; + } + + @Override + public ItemStack getItemStack(OptionSet options) { + return ItemUtils.item(XMaterial.SPYGLASS.or(XMaterial.BOOKSHELF), Lang.optionVisibility.toString(), getLore()); + } + + @Override + public void click(FinishGUI gui, Player p, ItemStack item, int slot, ClickType click) { + new VisibilityGUI(() -> { + ItemUtils.lore(item, getLore()); + gui.reopen(p); + }).create(p); + } + + class VisibilityGUI implements CustomInventory { + + private EnumMap locations = new EnumMap<>(VisibilityLocation.class); + private Runnable reopen; + + public VisibilityGUI(Runnable reopen) { + this.reopen = reopen; + } + + @Override + public Inventory open(Player p) { + Inventory inv = Bukkit.createInventory(null, InventoryType.HOPPER, Lang.INVENTORY_VISIBILITY.toString()); + + for (int i = 0; i < 4; i++) { + VisibilityLocation loc = VisibilityLocation.values()[i]; + boolean visible = getValue().contains(loc); + locations.put(loc, visible); + inv.setItem(i, ItemUtils.itemSwitch(loc.getName(), visible)); + } + inv.setItem(4, ItemUtils.itemDone); + + return p.openInventory(inv).getTopInventory(); + } + + @Override + public boolean onClick(Player p, Inventory inv, ItemStack current, int slot, ClickType click) { + if (slot >= 0 && slot < 4) { + locations.put(VisibilityLocation.values()[slot], ItemUtils.toggle(current)); + }else if (slot == 4) { + setValue(locations.entrySet().stream().filter(Entry::getValue).map(Entry::getKey).collect(Collectors.toList())); + reopen.run(); + } + return true; + } + + @Override + public CloseBehavior onClose(Player p, Inventory inv) { + Utils.runSync(reopen); + return CloseBehavior.NOTHING; + } + + } + + public enum VisibilityLocation { + TAB_NOT_STARTED(Lang.visibility_notStarted.toString()), + TAB_IN_PROGRESS(Lang.visibility_inProgress.toString()), + TAB_FINISHED(Lang.visibility_finished.toString()), + MAPS(Lang.visibility_maps.toString()); + + private final String name; + + private VisibilityLocation(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + } + +} diff --git a/core/src/main/java/fr/skytasul/quests/players/PlayerAccount.java b/core/src/main/java/fr/skytasul/quests/players/PlayerAccount.java index 2c346a0d..630f42a5 100644 --- a/core/src/main/java/fr/skytasul/quests/players/PlayerAccount.java +++ b/core/src/main/java/fr/skytasul/quests/players/PlayerAccount.java @@ -1,145 +1,175 @@ -package fr.skytasul.quests.players; - -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; - -import org.bukkit.OfflinePlayer; -import org.bukkit.configuration.ConfigurationSection; -import org.bukkit.entity.Player; - -import fr.skytasul.quests.players.accounts.AbstractAccount; -import fr.skytasul.quests.structure.Quest; -import fr.skytasul.quests.structure.pools.QuestPool; -import fr.skytasul.quests.utils.Utils; - -public class PlayerAccount { - - public final AbstractAccount abstractAcc; - protected final Map questDatas = new HashMap<>(); - protected final Map poolDatas = new HashMap<>(); - protected final int index; - - public PlayerAccount(AbstractAccount account, int index) { - this.abstractAcc = account; - this.index = index; - } - - /** - * @return if this account is currently used by the player (if true, {@link #getPlayer()} cannot return a null player) - */ - public boolean isCurrent() { - return abstractAcc.isCurrent(); - } - - /** - * @return the OfflinePlayer instance attached to this account (no matter if the player is online or not, or if the account is the currently used) - */ - public OfflinePlayer getOfflinePlayer(){ - return abstractAcc.getOfflinePlayer(); - } - - /** - * @return the Player instance who own this account. If the account is not which in use by the player ({@link #isCurrent()}), this will return null. - */ - public Player getPlayer(){ - return abstractAcc.getPlayer(); - } - - public boolean hasQuestDatas(Quest quest) { - return questDatas.containsKey(quest.getID()); - } - - public PlayerQuestDatas getQuestDatasIfPresent(Quest quest) { - return questDatas.get(quest.getID()); - } - - public PlayerQuestDatas getQuestDatas(Quest quest) { - PlayerQuestDatas datas = questDatas.get(quest.getID()); - if (datas == null) { - datas = PlayersManager.manager.createPlayerQuestDatas(this, quest); - questDatas.put(quest.getID(), datas); - } - return datas; - } - - public PlayerQuestDatas removeQuestDatas(Quest quest) { - return removeQuestDatas(quest.getID()); - } - - public PlayerQuestDatas removeQuestDatas(int id) { - PlayerQuestDatas removed = questDatas.remove(id); - if (removed != null) PlayersManager.manager.playerQuestDataRemoved(this, id, removed); - return removed; - } - - protected PlayerQuestDatas removeQuestDatasSilently(int id) { - return questDatas.remove(id); - } - - public Collection getQuestsDatas() { - return questDatas.values(); - } - - public boolean hasPoolDatas(QuestPool pool) { - return poolDatas.containsKey(pool.getID()); - } - - public PlayerPoolDatas getPoolDatas(QuestPool pool) { - PlayerPoolDatas datas = poolDatas.get(pool.getID()); - if (datas == null) { - datas = PlayersManager.manager.createPlayerPoolDatas(this, pool); - poolDatas.put(pool.getID(), datas); - } - return datas; - } - - public PlayerPoolDatas removePoolDatas(QuestPool pool) { - return removePoolDatas(pool.getID()); - } - - public PlayerPoolDatas removePoolDatas(int id) { - PlayerPoolDatas removed = poolDatas.remove(id); - if (removed != null) PlayersManager.manager.playerPoolDataRemoved(this, id, removed); - return removed; - } - - public Collection getPoolDatas() { - return poolDatas.values(); - } - - @Override - public boolean equals(Object arg0) { - if (arg0 == this) return true; - if (arg0.getClass() != this.getClass()) return false; - PlayerAccount otherAccount = (PlayerAccount) arg0; - if (!abstractAcc.equals(otherAccount.abstractAcc)) return false; - return true; - } - - @Override - public int hashCode() { - int hash = 1; - - hash = hash * 31 + index; - hash = hash * 31 + abstractAcc.hashCode(); - - return hash; - } - - public String getName() { - Player p = getPlayer(); - return p == null ? debugName() : p.getName(); - } - - public String debugName() { - return abstractAcc.getIdentifier() + " (#" + index + ")"; - } - - public void serialize(ConfigurationSection config) { - config.set("identifier", abstractAcc.getIdentifier()); - config.set("quests", questDatas.isEmpty() ? null : Utils.serializeList(questDatas.values(), PlayerQuestDatas::serialize)); - config.set("pools", poolDatas.isEmpty() ? null : Utils.serializeList(poolDatas.values(), PlayerPoolDatas::serialize)); - } - -} +package fr.skytasul.quests.players; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.bukkit.OfflinePlayer; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.entity.Player; + +import fr.skytasul.quests.api.data.SavableData; +import fr.skytasul.quests.players.accounts.AbstractAccount; +import fr.skytasul.quests.structure.Quest; +import fr.skytasul.quests.structure.pools.QuestPool; +import fr.skytasul.quests.utils.Utils; + +public class PlayerAccount { + + public static final List FORBIDDEN_DATA_ID = Arrays.asList("identifier", "quests", "pools"); + + public final AbstractAccount abstractAcc; + protected final Map questDatas = new HashMap<>(); + protected final Map poolDatas = new HashMap<>(); + protected final Map, Object> additionalDatas = new HashMap<>(); + protected final int index; + + protected PlayerAccount(AbstractAccount account, int index) { + this.abstractAcc = account; + this.index = index; + } + + /** + * @return if this account is currently used by the player (if true, {@link #getPlayer()} cannot return a null player) + */ + public boolean isCurrent() { + return abstractAcc.isCurrent(); + } + + /** + * @return the OfflinePlayer instance attached to this account (no matter if the player is online or not, or if the account is the currently used) + */ + public OfflinePlayer getOfflinePlayer(){ + return abstractAcc.getOfflinePlayer(); + } + + /** + * @return the Player instance who own this account. If the account is not which in use by the player ({@link #isCurrent()}), this will return null. + */ + public Player getPlayer(){ + return abstractAcc.getPlayer(); + } + + public boolean hasQuestDatas(Quest quest) { + return questDatas.containsKey(quest.getID()); + } + + public PlayerQuestDatas getQuestDatasIfPresent(Quest quest) { + return questDatas.get(quest.getID()); + } + + public PlayerQuestDatas getQuestDatas(Quest quest) { + PlayerQuestDatas datas = questDatas.get(quest.getID()); + if (datas == null) { + datas = PlayersManager.manager.createPlayerQuestDatas(this, quest); + questDatas.put(quest.getID(), datas); + } + return datas; + } + + public PlayerQuestDatas removeQuestDatas(Quest quest) { + return removeQuestDatas(quest.getID()); + } + + public PlayerQuestDatas removeQuestDatas(int id) { + PlayerQuestDatas removed = questDatas.remove(id); + if (removed != null) PlayersManager.manager.playerQuestDataRemoved(this, id, removed); + return removed; + } + + protected PlayerQuestDatas removeQuestDatasSilently(int id) { + return questDatas.remove(id); + } + + public Collection getQuestsDatas() { + return questDatas.values(); + } + + public boolean hasPoolDatas(QuestPool pool) { + return poolDatas.containsKey(pool.getID()); + } + + public PlayerPoolDatas getPoolDatas(QuestPool pool) { + PlayerPoolDatas datas = poolDatas.get(pool.getID()); + if (datas == null) { + datas = PlayersManager.manager.createPlayerPoolDatas(this, pool); + poolDatas.put(pool.getID(), datas); + } + return datas; + } + + public PlayerPoolDatas removePoolDatas(QuestPool pool) { + return removePoolDatas(pool.getID()); + } + + public PlayerPoolDatas removePoolDatas(int id) { + PlayerPoolDatas removed = poolDatas.remove(id); + if (removed != null) PlayersManager.manager.playerPoolDataRemoved(this, id, removed); + return removed; + } + + public Collection getPoolDatas() { + return poolDatas.values(); + } + + public T getData(SavableData data) { + if (!PlayersManager.manager.getAccountDatas().contains(data)) + throw new IllegalArgumentException("The " + data.getId() + " account data has not been registered."); + return (T) additionalDatas.getOrDefault(data, data.getDefaultValue()); + } + + public void setData(SavableData data, T value) { + if (!PlayersManager.manager.getAccountDatas().contains(data)) + throw new IllegalArgumentException("The " + data.getId() + " account data has not been registered."); + additionalDatas.put(data, value); + } + + public void resetDatas() { + additionalDatas.clear(); + } + + @Override + public boolean equals(Object arg0) { + if (arg0 == this) return true; + if (arg0.getClass() != this.getClass()) return false; + PlayerAccount otherAccount = (PlayerAccount) arg0; + if (!abstractAcc.equals(otherAccount.abstractAcc)) return false; + return true; + } + + @Override + public int hashCode() { + int hash = 1; + + hash = hash * 31 + index; + hash = hash * 31 + abstractAcc.hashCode(); + + return hash; + } + + public String getName() { + Player p = getPlayer(); + return p == null ? debugName() : p.getName(); + } + + public String getNameAndID() { + Player p = getPlayer(); + return p == null ? debugName() : p.getName() + " (# " + index + ")"; + } + + public String debugName() { + return abstractAcc.getIdentifier() + " (#" + index + ")"; + } + + public void serialize(ConfigurationSection config) { + config.set("identifier", abstractAcc.getIdentifier()); + config.set("quests", questDatas.isEmpty() ? null : Utils.serializeList(questDatas.values(), PlayerQuestDatas::serialize)); + config.set("pools", poolDatas.isEmpty() ? null : Utils.serializeList(poolDatas.values(), PlayerPoolDatas::serialize)); + additionalDatas.entrySet().forEach(entry -> { + config.set(entry.getKey().getId(), entry.getValue()); + }); + } + +} diff --git a/core/src/main/java/fr/skytasul/quests/players/PlayerQuestDatas.java b/core/src/main/java/fr/skytasul/quests/players/PlayerQuestDatas.java index 14e02308..ae173521 100644 --- a/core/src/main/java/fr/skytasul/quests/players/PlayerQuestDatas.java +++ b/core/src/main/java/fr/skytasul/quests/players/PlayerQuestDatas.java @@ -17,31 +17,33 @@ public class PlayerQuestDatas { protected final PlayerAccount acc; protected final int questID; - private int finished = 0; - private long timer = 0; - private int branch = -1, stage = -1; - private Map[] stageDatas = new Map[5]; - private StringJoiner questFlow = new StringJoiner(";"); + private int finished; + private long timer; + private int branch; + private int stage; + protected Map additionalDatas; + protected StringJoiner questFlow = new StringJoiner(";"); private Boolean hasDialogsCached = null; public PlayerQuestDatas(PlayerAccount acc, int questID) { this.acc = acc; this.questID = questID; + this.finished = 0; + this.timer = 0; + this.branch = -1; + this.stage = -1; + this.additionalDatas = new HashMap<>(); } - public PlayerQuestDatas(PlayerAccount acc, int questID, long timer, int finished, int branch, int stage, Map stage0datas, Map stage1datas, Map stage2datas, Map stage3datas, Map stage4datas, String questFlow) { + public PlayerQuestDatas(PlayerAccount acc, int questID, long timer, int finished, int branch, int stage, Map additionalDatas, String questFlow) { this.acc = acc; this.questID = questID; this.finished = finished; this.timer = timer; this.branch = branch; this.stage = stage; - this.stageDatas[0] = stage0datas; - this.stageDatas[1] = stage1datas; - this.stageDatas[2] = stage2datas; - this.stageDatas[3] = stage3datas; - this.stageDatas[4] = stage4datas; + this.additionalDatas = additionalDatas == null ? new HashMap<>() : additionalDatas; if (questFlow != null) this.questFlow.add(questFlow); if (branch != -1 && stage == -1) BeautyQuests.logger.warning("Incorrect quest " + questID + " datas for " + acc.debugName()); } @@ -110,12 +112,28 @@ public void setInEndingStages() { setStage(-2); } - public Map getStageDatas(int stage) { - return stageDatas[stage]; + public T getAdditionalData(String key) { + return (T) additionalDatas.get(key); + } + + public T setAdditionalData(String key, T value) { + return (T) (value == null ? additionalDatas.remove(key) : additionalDatas.put(key, value)); } - public void setStageDatas(int stage, Map stageDatas) { - this.stageDatas[stage] = stageDatas; + public Map getStageDatas(int stage) { + return getAdditionalData("stage" + stage); + } + + public void setStageDatas(int stage, Map datas) { + setAdditionalData("stage" + stage, datas); + } + + public long getStartingTime() { + return getAdditionalData("starting_time"); + } + + public void setStartingTime(long time) { + setAdditionalData("starting_time", time == 0 ? null : time); } public String getQuestFlow() { @@ -158,9 +176,7 @@ public Map serialize() { if (timer != 0) map.put("timer", timer); if (branch != -1) map.put("currentBranch", branch); if (stage != -1) map.put("currentStage", stage); - for (int i = 0; i < stageDatas.length; i++) { - if (stageDatas[i] != null) map.put("stage" + i + "datas", stageDatas[i]); - } + if (!additionalDatas.isEmpty()) map.put("datas", additionalDatas); if (questFlow.length() > 0) map.put("questFlow", questFlow.toString()); return map; @@ -173,10 +189,15 @@ public static PlayerQuestDatas deserialize(PlayerAccount acc, Map) map.get("stage" + i + "datas"); - } + if (map.containsKey("datas")) datas.additionalDatas = (Map) map.get("datas"); if (map.containsKey("questFlow")) datas.questFlow.add((String) map.get("questFlow")); + + for (int i = 0; i < 5; i++) { // TODO remove ; migration purpose ; added on 0.20 + if (map.containsKey("stage" + i + "datas")) { + datas.additionalDatas.put("stage" + i, map.get("stage" + i + "datas")); + } + } + return datas; } diff --git a/core/src/main/java/fr/skytasul/quests/players/PlayersManager.java b/core/src/main/java/fr/skytasul/quests/players/PlayersManager.java index a98199e2..c42af2ae 100644 --- a/core/src/main/java/fr/skytasul/quests/players/PlayersManager.java +++ b/core/src/main/java/fr/skytasul/quests/players/PlayersManager.java @@ -1,20 +1,29 @@ package fr.skytasul.quests.players; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Collection; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; -import java.util.Map.Entry; +import java.util.Set; import java.util.UUID; - import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; - +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import fr.skytasul.quests.BeautyQuests; import fr.skytasul.quests.QuestsConfiguration; import fr.skytasul.quests.api.QuestsAPI; +import fr.skytasul.quests.api.data.SavableData; +import fr.skytasul.quests.api.events.accounts.PlayerAccountJoinEvent; +import fr.skytasul.quests.api.events.accounts.PlayerAccountLeaveEvent; import fr.skytasul.quests.players.accounts.AbstractAccount; import fr.skytasul.quests.players.accounts.UUIDAccount; -import fr.skytasul.quests.players.events.PlayerAccountJoinEvent; -import fr.skytasul.quests.players.events.PlayerAccountLeaveEvent; import fr.skytasul.quests.structure.Quest; import fr.skytasul.quests.structure.pools.QuestPool; import fr.skytasul.quests.utils.DebugUtils; @@ -22,10 +31,11 @@ import fr.skytasul.quests.utils.compatibility.MissingDependencyException; public abstract class PlayersManager { + + protected final Set> accountDatas = new HashSet<>(); + private boolean loaded = false; - public static PlayersManager manager; - - protected abstract Entry load(Player player, long joinTimestamp); + public abstract void load(AccountFetchRequest request); protected abstract void removeAccount(PlayerAccount acc); @@ -41,16 +51,45 @@ public void playerPoolDataRemoved(PlayerAccount acc, int id, PlayerPoolDatas dat public abstract void unloadAccount(PlayerAccount acc); - public abstract void load(); + public void load() { + if (loaded) throw new IllegalStateException("Already loaded"); + loaded = true; + } + + public boolean isLoaded() { + return loaded; + } public abstract void save(); + + public void addAccountData(SavableData data) { + if (loaded) + throw new IllegalStateException("Cannot add account data after players manager has been loaded"); + if (PlayerAccount.FORBIDDEN_DATA_ID.contains(data.getId())) + throw new IllegalArgumentException("Forbidden account data id " + data.getId()); + if (accountDatas.stream().anyMatch(x -> x.getId().equals(data.getId()))) + throw new IllegalArgumentException("Another account data already exists with the id " + data.getId()); + if (data.getDataType().isPrimitive()) + throw new IllegalArgumentException("Primitive account data types are not supported"); + accountDatas.add(data); + DebugUtils.logMessage("Registered account data " + data.getId()); + } + + public Collection> getAccountDatas() { + return accountDatas; + } public AbstractAccount createAbstractAccount(Player p) { return QuestsConfiguration.hookAccounts() ? Accounts.getPlayerAccount(p) : new UUIDAccount(p.getUniqueId()); } - public String getIdentifier(Player p) { - return QuestsConfiguration.hookAccounts() ? "Hooked|" + Accounts.getPlayerCurrentIdentifier(p) : p.getUniqueId().toString(); + public String getIdentifier(OfflinePlayer p) { + if (QuestsConfiguration.hookAccounts()) { + if (!p.isOnline()) + throw new IllegalArgumentException("Cannot fetch player identifier of an offline player with AccountsHook"); + return "Hooked|" + Accounts.getPlayerCurrentIdentifier(p.getPlayer()); + } + return p.getUniqueId().toString(); } protected AbstractAccount createAccountFromIdentifier(String identifier) { @@ -80,8 +119,14 @@ protected AbstractAccount createAccountFromIdentifier(String identifier) { } protected static Map cachedAccounts = new HashMap<>(); + private static Map cachedPlayerNames = new HashMap<>(); + private static Gson gson = new Gson(); + private static long lastOnlineFailure = 0; + public static PlayersManager manager; public static synchronized void loadPlayer(Player p) { + cachedPlayerNames.put(p.getUniqueId(), p.getName()); + long time = System.currentTimeMillis(); DebugUtils.logMessage("Loading player " + p.getName() + "..."); cachedAccounts.remove(p); @@ -90,34 +135,49 @@ public static synchronized void loadPlayer(Player p) { while (i > 0) { i--; try { - - Entry entry = manager.load(p, time); - PlayerAccount account = entry.getKey(); - boolean created = entry.getValue(); - if (!p.isOnline()) { - if (created) { - DebugUtils.logMessage("New account registered for " + p.getName() + "... but deleted as player left before loading."); - manager.removeAccount(account); + AccountFetchRequest request = new AccountFetchRequest(p, time, true, true); + manager.load(request); + + if (request.isFinished() && request.getAccount() != null) { + if (!p.isOnline()) { + if (request.isAccountCreated()) { + DebugUtils.logMessage("New account registered for " + p.getName() + + "... but deleted as player left before loading."); + manager.removeAccount(request.getAccount()); + } + return; } + if (request.isAccountCreated()) + DebugUtils.logMessage("New account registered for " + p.getName() + " (" + + request.getAccount().abstractAcc.getIdentifier() + "), index " + + request.getAccount().index + " via " + DebugUtils.stackTraces(2, 4)); + cachedAccounts.put(p, request.getAccount()); + Bukkit.getScheduler().runTask(BeautyQuests.getInstance(), () -> { + String loadMessage = "Completed load of " + p.getName() + " datas within " + + (System.currentTimeMillis() - time) + " ms (" + + request.getAccount().getQuestsDatas().size() + " quests, " + + request.getAccount().getPoolDatas().size() + " pools)"; + if (request.getLoadedFrom() != null) + loadMessage += " | Loaded from " + request.getLoadedFrom(); + DebugUtils.logMessage(loadMessage); + if (p.isOnline()) { + Bukkit.getPluginManager().callEvent( + new PlayerAccountJoinEvent(p, request.getAccount(), request.isAccountCreated())); + } else { + BeautyQuests.logger.warning("Player " + p.getName() + + " has quit the server while loading its datas. This may be a bug."); + if (request.isAccountCreated()) { + manager.removeAccount(request.getAccount()); + } + } + }); return; } - if (created) DebugUtils.logMessage("New account registered for " + p.getName() + " (" + account.abstractAcc.getIdentifier() + "), index " + account.index + " via " + DebugUtils.stackTraces(2, 4)); - cachedAccounts.put(p, account); - Bukkit.getScheduler().runTask(BeautyQuests.getInstance(), () -> { - DebugUtils.logMessage("Completed load of " + p.getName() + " datas within " + (System.currentTimeMillis() - time) + " ms (" + account.getQuestsDatas().size() + " quests, " + account.getPoolDatas().size() + " pools)"); - if (p.isOnline()) { - Bukkit.getPluginManager().callEvent(new PlayerAccountJoinEvent(p, account, created)); - }else { - BeautyQuests.logger.warning("Player " + p.getName() + " has quit the server while loading its datas. This may be a bug."); - if (created) { - manager.removeAccount(account); - } - } - }); - return; + BeautyQuests.logger.severe("The account of " + p.getName() + " has not been properly loaded."); }catch (Exception ex) { - BeautyQuests.logger.severe("An error ocurred while trying to load datas of " + p.getName() + ". Doing " + i + " more attempt.", ex); + BeautyQuests.logger.severe("An error ocurred while trying to load datas of " + p.getName() + ".", ex); } + BeautyQuests.logger.severe("Doing " + i + " more attempt."); } BeautyQuests.logger.severe("Datas of " + p.getName() + " have failed to load. This may cause MANY issues."); }); @@ -134,8 +194,169 @@ public static synchronized void unloadPlayer(Player p) { public static PlayerAccount getPlayerAccount(Player p) { if (QuestsAPI.getNPCsManager().isNPC(p)) return null; + if (!p.isOnline()) BeautyQuests.logger.severe("Trying to fetch the account of an offline player (" + p.getName() + ")"); return cachedAccounts.get(p); } - + + public static synchronized String getPlayerName(UUID uuid) { + if (cachedPlayerNames.containsKey(uuid)) + return cachedPlayerNames.get(uuid); + + String name; + if (Bukkit.getOnlineMode()) { + try { + if (System.currentTimeMillis() - lastOnlineFailure < 30_000) { + DebugUtils.logMessage("Trying to fetch a name from an UUID but it failed within 30 seconds."); + return null; + } + + HttpURLConnection connection = (HttpURLConnection) new URL( + "https://sessionserver.mojang.com/session/minecraft/profile/" + uuid.toString()).openConnection(); + connection.setReadTimeout(5000); + + JsonObject profile = gson.fromJson(new BufferedReader(new InputStreamReader(connection.getInputStream())), + JsonObject.class); + JsonElement nameElement = profile.get("name"); + if (nameElement == null) { + name = null; + DebugUtils.logMessage("Cannot find name for UUID " + uuid.toString()); + } else { + name = nameElement.getAsString(); + } + } catch (Exception e) { + BeautyQuests.logger.warning("Cannot connect to the mojang servers. UUIDs cannot be parsed."); + lastOnlineFailure = System.currentTimeMillis(); + return null; + } + } else { + name = Bukkit.getOfflinePlayer(uuid).getName(); + } + + cachedPlayerNames.put(uuid, name); + return name; + } + + public static class AccountFetchRequest { + private final OfflinePlayer player; + private final long joinTimestamp; + private final boolean allowCreation; + private final boolean shouldCache; + + private boolean finished = false; + private boolean created; + private PlayerAccount account; + private String loadedFrom; + + public AccountFetchRequest(OfflinePlayer player, long joinTimestamp, boolean allowCreation, boolean shouldCache) { + this.player = player; + this.joinTimestamp = joinTimestamp; + this.allowCreation = allowCreation; + this.shouldCache = shouldCache; + + if (allowCreation && !player.isOnline()) + throw new IllegalArgumentException("Cannot create an account for an offline player."); + } + + public OfflinePlayer getOfflinePlayer() { + return player; + } + + public Player getOnlinePlayer() { + if (player.isOnline()) + return player.getPlayer(); + throw new IllegalStateException("The player " + player.getName() + " is offline."); + } + + public long getJoinTimestamp() { + return joinTimestamp; + } + + /** + * @return true if an account must be created when no account can be loaded + */ + public boolean mustCreateMissing() { + return allowCreation; + } + + /** + * @return true if the loaded account should be cached internally (usually because this + * account will get associated with an online player) + */ + public boolean shouldCache() { + return shouldCache; + } + + public String getDebugPlayerName() { + String name = player.getName(); + if (name == null) + name = player.getUniqueId().toString(); + return name; + } + + /** + * This method must be called when the request results in a successfully loaded account. + * + * @param account account that has been loaded + * @param from source of the saved account + */ + public void loaded(PlayerAccount account, String from) { + ensureAvailable(); + this.account = account; + this.loadedFrom = from; + this.created = false; + } + + /** + * This method must be called when the request results in the creation of a new account. + *

+ * It cannot be called when the {@link AccountFetchRequest#mustCreateMissing()} + * method returns false. + * + * @param account account that has been created + */ + public void created(PlayerAccount account) { + if (!mustCreateMissing()) + throw new IllegalStateException( + "This method cannot be called as this request does not allow account creation"); + ensureAvailable(); + this.account = account; + this.created = true; + } + + /** + * This method must be called when the request cannot load any account associated with the player + * and the {@link AccountFetchRequest#mustCreateMissing()} returns false. + */ + public void notLoaded() { + if (mustCreateMissing()) + throw new IllegalStateException( + "This method cannot be called as this request requires account creation if no account can be loaded"); + ensureAvailable(); + } + + private void ensureAvailable() { + if (finished) + throw new IllegalStateException("This request has already been completed"); + this.finished = true; + } + + public boolean isFinished() { + return finished; + } + + public PlayerAccount getAccount() { + return account; + } + + public boolean isAccountCreated() { + return created; + } + + public String getLoadedFrom() { + return loadedFrom; + } + + } + } diff --git a/core/src/main/java/fr/skytasul/quests/players/PlayersManagerDB.java b/core/src/main/java/fr/skytasul/quests/players/PlayersManagerDB.java index 3c1acdfa..8cf85048 100644 --- a/core/src/main/java/fr/skytasul/quests/players/PlayersManagerDB.java +++ b/core/src/main/java/fr/skytasul/quests/players/PlayersManagerDB.java @@ -1,617 +1,786 @@ -package fr.skytasul.quests.players; - -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Statement; -import java.util.AbstractMap; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; -import java.util.stream.Collectors; - -import org.apache.commons.lang.StringUtils; -import org.bukkit.entity.Player; -import org.bukkit.scheduler.BukkitRunnable; - -import fr.skytasul.quests.BeautyQuests; -import fr.skytasul.quests.api.stages.AbstractStage; -import fr.skytasul.quests.players.accounts.AbstractAccount; -import fr.skytasul.quests.structure.Quest; -import fr.skytasul.quests.structure.pools.QuestPool; -import fr.skytasul.quests.utils.CustomizedObjectTypeAdapter; -import fr.skytasul.quests.utils.Database; -import fr.skytasul.quests.utils.DebugUtils; - -public class PlayersManagerDB extends PlayersManager { - - private final String ACCOUNTS_TABLE; - private final String QUESTS_DATAS_TABLE; - private final String POOLS_DATAS_TABLE; - - private Database db; - - /* Accounts statements */ - private String getAccounts; - private String insertAccount; - private String deleteAccount; - - /* Quest datas statements */ - private String insertQuestData; - private String removeQuestData; - private String getQuestsData; - private String getQuestAccountData; - - private String removeExistingQuestDatas; - - private String updateFinished; - private String updateTimer; - private String updateBranch; - private String updateStage; - private String[] updateDatas = new String[5]; - private String updateFlow; - - /* Pool datas statements */ - private String insertPoolData; - private String removePoolData; - private String getPoolData; - private String getPoolAccountData; - - private String updatePoolLastGive; - private String updatePoolCompletedQuests; - - public PlayersManagerDB(Database db) { - this.db = db; - ACCOUNTS_TABLE = db.getConfig().getString("tables.playerAccounts"); - QUESTS_DATAS_TABLE = db.getConfig().getString("tables.playerQuests"); - POOLS_DATAS_TABLE = db.getConfig().getString("tables.playerPools"); - } - - private synchronized void retrievePlayerDatas(PlayerAccount acc) { - try (Connection connection = db.getConnection()) { - try (PreparedStatement statement = connection.prepareStatement(getQuestsData)) { - statement.setInt(1, acc.index); - ResultSet result = statement.executeQuery(); - while (result.next()) { - int questID = result.getInt("quest_id"); - acc.questDatas.put(questID, new PlayerQuestDatasDB(acc, questID, result)); - } - result.close(); - } - try (PreparedStatement statement = connection.prepareStatement(getPoolData)) { - statement.setInt(1, acc.index); - ResultSet result = statement.executeQuery(); - while (result.next()) { - int poolID = result.getInt("pool_id"); - String completedQuests = result.getString("completed_quests"); - if (StringUtils.isEmpty(completedQuests)) completedQuests = null; - acc.poolDatas.put(poolID, new PlayerPoolDatasDB(acc, poolID, result.getLong("last_give"), completedQuests == null ? new HashSet<>() : Arrays.stream(completedQuests.split(";")).map(Integer::parseInt).collect(Collectors.toSet()))); - } - result.close(); - } - }catch (SQLException e) { - e.printStackTrace(); - } - } - - @Override - protected synchronized Entry load(Player player, long joinTimestamp) { - try (Connection connection = db.getConnection()) { - String uuid = player.getUniqueId().toString(); - try (PreparedStatement statement = connection.prepareStatement(getAccounts)) { - statement.setString(1, uuid); - ResultSet result = statement.executeQuery(); - while (result.next()) { - AbstractAccount abs = createAccountFromIdentifier(result.getString("identifier")); - if (abs.isCurrent()) { - PlayerAccount account = new PlayerAccount(abs, result.getInt("id")); - result.close(); - try { - // in order to ensure that, if the player was previously connected to another server, - // its datas have been fully pushed to database, we wait for 0,4 seconds - long timeout = 400 - (System.currentTimeMillis() - joinTimestamp); - if (timeout > 0) wait(timeout); - }catch (InterruptedException e) { - e.printStackTrace(); - Thread.currentThread().interrupt(); - } - retrievePlayerDatas(account); - return new AbstractMap.SimpleEntry<>(account, false); - } - } - result.close(); - } - try (PreparedStatement statement = connection.prepareStatement(insertAccount, PreparedStatement.RETURN_GENERATED_KEYS)) { - AbstractAccount absacc = super.createAbstractAccount(player); - statement.setString(1, absacc.getIdentifier()); - statement.setString(2, uuid); - statement.executeUpdate(); - ResultSet result = statement.getGeneratedKeys(); - if (!result.next()) throw new SQLException("The plugin has not been able to create a player account."); - int index = result.getInt(1); // some drivers don't return a ResultSet with correct column names - result.close(); - return new AbstractMap.SimpleEntry<>(new PlayerAccount(absacc, index), true); - } - }catch (SQLException e) { - e.printStackTrace(); - } - return null; - } - - @Override - protected synchronized void removeAccount(PlayerAccount acc) { - try (Connection connection = db.getConnection(); - PreparedStatement statement = connection.prepareStatement(deleteAccount)) { - statement.setInt(1, acc.index); - statement.executeUpdate(); - }catch (SQLException ex) { - ex.printStackTrace(); - } - } - - private static Map extractStageDatas(ResultSet result, int index) throws SQLException { - String json = result.getString("stage_" + index + "_datas"); - if (json == null) return null; - return CustomizedObjectTypeAdapter.GSON.fromJson(json, Map.class); - } - - @Override - public PlayerQuestDatas createPlayerQuestDatas(PlayerAccount acc, Quest quest) { - return new PlayerQuestDatasDB(acc, quest.getID()); - } - - @Override - public synchronized void playerQuestDataRemoved(PlayerAccount acc, int id, PlayerQuestDatas datas) { - try (Connection connection = db.getConnection(); - PreparedStatement statement = connection.prepareStatement(removeQuestData)) { - ((PlayerQuestDatasDB) datas).stop(); - statement.setInt(1, acc.index); - statement.setInt(2, id); - statement.executeUpdate(); - }catch (SQLException e) { - e.printStackTrace(); - } - } - - @Override - public PlayerPoolDatas createPlayerPoolDatas(PlayerAccount acc, QuestPool pool) { - return new PlayerPoolDatasDB(acc, pool.getID()); - } - - @Override - public synchronized void playerPoolDataRemoved(PlayerAccount acc, int id, PlayerPoolDatas datas) { - try (Connection connection = db.getConnection(); - PreparedStatement statement = connection.prepareStatement(removePoolData)) { - statement.setInt(1, acc.index); - statement.setInt(2, id); - statement.executeUpdate(); - }catch (SQLException e) { - e.printStackTrace(); - } - } - - @Override - public synchronized int removeQuestDatas(Quest quest) { - int amount = 0; - try (Connection connection = db.getConnection(); - PreparedStatement statement = connection.prepareStatement(removeExistingQuestDatas)) { - for (PlayerAccount acc : PlayersManager.cachedAccounts.values()) { - PlayerQuestDatasDB datas = (PlayerQuestDatasDB) acc.removeQuestDatasSilently(quest.getID()); - if (datas != null) datas.stop(); - } - statement.setInt(1, quest.getID()); - amount += statement.executeUpdate(); - }catch (SQLException e) { - e.printStackTrace(); - } - DebugUtils.logMessage("Removed " + amount + " quest datas for quest " + quest.getID()); - return amount; - } - - public synchronized boolean hasAccounts(Player p) { - try (Connection connection = db.getConnection(); - PreparedStatement statement = connection.prepareStatement(getAccounts)) { - statement.setString(1, p.getUniqueId().toString()); - ResultSet result = statement.executeQuery(); - boolean has = result.next(); - result.close(); - return has; - }catch (SQLException e) { - e.printStackTrace(); - } - return false; - } - - @Override - public void load() { - try { - createTables(); - - getAccounts = "SELECT * FROM " + ACCOUNTS_TABLE + " WHERE `player_uuid` = ?"; - insertAccount = "INSERT INTO " + ACCOUNTS_TABLE + " (`identifier`, `player_uuid`) VALUES (?, ?)"; - deleteAccount = "DELETE FROM " + ACCOUNTS_TABLE + " WHERE `id` = ?"; - - insertQuestData = "INSERT INTO " + QUESTS_DATAS_TABLE + " (`account_id`, `quest_id`) VALUES (?, ?)"; - removeQuestData = "DELETE FROM " + QUESTS_DATAS_TABLE + " WHERE `account_id` = ? AND `quest_id` = ?"; - getQuestsData = "SELECT * FROM " + QUESTS_DATAS_TABLE + " WHERE `account_id` = ?"; - getQuestAccountData = "SELECT 1 FROM " + QUESTS_DATAS_TABLE + " WHERE `account_id` = ? AND `quest_id` = ?"; - - removeExistingQuestDatas = "DELETE FROM " + QUESTS_DATAS_TABLE + " WHERE `quest_id` = ?"; - - updateFinished = prepareDatasStatement("finished"); - updateTimer = prepareDatasStatement("timer"); - updateBranch = prepareDatasStatement("current_branch"); - updateStage = prepareDatasStatement("current_stage"); - for (int i = 0; i < 5; i++) { - updateDatas[i] = prepareDatasStatement("stage_" + i + "_datas"); - } - updateFlow = prepareDatasStatement("quest_flow"); - - insertPoolData = "INSERT INTO " + POOLS_DATAS_TABLE + " (`account_id`, `pool_id`) VALUES (?, ?)"; - removePoolData = "DELETE FROM " + POOLS_DATAS_TABLE + " WHERE `account_id` = ? AND `pool_id` = ?"; - getPoolData = "SELECT * FROM " + POOLS_DATAS_TABLE + " WHERE `account_id` = ?"; - getPoolAccountData = "SELECT 1 FROM " + POOLS_DATAS_TABLE + " WHERE `account_id` = ? AND `pool_id` = ?"; - - updatePoolLastGive = "UPDATE " + POOLS_DATAS_TABLE + " SET `last_give` = ? WHERE `account_id` = ? AND `pool_id` = ?"; - updatePoolCompletedQuests = "UPDATE " + POOLS_DATAS_TABLE + " SET `completed_quests` = ? WHERE `account_id` = ? AND `pool_id` = ?"; - }catch (SQLException e) { - e.printStackTrace(); - } - } - - private String prepareDatasStatement(String column) throws SQLException { - return "UPDATE " + QUESTS_DATAS_TABLE + " SET `" + column + "` = ? WHERE `account_id` = ? AND `quest_id` = ?"; - } - - @Override - public void save() { - PlayersManager.cachedAccounts.values().forEach(x -> saveAccount(x, false)); - } - - private void createTables() throws SQLException { - try (Connection connection = db.getConnection(); Statement statement = connection.createStatement()) { - statement.execute("CREATE TABLE IF NOT EXISTS " + ACCOUNTS_TABLE + " (" - + " `id` int NOT NULL AUTO_INCREMENT ," - + " `identifier` text NOT NULL ," - + " `player_uuid` char(36) NOT NULL ," - + " PRIMARY KEY (`id`)" - + " )"); - statement.execute("CREATE TABLE IF NOT EXISTS " + QUESTS_DATAS_TABLE + " (" + - " `id` int NOT NULL AUTO_INCREMENT ," + - " `account_id` int(11) NOT NULL," + - " `quest_id` int(11) NOT NULL," + - " `finished` INT(11) DEFAULT NULL," + - " `timer` bigint(20) DEFAULT NULL," + - " `current_branch` tinyint(4) DEFAULT NULL," + - " `current_stage` tinyint(4) DEFAULT NULL," + - " `stage_0_datas` longtext DEFAULT NULL," + - " `stage_1_datas` longtext DEFAULT NULL," + - " `stage_2_datas` longtext DEFAULT NULL," + - " `stage_3_datas` longtext DEFAULT NULL," + - " `stage_4_datas` longtext DEFAULT NULL," + - " `quest_flow` VARCHAR(8000) DEFAULT NULL," + - " PRIMARY KEY (`id`)" + - ")"); - statement.execute("CREATE TABLE IF NOT EXISTS " + POOLS_DATAS_TABLE + " (" - + "`id` int NOT NULL AUTO_INCREMENT, " - + "`account_id` int(11) NOT NULL, " - + "`pool_id` int(11) NOT NULL, " - + "`last_give` bigint(20) DEFAULT NULL, " - + "`completed_quests` varchar(1000) DEFAULT NULL, " - + "PRIMARY KEY (`id`)" - + ")"); - - List columns = new ArrayList<>(14); - try (ResultSet set = connection.getMetaData().getColumns(db.getDatabase(), null, QUESTS_DATAS_TABLE, null)) { - while (set.next()) { - columns.add(set.getString("COLUMN_NAME").toLowerCase()); - } - } - if (columns.isEmpty()) { - BeautyQuests.logger.severe("Cannot check integrity of SQL table " + QUESTS_DATAS_TABLE); - }else if (!columns.contains("quest_flow")) { - statement.execute("ALTER TABLE " + QUESTS_DATAS_TABLE + " ADD COLUMN quest_flow VARCHAR(8000) DEFAULT NULL"); - BeautyQuests.logger.info("Updated database with quest_flow column."); - } - statement.execute("ALTER TABLE " + QUESTS_DATAS_TABLE + " MODIFY COLUMN finished INT(11) DEFAULT 0"); - } - } - - public static synchronized String migrate(Database db, PlayersManagerYAML yaml) throws SQLException { - try (Connection connection = db.getConnection()) { - ResultSet result = connection.getMetaData().getTables(null, null, "%", null); - while (result.next()) { - String tableName = result.getString(3); - if (tableName.equals("player_accounts") || tableName.equals("player_quests")) { - result.close(); - return "§cTable \"" + tableName + "\" already exists. Please drop it before migration."; - } - } - result.close(); - - PlayersManagerDB manager = new PlayersManagerDB(db); - manager.createTables(); - - PreparedStatement insertAccount = - connection.prepareStatement("INSERT INTO " + manager.ACCOUNTS_TABLE + " (`id`, `identifier`, `player_uuid`) VALUES (?, ?, ?)"); - PreparedStatement insertQuestData = - connection.prepareStatement("INSERT INTO " + manager.QUESTS_DATAS_TABLE - + " (`account_id`, `quest_id`, `finished`, `timer`, `current_branch`, `current_stage`, `stage_0_datas`, `stage_1_datas`, `stage_2_datas`, `stage_3_datas`, `stage_4_datas`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); - PreparedStatement insertPoolData = - connection.prepareStatement("INSERT INTO " + manager.POOLS_DATAS_TABLE + " (`account_id`, `pool_id`, `last_give`, `completed_quests`) VALUES (?, ?, ?, ?)"); - - int amount = 0, failed = 0; - yaml.loadAllAccounts(); - for (PlayerAccount acc : yaml.loadedAccounts.values()) { - try { - insertAccount.setInt(1, acc.index); - insertAccount.setString(2, acc.abstractAcc.getIdentifier()); - insertAccount.setString(3, acc.getOfflinePlayer().getUniqueId().toString()); - insertAccount.executeUpdate(); - - for (Entry entry : acc.questDatas.entrySet()) { - insertQuestData.setInt(1, acc.index); - insertQuestData.setInt(2, entry.getKey()); - insertQuestData.setInt(3, entry.getValue().getTimesFinished()); - insertQuestData.setLong(4, entry.getValue().getTimer()); - insertQuestData.setInt(5, entry.getValue().getBranch()); - insertQuestData.setInt(6, entry.getValue().getStage()); - for (int i = 0; i < 5; i++) { - Map stageDatas = entry.getValue().getStageDatas(i); - insertQuestData.setString(7 + i, stageDatas == null ? null : CustomizedObjectTypeAdapter.GSON.toJson(stageDatas)); - } - insertQuestData.executeUpdate(); - } - - for (Entry entry : acc.poolDatas.entrySet()) { - insertPoolData.setInt(1, acc.index); - insertPoolData.setInt(2, entry.getKey()); - insertPoolData.setLong(3, entry.getValue().getLastGive()); - insertPoolData.setString(4, getCompletedQuestsString(entry.getValue().getCompletedQuests())); - insertPoolData.executeUpdate(); - } - - amount++; - }catch (Exception ex) { - BeautyQuests.logger.severe("Failed to migrate datas for account " + acc.debugName(), ex); - failed++; - } - } - - insertAccount.close(); - insertQuestData.close(); - insertPoolData.close(); - - return "§aMigration succeed! " + amount + " accounts migrated, " + failed + " accounts failed to migrate.\n§oDatabase saving system is §lnot§r§a§o enabled. You need to reboot the server with the line \"database.enabled\" set to true."; - } - } - - @Override - public void unloadAccount(PlayerAccount acc) { - saveAccount(acc, true); - } - - public void saveAccount(PlayerAccount acc, boolean stop) { - acc.getQuestsDatas() - .stream() - .map(PlayerQuestDatasDB.class::cast) - .forEach(x -> x.flushAll(stop)); - } - - protected static String getCompletedQuestsString(Set completedQuests) { - return completedQuests.isEmpty() ? null : completedQuests.stream().map(x -> Integer.toString(x)).collect(Collectors.joining(";")); - } - - public class PlayerQuestDatasDB extends PlayerQuestDatas { - - private static final int DATA_FLUSHING_TIME = 10; - - private Map> cachedDatas = new HashMap<>(5); - private Lock datasLock = new ReentrantLock(); - private boolean disabled = false; - - public PlayerQuestDatasDB(PlayerAccount acc, int questID) { - super(acc, questID); - } - - public PlayerQuestDatasDB(PlayerAccount acc, int questID, ResultSet result) throws SQLException { - super( - acc, - questID, - result.getLong("timer"), - result.getInt("finished"), - result.getInt("current_branch"), - result.getInt("current_stage"), - extractStageDatas(result, 0), - extractStageDatas(result, 1), - extractStageDatas(result, 2), - extractStageDatas(result, 3), - extractStageDatas(result, 4), - result.getString("quest_flow")); - } - - @Override - public void incrementFinished() { - super.incrementFinished(); - setDataStatement(updateFinished, getTimesFinished(), false); - } - - @Override - public void setTimer(long timer) { - super.setTimer(timer); - setDataStatement(updateTimer, timer, false); - } - - @Override - public void setBranch(int branch) { - super.setBranch(branch); - setDataStatement(updateBranch, branch, false); - } - - @Override - public void setStage(int stage) { - super.setStage(stage); - setDataStatement(updateStage, stage, false); - } - - @Override - public void setStageDatas(int stage, Map stageDatas) { - super.setStageDatas(stage, stageDatas); - setDataStatement(updateDatas[stage], stageDatas == null ? null : CustomizedObjectTypeAdapter.GSON.toJson(stageDatas), true); - } - - @Override - public void addQuestFlow(AbstractStage finished) { - super.addQuestFlow(finished); - setDataStatement(updateFlow, getQuestFlow(), true); - } - - @Override - public void resetQuestFlow() { - super.resetQuestFlow(); - setDataStatement(updateFlow, null, true); - } - - private void setDataStatement(String dataStatement, Object data, boolean allowNull) { - if (disabled) return; - try { - datasLock.lock(); - if (disabled) { - // in case disabled while acquiring lock - }else if (cachedDatas.containsKey(dataStatement)) { - cachedDatas.get(dataStatement).setValue(data); - }else { - BukkitRunnable runnable = new BukkitRunnable() { - - @Override - public void run() { - if (disabled) return; - Entry entry = null; - datasLock.lock(); - try { - if (!disabled) { // in case disabled while acquiring lock - entry = cachedDatas.remove(dataStatement); - } - }finally { - datasLock.unlock(); - } - if (entry != null) { - try (Connection connection = db.getConnection()) { - try (PreparedStatement statement = connection.prepareStatement(getQuestAccountData)) { - statement.setInt(1, acc.index); - statement.setInt(2, questID); - if (!statement.executeQuery().next()) { // if result set empty => need to insert data then update - try (PreparedStatement insertStatement = connection.prepareStatement(insertQuestData)) { - insertStatement.setInt(1, acc.index); - insertStatement.setInt(2, questID); - insertStatement.executeUpdate(); - } - } - } - try (PreparedStatement statement = connection.prepareStatement(dataStatement)) { - statement.setObject(1, entry.getValue()); - statement.setInt(2, acc.index); - statement.setInt(3, questID); - statement.executeUpdate(); - if (entry.getValue() == null && !allowNull) { - BeautyQuests.logger.warning("Setting an illegal NULL value in statement \"" + dataStatement + "\" for account " + acc.index + " and quest " + questID); - } - } - }catch (SQLException e) { - e.printStackTrace(); - } - } - } - }; - runnable.runTaskLaterAsynchronously(BeautyQuests.getInstance(), DATA_FLUSHING_TIME); - cachedDatas.put(dataStatement, new AbstractMap.SimpleEntry<>(runnable, data)); - } - }finally { - datasLock.unlock(); - } - } - - protected void flushAll(boolean stop) { - datasLock.lock(); - cachedDatas.values() - .stream() - .map(Entry::getKey) - .collect(Collectors.toList()) // to prevent ConcurrentModificationException - .forEach(run -> { - run.run(); - run.cancel(); - }); - if (!cachedDatas.isEmpty()) BeautyQuests.logger.warning("Still waiting values in quest data " + questID + " for account " + acc.index + " despite flushing all."); - if (stop) disabled = true; - datasLock.unlock(); - } - - protected void stop() { - disabled = true; - datasLock.lock(); - cachedDatas.values() - .stream() - .map(Entry::getKey) - .forEach(BukkitRunnable::cancel); - cachedDatas.clear(); - datasLock.unlock(); - } - - } - - public class PlayerPoolDatasDB extends PlayerPoolDatas { - - public PlayerPoolDatasDB(PlayerAccount acc, int poolID) { - super(acc, poolID); - } - - public PlayerPoolDatasDB(PlayerAccount acc, int poolID, long lastGive, Set completedQuests) { - super(acc, poolID, lastGive, completedQuests); - } - - @Override - public void setLastGive(long lastGive) { - super.setLastGive(lastGive); - updateData(updatePoolLastGive, lastGive); - } - - @Override - public void updatedCompletedQuests() { - updateData(updatePoolCompletedQuests, getCompletedQuestsString(getCompletedQuests())); - } - - private void updateData(String dataStatement, Object data) { - try (Connection connection = db.getConnection()) { - try (PreparedStatement statement = connection.prepareStatement(getPoolAccountData)) { - statement.setInt(1, acc.index); - statement.setInt(2, poolID); - if (!statement.executeQuery().next()) { // if result set empty => need to insert data then update - try (PreparedStatement insertStatement = connection.prepareStatement(insertPoolData)) { - insertStatement.setInt(1, acc.index); - insertStatement.setInt(2, poolID); - insertStatement.executeUpdate(); - } - } - } - try (PreparedStatement statement = connection.prepareStatement(dataStatement)) { - statement.setObject(1, data); - statement.setInt(2, acc.index); - statement.setInt(3, poolID); - statement.executeUpdate(); - } - }catch (SQLException e) { - e.printStackTrace(); - } - } - - } - -} +package fr.skytasul.quests.players; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.Collectors; +import org.apache.commons.lang.StringUtils; +import org.bukkit.entity.Player; +import org.bukkit.scheduler.BukkitRunnable; +import fr.skytasul.quests.BeautyQuests; +import fr.skytasul.quests.api.data.SQLDataSaver; +import fr.skytasul.quests.api.data.SavableData; +import fr.skytasul.quests.api.stages.AbstractStage; +import fr.skytasul.quests.players.accounts.AbstractAccount; +import fr.skytasul.quests.structure.Quest; +import fr.skytasul.quests.structure.pools.QuestPool; +import fr.skytasul.quests.utils.CustomizedObjectTypeAdapter; +import fr.skytasul.quests.utils.Database; +import fr.skytasul.quests.utils.DebugUtils; +import fr.skytasul.quests.utils.ThrowingConsumer; +import fr.skytasul.quests.utils.Utils; + +public class PlayersManagerDB extends PlayersManager { + + public final String ACCOUNTS_TABLE; + public final String QUESTS_DATAS_TABLE; + public final String POOLS_DATAS_TABLE; + + private final Database db; + + private final Map, SQLDataSaver> accountDatas = new HashMap<>(); + private String getAccountDatas; + private String resetAccountDatas; + + /* Accounts statements */ + private String getAccountsIDs; + private String insertAccount; + private String deleteAccount; + + /* Quest datas statements */ + private String insertQuestData; + private String removeQuestData; + private String getQuestsData; + + private String removeExistingQuestDatas; + + private String updateFinished; + private String updateTimer; + private String updateBranch; + private String updateStage; + private String updateDatas; + private String updateFlow; + + /* Pool datas statements */ + private String insertPoolData; + private String removePoolData; + private String getPoolData; + private String getPoolAccountData; + + private String updatePoolLastGive; + private String updatePoolCompletedQuests; + + public PlayersManagerDB(Database db) { + this.db = db; + ACCOUNTS_TABLE = db.getConfig().getString("tables.playerAccounts"); + QUESTS_DATAS_TABLE = db.getConfig().getString("tables.playerQuests"); + POOLS_DATAS_TABLE = db.getConfig().getString("tables.playerPools"); + } + + public Database getDatabase() { + return db; + } + + @Override + public void addAccountData(SavableData data) { + super.addAccountData(data); + accountDatas.put(data, new SQLDataSaver<>(data, "UPDATE " + ACCOUNTS_TABLE + " SET `" + data.getColumnName() + "` = ? WHERE `id` = ?")); + getAccountDatas = accountDatas.keySet() + .stream() + .map(x -> "`" + x.getColumnName() + "`") + .collect(Collectors.joining(", ", "SELECT ", " FROM " + ACCOUNTS_TABLE + " WHERE `id` = ?")); + resetAccountDatas = accountDatas.values() + .stream() + .map(x -> "`" + x.getWrappedData().getColumnName() + "` = " + x.getDefaultValueString()) + .collect(Collectors.joining(", ", "UPDATE " + ACCOUNTS_TABLE + " SET ", " WHERE `id` = ?")); + } + + private synchronized void retrievePlayerDatas(PlayerAccount acc) { + try (Connection connection = db.getConnection()) { + try (PreparedStatement statement = connection.prepareStatement(getQuestsData)) { + statement.setInt(1, acc.index); + ResultSet result = statement.executeQuery(); + while (result.next()) { + int questID = result.getInt("quest_id"); + acc.questDatas.put(questID, new PlayerQuestDatasDB(acc, questID, result)); + } + result.close(); + } + try (PreparedStatement statement = connection.prepareStatement(getPoolData)) { + statement.setInt(1, acc.index); + ResultSet result = statement.executeQuery(); + while (result.next()) { + int poolID = result.getInt("pool_id"); + String completedQuests = result.getString("completed_quests"); + if (StringUtils.isEmpty(completedQuests)) completedQuests = null; + acc.poolDatas.put(poolID, new PlayerPoolDatasDB(acc, poolID, result.getLong("last_give"), completedQuests == null ? new HashSet<>() : Arrays.stream(completedQuests.split(";")).map(Integer::parseInt).collect(Collectors.toSet()))); + } + result.close(); + } + if (getAccountDatas != null) { + try (PreparedStatement statement = connection.prepareStatement(getAccountDatas)) { + statement.setInt(1, acc.index); + ResultSet result = statement.executeQuery(); + result.next(); + for (SQLDataSaver data : accountDatas.values()) { + acc.additionalDatas.put(data.getWrappedData(), data.getFromResultSet(result)); + } + result.close(); + } + } + } catch (SQLException ex) { + BeautyQuests.logger.severe("An error occurred while fetching account datas of " + acc.debugName(), ex); + } + } + + @Override + public synchronized void load(AccountFetchRequest request) { + try (Connection connection = db.getConnection()) { + String uuid = request.getOfflinePlayer().getUniqueId().toString(); + try (PreparedStatement statement = connection.prepareStatement(getAccountsIDs)) { + statement.setString(1, uuid); + ResultSet result = statement.executeQuery(); + while (result.next()) { + AbstractAccount abs = createAccountFromIdentifier(result.getString("identifier")); + if (abs.isCurrent()) { + PlayerAccount account = new PlayerAccountDB(abs, result.getInt("id")); + result.close(); + try { + // in order to ensure that, if the player was previously connected to another server, + // its datas have been fully pushed to database, we wait for 0,4 seconds + long timeout = 400 - (System.currentTimeMillis() - request.getJoinTimestamp()); + if (timeout > 0) wait(timeout); + }catch (InterruptedException e) { + e.printStackTrace(); + Thread.currentThread().interrupt(); + } + retrievePlayerDatas(account); + request.loaded(account, "database"); + return; + } + } + } + + if (request.mustCreateMissing()) { + try (PreparedStatement statement = + connection.prepareStatement(insertAccount, Statement.RETURN_GENERATED_KEYS)) { + AbstractAccount absacc = super.createAbstractAccount(request.getOnlinePlayer()); + statement.setString(1, absacc.getIdentifier()); + statement.setString(2, uuid); + statement.executeUpdate(); + ResultSet result = statement.getGeneratedKeys(); + if (!result.next()) + throw new SQLException("The plugin has not been able to create a player account."); + int index = result.getInt(1); // some drivers don't return a ResultSet with correct column names + request.created(new PlayerAccountDB(absacc, index)); + } + } else { + request.notLoaded(); + } + } catch (SQLException ex) { + BeautyQuests.logger.severe("An error occurred while loading account of " + request.getDebugPlayerName(), ex); + } + } + + @Override + protected synchronized void removeAccount(PlayerAccount acc) { + try (Connection connection = db.getConnection(); + PreparedStatement statement = connection.prepareStatement(deleteAccount)) { + statement.setInt(1, acc.index); + statement.executeUpdate(); + }catch (SQLException ex) { + ex.printStackTrace(); + } + } + + @Override + public PlayerQuestDatas createPlayerQuestDatas(PlayerAccount acc, Quest quest) { + return new PlayerQuestDatasDB(acc, quest.getID()); + } + + @Override + public synchronized void playerQuestDataRemoved(PlayerAccount acc, int id, PlayerQuestDatas datas) { + try (Connection connection = db.getConnection(); + PreparedStatement statement = connection.prepareStatement(removeQuestData)) { + ((PlayerQuestDatasDB) datas).stop(); + statement.setInt(1, acc.index); + statement.setInt(2, id); + statement.executeUpdate(); + }catch (SQLException e) { + e.printStackTrace(); + } + } + + @Override + public PlayerPoolDatas createPlayerPoolDatas(PlayerAccount acc, QuestPool pool) { + return new PlayerPoolDatasDB(acc, pool.getID()); + } + + @Override + public synchronized void playerPoolDataRemoved(PlayerAccount acc, int id, PlayerPoolDatas datas) { + try (Connection connection = db.getConnection(); + PreparedStatement statement = connection.prepareStatement(removePoolData)) { + statement.setInt(1, acc.index); + statement.setInt(2, id); + statement.executeUpdate(); + }catch (SQLException e) { + e.printStackTrace(); + } + } + + @Override + public synchronized int removeQuestDatas(Quest quest) { + int amount = 0; + try (Connection connection = db.getConnection(); + PreparedStatement statement = connection.prepareStatement(removeExistingQuestDatas)) { + for (PlayerAccount acc : PlayersManager.cachedAccounts.values()) { + PlayerQuestDatasDB datas = (PlayerQuestDatasDB) acc.removeQuestDatasSilently(quest.getID()); + if (datas != null) datas.stop(); + } + statement.setInt(1, quest.getID()); + amount += statement.executeUpdate(); + }catch (SQLException e) { + e.printStackTrace(); + } + DebugUtils.logMessage("Removed " + amount + " quest datas for quest " + quest.getID()); + return amount; + } + + public synchronized boolean hasAccounts(Player p) { + try (Connection connection = db.getConnection(); + PreparedStatement statement = connection.prepareStatement(getAccountsIDs)) { + statement.setString(1, p.getUniqueId().toString()); + ResultSet result = statement.executeQuery(); + boolean has = result.next(); + result.close(); + return has; + }catch (SQLException e) { + e.printStackTrace(); + } + return false; + } + + @Override + public void load() { + super.load(); + try { + createTables(); + + getAccountsIDs = "SELECT `id`, `identifier` FROM " + ACCOUNTS_TABLE + " WHERE `player_uuid` = ?"; + insertAccount = "INSERT INTO " + ACCOUNTS_TABLE + " (`identifier`, `player_uuid`) VALUES (?, ?)"; + deleteAccount = "DELETE FROM " + ACCOUNTS_TABLE + " WHERE `id` = ?"; + + insertQuestData = "INSERT INTO " + QUESTS_DATAS_TABLE + " (`account_id`, `quest_id`) VALUES (?, ?)"; + removeQuestData = "DELETE FROM " + QUESTS_DATAS_TABLE + " WHERE `account_id` = ? AND `quest_id` = ?"; + getQuestsData = "SELECT * FROM " + QUESTS_DATAS_TABLE + " WHERE `account_id` = ?"; + + removeExistingQuestDatas = "DELETE FROM " + QUESTS_DATAS_TABLE + " WHERE `quest_id` = ?"; + + updateFinished = prepareDatasStatement("finished"); + updateTimer = prepareDatasStatement("timer"); + updateBranch = prepareDatasStatement("current_branch"); + updateStage = prepareDatasStatement("current_stage"); + updateDatas = prepareDatasStatement("additional_datas"); + updateFlow = prepareDatasStatement("quest_flow"); + + insertPoolData = "INSERT INTO " + POOLS_DATAS_TABLE + " (`account_id`, `pool_id`) VALUES (?, ?)"; + removePoolData = "DELETE FROM " + POOLS_DATAS_TABLE + " WHERE `account_id` = ? AND `pool_id` = ?"; + getPoolData = "SELECT * FROM " + POOLS_DATAS_TABLE + " WHERE `account_id` = ?"; + getPoolAccountData = "SELECT 1 FROM " + POOLS_DATAS_TABLE + " WHERE `account_id` = ? AND `pool_id` = ?"; + + updatePoolLastGive = "UPDATE " + POOLS_DATAS_TABLE + " SET `last_give` = ? WHERE `account_id` = ? AND `pool_id` = ?"; + updatePoolCompletedQuests = "UPDATE " + POOLS_DATAS_TABLE + " SET `completed_quests` = ? WHERE `account_id` = ? AND `pool_id` = ?"; + }catch (SQLException e) { + throw new RuntimeException(e); + } + } + + private String prepareDatasStatement(String column) throws SQLException { + return "UPDATE " + QUESTS_DATAS_TABLE + " SET `" + column + "` = ? WHERE `id` = ?"; + } + + @Override + public void save() { + PlayersManager.cachedAccounts.values().forEach(x -> saveAccount(x, false)); + } + + private void createTables() throws SQLException { + try (Connection connection = db.getConnection(); Statement statement = connection.createStatement()) { + statement.execute("CREATE TABLE IF NOT EXISTS " + ACCOUNTS_TABLE + " (" + + " `id` int NOT NULL AUTO_INCREMENT ," + + " `identifier` text NOT NULL ," + + " `player_uuid` char(36) NOT NULL ," + + accountDatas.values().stream().map(data -> " " + data.getColumnDefinition() + " ,").collect(Collectors.joining()) + + " PRIMARY KEY (`id`)" + + " )"); + statement.execute("CREATE TABLE IF NOT EXISTS " + QUESTS_DATAS_TABLE + " (" + + " `id` int NOT NULL AUTO_INCREMENT ," + + " `account_id` int(11) NOT NULL," + + " `quest_id` int(11) NOT NULL," + + " `finished` INT(11) DEFAULT NULL," + + " `timer` bigint(20) DEFAULT NULL," + + " `current_branch` tinyint(4) DEFAULT NULL," + + " `current_stage` tinyint(4) DEFAULT NULL," + + " `additional_datas` longtext DEFAULT NULL," + + " `quest_flow` VARCHAR(8000) DEFAULT NULL," + + " PRIMARY KEY (`id`)" + + ")"); + statement.execute("CREATE TABLE IF NOT EXISTS " + POOLS_DATAS_TABLE + " (" + + "`id` int NOT NULL AUTO_INCREMENT, " + + "`account_id` int(11) NOT NULL, " + + "`pool_id` int(11) NOT NULL, " + + "`last_give` bigint(20) DEFAULT NULL, " + + "`completed_quests` varchar(1000) DEFAULT NULL, " + + "PRIMARY KEY (`id`)" + + ")"); + statement.execute("ALTER TABLE " + QUESTS_DATAS_TABLE + " MODIFY COLUMN finished INT(11) DEFAULT 0"); + + upgradeTable(connection, QUESTS_DATAS_TABLE, columns -> { + if (!columns.contains("quest_flow")) { // 0.19 + statement.execute("ALTER TABLE " + QUESTS_DATAS_TABLE + + " ADD COLUMN quest_flow VARCHAR(8000) DEFAULT NULL"); + BeautyQuests.logger.info("Updated database with quest_flow column."); + } + + if (!columns.contains("additional_datas") || columns.contains("stage_0_datas")) { // 0.20 + // tests for stage_0_datas: it's in the case the server crashed/stopped during the migration process. + if (!columns.contains("additional_datas")) { + statement.execute("ALTER TABLE " + QUESTS_DATAS_TABLE + + " ADD COLUMN `additional_datas` longtext DEFAULT NULL AFTER `current_stage`"); + BeautyQuests.logger.info("Updated table " + QUESTS_DATAS_TABLE + " with additional_datas column."); + } + + Utils.runAsync(this::migrateOldQuestDatas); + } + }); + + upgradeTable(connection, ACCOUNTS_TABLE, columns -> { + for (SQLDataSaver data : accountDatas.values()) { + if (!columns.contains(data.getWrappedData().getColumnName().toLowerCase())) { + statement.execute("ALTER TABLE " + ACCOUNTS_TABLE + + " ADD COLUMN " + data.getColumnDefinition()); + BeautyQuests.logger.info("Updated database by adding the missing " + data.getWrappedData().getColumnName() + " column in the player accounts table."); + } + } + }); + } + } + + private void upgradeTable(Connection connection, String tableName, ThrowingConsumer, SQLException> columnsConsumer) throws SQLException { + List columns = new ArrayList<>(14); + try (ResultSet set = connection.getMetaData().getColumns(db.getDatabase(), null, tableName, null)) { + while (set.next()) { + columns.add(set.getString("COLUMN_NAME").toLowerCase()); + } + } + if (columns.isEmpty()) { + BeautyQuests.logger.severe("Cannot check integrity of SQL table " + tableName); + }else { + columnsConsumer.accept(columns); + } + } + + private void migrateOldQuestDatas() { + BeautyQuests.logger.info("---- CAUTION ----\n" + + "BeautyQuests will now migrate old quest datas in database to the newest format.\n" + + "This may take a LONG time. Players should NOT enter the server during this time, " + + "or serious data loss can occur."); + + try (Connection connection = db.getConnection(); Statement statement = connection.createStatement()) { + + int deletedDuplicates = + statement.executeUpdate("DELETE R1 FROM " + QUESTS_DATAS_TABLE + " R1" + + " JOIN " + QUESTS_DATAS_TABLE + " R2" + + " ON R1.account_id = R2.account_id" + + " AND R1.quest_id = R2.quest_id" + + " AND R1.id < R2.id;"); + if (deletedDuplicates > 0) BeautyQuests.logger.info("Deleted " + deletedDuplicates + " duplicated rows in the " + QUESTS_DATAS_TABLE + " table."); + + int batchCount = 0; + PreparedStatement migration = connection.prepareStatement("UPDATE " + QUESTS_DATAS_TABLE + " SET `additional_datas` = ? WHERE `id` = ?"); + ResultSet result = statement.executeQuery("SELECT `id`, `stage_0_datas`, `stage_1_datas`, `stage_2_datas`, `stage_3_datas`, `stage_4_datas` FROM " + QUESTS_DATAS_TABLE); + while (result.next()) { + Map datas = new HashMap<>(); + for (int i = 0; i < 5; i++) { + String stageDatas = result.getString("stage_" + i + "_datas"); + if (stageDatas != null && !"{}".equals(stageDatas)) datas.put("stage" + i, CustomizedObjectTypeAdapter.deserializeNullable(stageDatas, Map.class)); + } + + if (datas.isEmpty()) continue; + migration.setString(1, CustomizedObjectTypeAdapter.serializeNullable(datas)); + migration.setInt(2, result.getInt("id")); + migration.addBatch(); + batchCount++; + } + BeautyQuests.logger.info("Migrating " + batchCount + "quest datas..."); + int migrated = migration.executeBatch().length; + BeautyQuests.logger.info("Migrated " + migrated + " quest datas."); + + statement.execute("ALTER TABLE " + QUESTS_DATAS_TABLE + + " DROP COLUMN `stage_0_datas`," + + " DROP COLUMN `stage_1_datas`," + + " DROP COLUMN `stage_2_datas`," + + " DROP COLUMN `stage_3_datas`," + + " DROP COLUMN `stage_4_datas`;"); + BeautyQuests.logger.info("Updated database by deleting old stage_[0::4]_datas columns."); + BeautyQuests.logger.info("---- CAUTION ----\n" + + "The data migration succeeded. Players can now safely connect."); + }catch (SQLException ex) { + BeautyQuests.logger.severe("---- CAUTION ----\n" + + "The plugin failed to migrate old quest datas in database.", ex); + } + } + + public static synchronized String migrate(Database db, PlayersManagerYAML yaml) throws SQLException { + try (Connection connection = db.getConnection()) { + ResultSet result = connection.getMetaData().getTables(null, null, "%", null); + while (result.next()) { + String tableName = result.getString(3); + if (tableName.equals("player_accounts") || tableName.equals("player_quests")) { + result.close(); + return "§cTable \"" + tableName + "\" already exists. Please drop it before migration."; + } + } + result.close(); + + PlayersManagerDB manager = new PlayersManagerDB(db); + manager.createTables(); + + PreparedStatement insertAccount = + connection.prepareStatement("INSERT INTO " + manager.ACCOUNTS_TABLE + " (`id`, `identifier`, `player_uuid`) VALUES (?, ?, ?)"); + PreparedStatement insertQuestData = + connection.prepareStatement("INSERT INTO " + manager.QUESTS_DATAS_TABLE + + " (`account_id`, `quest_id`, `finished`, `timer`, `current_branch`, `current_stage`, `stage_0_datas`, `stage_1_datas`, `stage_2_datas`, `stage_3_datas`, `stage_4_datas`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); + PreparedStatement insertPoolData = + connection.prepareStatement("INSERT INTO " + manager.POOLS_DATAS_TABLE + " (`account_id`, `pool_id`, `last_give`, `completed_quests`) VALUES (?, ?, ?, ?)"); + + int amount = 0, failed = 0; + yaml.loadAllAccounts(); + for (PlayerAccount acc : yaml.loadedAccounts.values()) { + try { + insertAccount.setInt(1, acc.index); + insertAccount.setString(2, acc.abstractAcc.getIdentifier()); + insertAccount.setString(3, acc.getOfflinePlayer().getUniqueId().toString()); + insertAccount.executeUpdate(); + + for (Entry entry : acc.questDatas.entrySet()) { + insertQuestData.setInt(1, acc.index); + insertQuestData.setInt(2, entry.getKey()); + insertQuestData.setInt(3, entry.getValue().getTimesFinished()); + insertQuestData.setLong(4, entry.getValue().getTimer()); + insertQuestData.setInt(5, entry.getValue().getBranch()); + insertQuestData.setInt(6, entry.getValue().getStage()); + for (int i = 0; i < 5; i++) { + Map stageDatas = entry.getValue().getStageDatas(i); + insertQuestData.setString(7 + i, stageDatas == null ? null : CustomizedObjectTypeAdapter.GSON.toJson(stageDatas)); + } + insertQuestData.executeUpdate(); + } + + for (Entry entry : acc.poolDatas.entrySet()) { + insertPoolData.setInt(1, acc.index); + insertPoolData.setInt(2, entry.getKey()); + insertPoolData.setLong(3, entry.getValue().getLastGive()); + insertPoolData.setString(4, getCompletedQuestsString(entry.getValue().getCompletedQuests())); + insertPoolData.executeUpdate(); + } + + amount++; + }catch (Exception ex) { + BeautyQuests.logger.severe("Failed to migrate datas for account " + acc.debugName(), ex); + failed++; + } + } + + insertAccount.close(); + insertQuestData.close(); + insertPoolData.close(); + + return "§aMigration succeed! " + amount + " accounts migrated, " + failed + " accounts failed to migrate.\n§oDatabase saving system is §lnot§r§a§o enabled. You need to reboot the server with the line \"database.enabled\" set to true."; + } + } + + @Override + public void unloadAccount(PlayerAccount acc) { + Utils.runAsync(() -> saveAccount(acc, true)); + } + + public void saveAccount(PlayerAccount acc, boolean stop) { + acc.getQuestsDatas() + .stream() + .map(PlayerQuestDatasDB.class::cast) + .forEach(x -> x.flushAll(stop)); + } + + protected static String getCompletedQuestsString(Set completedQuests) { + return completedQuests.isEmpty() ? null : completedQuests.stream().map(x -> Integer.toString(x)).collect(Collectors.joining(";")); + } + + public class PlayerQuestDatasDB extends PlayerQuestDatas { + + private static final int DATA_QUERY_TIMEOUT = 15; + private static final int DATA_FLUSHING_TIME = 10; + + private Map> cachedDatas = new HashMap<>(5); + private Lock datasLock = new ReentrantLock(); + private Lock dbLock = new ReentrantLock(); + private boolean disabled = false; + private int dbId = -1; + + public PlayerQuestDatasDB(PlayerAccount acc, int questID) { + super(acc, questID); + } + + public PlayerQuestDatasDB(PlayerAccount acc, int questID, ResultSet result) throws SQLException { + super( + acc, + questID, + result.getLong("timer"), + result.getInt("finished"), + result.getInt("current_branch"), + result.getInt("current_stage"), + CustomizedObjectTypeAdapter.deserializeNullable(result.getString("additional_datas"), Map.class), + result.getString("quest_flow")); + this.dbId = result.getInt("id"); + } + + @Override + public void incrementFinished() { + super.incrementFinished(); + setDataStatement(updateFinished, getTimesFinished(), false); + } + + @Override + public void setTimer(long timer) { + super.setTimer(timer); + setDataStatement(updateTimer, timer, false); + } + + @Override + public void setBranch(int branch) { + super.setBranch(branch); + setDataStatement(updateBranch, branch, false); + } + + @Override + public void setStage(int stage) { + super.setStage(stage); + setDataStatement(updateStage, stage, false); + } + + @Override + public T setAdditionalData(String key, T value) { + T additionalData = super.setAdditionalData(key, value); + setDataStatement(updateDatas, super.additionalDatas.isEmpty() ? null : CustomizedObjectTypeAdapter.serializeNullable(super.additionalDatas), true); + return additionalData; + } + + @Override + public void addQuestFlow(AbstractStage finished) { + super.addQuestFlow(finished); + setDataStatement(updateFlow, getQuestFlow(), true); + } + + @Override + public void resetQuestFlow() { + super.resetQuestFlow(); + setDataStatement(updateFlow, null, true); + } + + private void setDataStatement(String dataStatement, Object data, boolean allowNull) { + if (disabled) return; + try { + datasLock.lock(); + if (disabled) { + // in case disabled while acquiring lock + }else if (cachedDatas.containsKey(dataStatement)) { + cachedDatas.get(dataStatement).setValue(data); + }else { + BukkitRunnable runnable = new BukkitRunnable() { + + @Override + public void run() { + if (disabled) return; + Entry entry = null; + datasLock.lock(); + try { + if (!disabled) { // in case disabled while acquiring lock + entry = cachedDatas.remove(dataStatement); + } + }finally { + datasLock.unlock(); + } + if (entry != null) { + try { + if (dbLock.tryLock(DATA_QUERY_TIMEOUT, TimeUnit.SECONDS)) { + try (Connection connection = db.getConnection()) { + if (dbId == -1) createDataRow(connection); + try (PreparedStatement statement = connection.prepareStatement(dataStatement)) { + statement.setObject(1, entry.getValue()); + statement.setInt(2, dbId); + statement.setQueryTimeout(DATA_QUERY_TIMEOUT); + statement.executeUpdate(); + if (entry.getValue() == null && !allowNull) { + BeautyQuests.logger.warning("Setting an illegal NULL value in statement \"" + dataStatement + "\" for account " + acc.index + " and quest " + questID); + } + } + }catch (SQLException ex) { + BeautyQuests.logger.severe("An error occurred while updating a player's quest datas.", ex); + }finally { + dbLock.unlock(); + } + }else { + BeautyQuests.logger.severe("Cannot acquire database lock for quest " + questID + ", player " + acc.getNameAndID()); + } + }catch (InterruptedException ex) { + BeautyQuests.logger.severe("Interrupted database locking.", ex); + Thread.currentThread().interrupt(); + } + } + } + }; + runnable.runTaskLaterAsynchronously(BeautyQuests.getInstance(), DATA_FLUSHING_TIME); + cachedDatas.put(dataStatement, new AbstractMap.SimpleEntry<>(runnable, data)); + } + }finally { + datasLock.unlock(); + } + } + + protected void flushAll(boolean stop) { + try { + if (datasLock.tryLock(DATA_QUERY_TIMEOUT * 2L, TimeUnit.SECONDS)) { + cachedDatas.values().stream().map(Entry::getKey).collect(Collectors.toList()) // to prevent ConcurrentModificationException + .forEach(run -> { + run.cancel(); + run.run(); + }); + if (!cachedDatas.isEmpty()) BeautyQuests.logger.warning("Still waiting values in quest data " + questID + " for account " + acc.index + " despite flushing all."); + if (stop) disabled = true; + datasLock.unlock(); + }else { + BeautyQuests.logger.severe("Cannot acquire database lock to save all datas of quest " + questID + ", player " + acc.getNameAndID()); + } + }catch (InterruptedException ex) { + BeautyQuests.logger.severe("Interrupted database locking.", ex); + Thread.currentThread().interrupt(); + } + } + + protected void stop() { + disabled = true; + datasLock.lock(); + cachedDatas.values() + .stream() + .map(Entry::getKey) + .forEach(BukkitRunnable::cancel); + cachedDatas.clear(); + datasLock.unlock(); + } + + private void createDataRow(Connection connection) throws SQLException { + DebugUtils.logMessage("Inserting DB row of quest " + questID + " for account " + acc.index); + try (PreparedStatement insertStatement = connection.prepareStatement(insertQuestData, Statement.RETURN_GENERATED_KEYS)) { + insertStatement.setInt(1, acc.index); + insertStatement.setInt(2, questID); + insertStatement.setQueryTimeout(DATA_QUERY_TIMEOUT); + insertStatement.executeUpdate(); + ResultSet generatedKeys = insertStatement.getGeneratedKeys(); + generatedKeys.next(); + dbId = generatedKeys.getInt(1); + DebugUtils.logMessage("Created row " + dbId + " for quest " + questID + ", account " + acc.index); + } + } + + } + + public class PlayerPoolDatasDB extends PlayerPoolDatas { + + public PlayerPoolDatasDB(PlayerAccount acc, int poolID) { + super(acc, poolID); + } + + public PlayerPoolDatasDB(PlayerAccount acc, int poolID, long lastGive, Set completedQuests) { + super(acc, poolID, lastGive, completedQuests); + } + + @Override + public void setLastGive(long lastGive) { + super.setLastGive(lastGive); + updateData(updatePoolLastGive, lastGive); + } + + @Override + public void updatedCompletedQuests() { + updateData(updatePoolCompletedQuests, getCompletedQuestsString(getCompletedQuests())); + } + + private void updateData(String dataStatement, Object data) { + try (Connection connection = db.getConnection()) { + try (PreparedStatement statement = connection.prepareStatement(getPoolAccountData)) { + statement.setInt(1, acc.index); + statement.setInt(2, poolID); + if (!statement.executeQuery().next()) { // if result set empty => need to insert data then update + try (PreparedStatement insertStatement = connection.prepareStatement(insertPoolData)) { + insertStatement.setInt(1, acc.index); + insertStatement.setInt(2, poolID); + insertStatement.executeUpdate(); + } + } + } + try (PreparedStatement statement = connection.prepareStatement(dataStatement)) { + statement.setObject(1, data); + statement.setInt(2, acc.index); + statement.setInt(3, poolID); + statement.executeUpdate(); + } + }catch (SQLException ex) { + BeautyQuests.logger.severe("An error occurred while updating a player's pool datas.", ex); + } + } + + } + + public class PlayerAccountDB extends PlayerAccount { + + public PlayerAccountDB(AbstractAccount account, int index) { + super(account, index); + } + + @Override + public void setData(SavableData data, T value) { + super.setData(data, value); + + SQLDataSaver dataSaver = (SQLDataSaver) accountDatas.get(data); + try (Connection connection = db.getConnection(); + PreparedStatement statement = connection.prepareStatement(dataSaver.getUpdateStatement())) { + dataSaver.setInStatement(statement, 1, value); + statement.setInt(2, index); + statement.executeUpdate(); + }catch (SQLException ex) { + BeautyQuests.logger.severe("An error occurred while saving account data " + data.getId() + " to database", ex); + } + } + + @Override + public void resetDatas() { + super.resetDatas(); + + if (resetAccountDatas != null) { + try (Connection connection = db.getConnection(); + PreparedStatement statement = connection.prepareStatement(resetAccountDatas)) { + statement.setInt(1, index); + statement.executeUpdate(); + }catch (SQLException ex) { + BeautyQuests.logger.severe("An error occurred while resetting account " + index + " datas from database", ex); + } + } + } + + } + +} diff --git a/core/src/main/java/fr/skytasul/quests/players/PlayersManagerYAML.java b/core/src/main/java/fr/skytasul/quests/players/PlayersManagerYAML.java index 185c6fec..61662dcd 100644 --- a/core/src/main/java/fr/skytasul/quests/players/PlayersManagerYAML.java +++ b/core/src/main/java/fr/skytasul/quests/players/PlayersManagerYAML.java @@ -1,312 +1,341 @@ -package fr.skytasul.quests.players; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.util.AbstractMap; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.concurrent.TimeUnit; - -import org.apache.commons.lang.Validate; -import org.bukkit.Bukkit; -import org.bukkit.configuration.ConfigurationSection; -import org.bukkit.configuration.file.FileConfiguration; -import org.bukkit.configuration.file.YamlConfiguration; -import org.bukkit.entity.Player; - -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; - -import fr.skytasul.quests.BeautyQuests; -import fr.skytasul.quests.players.accounts.AbstractAccount; -import fr.skytasul.quests.players.accounts.GhostAccount; -import fr.skytasul.quests.structure.Quest; -import fr.skytasul.quests.structure.pools.QuestPool; -import fr.skytasul.quests.utils.DebugUtils; -import fr.skytasul.quests.utils.Utils; - -public class PlayersManagerYAML extends PlayersManager { - - private static final int ACCOUNTS_THRESHOLD = 1000; - - Map loadedAccounts = new HashMap<>(); - private Map identifiersIndex = Collections.synchronizedMap(new HashMap<>()); - private int lastAccountID = 0; - - private Cache unloadedAccounts = CacheBuilder.newBuilder().expireAfterWrite(2, TimeUnit.MINUTES).build(); - - private File directory = new File(BeautyQuests.getInstance().getDataFolder(), "players"); - - @Override - protected Entry load(Player player, long joinTimestamp) { - String identifier = super.getIdentifier(player); - if (identifiersIndex.containsValue(identifier)) { - int id = Utils.getKeyByValue(identifiersIndex, identifier); - return new AbstractMap.SimpleEntry<>(getByIndex(id), false); - } - - AbstractAccount absacc = super.createAbstractAccount(player); - PlayerAccount acc = new PlayerAccount(absacc, lastAccountID + 1); - addAccount(acc); - - return new AbstractMap.SimpleEntry<>(acc, true); - } - - @Override - protected void removeAccount(PlayerAccount acc) { - loadedAccounts.remove(acc.index); - identifiersIndex.remove(acc.index); - removePlayerFile(acc.index); - } - - @Override - public PlayerQuestDatas createPlayerQuestDatas(PlayerAccount acc, Quest quest) { - return new PlayerQuestDatas(acc, quest.getID()); - } - - @Override - public PlayerPoolDatas createPlayerPoolDatas(PlayerAccount acc, QuestPool pool) { - return new PlayerPoolDatas(acc, pool.getID()); - } - - @Override - public int removeQuestDatas(Quest quest) { - loadAllAccounts(); - int amount = 0; - - for (PlayerAccount account : loadedAccounts.values()) { - if (account.removeQuestDatas(quest) != null) amount++; - } - - return amount; - } - - public boolean hasAccounts(Player p) { - return identifiersIndex.containsValue(getIdentifier(p)); - } - - private synchronized PlayerAccount createPlayerAccount(String identifier, int index) { - Validate.notNull(identifier, "Identifier cannot be null (index: " + index + ")"); - AbstractAccount abs = super.createAccountFromIdentifier(identifier); - if (abs == null) { - BeautyQuests.logger.info("Player account with identifier " + identifier + " is not enabled, but will be kept in the data file."); - return new PlayerAccount(new GhostAccount(identifier), index); - } - return new PlayerAccount(abs, index); - } - - void loadAllAccounts() { - BeautyQuests.getInstance().getLogger().warning("CAUTION - BeautyQuests will now load every single player data into the server's memory. We HIGHLY recommend the server to be restarted at the end of the operation. Be prepared to experience some lags."); - for (Entry entry : identifiersIndex.entrySet()) { - if (loadedAccounts.containsKey(entry.getKey())) continue; - try { - PlayerAccount acc = loadFromFile(entry.getKey(), false); - if (acc == null) { - acc = createPlayerAccount(entry.getValue(), entry.getKey()); - addAccount(acc); - } - }catch (Exception ex) { - BeautyQuests.logger.severe("An error occured when loading player account " + entry.getKey(), ex); - } - } - BeautyQuests.getInstance().getLogger().info("Total loaded accounts: " + loadedAccounts.size()); - } - - public void debugDuplicate() { - for (Player p : Bukkit.getOnlinePlayers()) { - p.kickPlayer("§cCleanup operation."); - } - PlayersManager.cachedAccounts.clear(); - - loadAllAccounts(); - int amount = 0; - - Map> playerAccounts = new HashMap<>(); - for (PlayerAccount acc : loadedAccounts.values()) { - List list = playerAccounts.get(acc.abstractAcc.getIdentifier()); - if (list == null) { - list = new ArrayList<>(); - playerAccounts.put(acc.abstractAcc.getIdentifier(), list); - } - list.add(acc); - } - BeautyQuests.getInstance().getLogger().info(playerAccounts.size() + " unique identifiers."); - - List removed = new ArrayList<>(); - for (Entry> en : playerAccounts.entrySet()) { - if (removed.contains(en.getKey())) System.out.println("CRITICAL - Already removed " + en.getKey()); - - List list = en.getValue(); - - int maxID = 0; - int maxSize = 0; - for (int i = 0; i < list.size(); i++) { - PlayerAccount acc = list.get(i); - if (acc.questDatas.size() > maxSize) { - maxID = i; - maxSize = acc.questDatas.size(); - } - } - for (int i = 0; i < list.size(); i++) { - if (i != maxID) { - PlayerAccount acc = list.get(i); - int index = Utils.getKeyByValue(loadedAccounts, acc); - loadedAccounts.remove(index); - identifiersIndex.remove(index); - removePlayerFile(index); - amount++; - } - } - removed.add(en.getKey()); - } - - BeautyQuests.getInstance().getLogger().info(amount + " duplicated accounts removeds. Total loaded accounts/identifiers: " + loadedAccounts.size() + "/" + identifiersIndex.size()); - BeautyQuests.getInstance().getLogger().info("Now scanning for remaining duplicated accounts..."); - boolean dup = false; - for (String id : identifiersIndex.values()) { - int size = Utils.getKeysByValue(identifiersIndex, id).size(); - if (size != 1) { - dup = true; - System.out.println(size + " accounts with identifier " + id); - } - } - if (dup) BeautyQuests.getInstance().getLogger().warning("There is still duplicated accounts."); - BeautyQuests.getInstance().getLogger().info("Operation complete."); - } - - public PlayerAccount getByIndex(Object index) { // TODO remove on 0.19 - int id = index instanceof Integer ? (int) index : Utils.parseInt(index); - PlayerAccount acc = loadedAccounts.get(id); - if (acc != null) return acc; - acc = unloadedAccounts.asMap().remove(id); - if (acc != null) { - loadedAccounts.put(id, acc); - return acc; - } - acc = loadFromFile(id, true); - if (acc != null) return acc; - acc = createPlayerAccount(identifiersIndex.get(id), id); - addAccount(acc); - return acc; - } - - private synchronized void addAccount(PlayerAccount acc) { - loadedAccounts.put(acc.index, acc); - identifiersIndex.put(acc.index, acc.abstractAcc.getIdentifier()); - if (acc.index >= lastAccountID) lastAccountID = acc.index; - } - - public PlayerAccount loadFromFile(int index, boolean msg) { - File file = new File(directory, index + ".yml"); - if (!file.exists()) return null; - DebugUtils.logMessage("Loading account #" + index + ". Last file edition: " + new Date(file.lastModified()).toString()); - YamlConfiguration playerConfig = YamlConfiguration.loadConfiguration(file); - return loadFromConfig(index, playerConfig); - } - - private PlayerAccount loadFromConfig(int index, ConfigurationSection datas) { - String identifier = datas.getString("identifier"); - if (identifier == null) { - BeautyQuests.logger.warning("No identifier found in file for index " + index + "."); - identifier = identifiersIndex.get(index); - } - PlayerAccount acc = createPlayerAccount(identifier, index); - for (Map questConfig : datas.getMapList("quests")) { - PlayerQuestDatas questDatas = PlayerQuestDatas.deserialize(acc, (Map) questConfig); - acc.questDatas.put(questDatas.questID, questDatas); - } - for (Map poolConfig : datas.getMapList("pools")) { - PlayerPoolDatas questDatas = PlayerPoolDatas.deserialize(acc, (Map) poolConfig); - acc.poolDatas.put(questDatas.getPoolID(), questDatas); - } - addAccount(acc); - return acc; - } - - public void savePlayerFile(PlayerAccount acc) throws IOException { - File file = new File(directory, acc.index + ".yml"); - file.createNewFile(); - YamlConfiguration playerConfig = YamlConfiguration.loadConfiguration(file); - acc.serialize(playerConfig); - playerConfig.save(file); - } - - public void removePlayerFile(int index) { - File file = new File(directory, index + ".yml"); - if (file.exists()) { - try { - Files.delete(file.toPath()); - DebugUtils.logMessage("Removed " + file.getName()); - }catch (IOException e) { - e.printStackTrace(); - } - }else DebugUtils.logMessage("Can't remove " + file.getName() + ": file does not exist"); - } - - @Override - public void load() { - if (!directory.exists()) directory.mkdirs(); - - FileConfiguration config = BeautyQuests.getInstance().getDataFile(); - if (config.isConfigurationSection("players")) { - for (String key : config.getConfigurationSection("players").getKeys(false)) { - try { - String path = "players." + key; - int index = Integer.parseInt(key); - identifiersIndex.put(index, config.getString(path)); - if (index >= lastAccountID) lastAccountID = index; - }catch (Exception ex) { - BeautyQuests.logger.severe("An error occured while loading player account. Data: " + config.get(key), ex); - } - } - } - DebugUtils.logMessage(loadedAccounts.size() + " accounts loaded and " + identifiersIndex.size() + " identifiers."); - - if (identifiersIndex.size() >= ACCOUNTS_THRESHOLD) { - BeautyQuests.logger.warning( - "⚠ WARNING - " + identifiersIndex.size() + " players are registered on this server." - + " It is recommended to switch to a SQL database setup in order to keep proper performances and scalability." - + " In order to do that, setup your database credentials in config.yml (without enabling it) and run the command" - + " /quests migrateDatas. Then follow steps on screen."); - } - } - - @Override - public synchronized void save() { - DebugUtils.logMessage("Saving " + loadedAccounts.size() + " loaded accounts and " + identifiersIndex.size() + " identifiers."); - - BeautyQuests.getInstance().getDataFile().set("players", identifiersIndex); - - // as the save can take a few seconds and MAY be done asynchronously, - // it is possible that the "loadedAccounts" map is being edited concurrently. - // therefore, we create a new list to avoid this issue. - ArrayList accountToSave = new ArrayList<>(loadedAccounts.values()); - for (PlayerAccount acc : accountToSave) { - try { - savePlayerFile(acc); - }catch (Exception e) { - BeautyQuests.logger.severe("An error ocurred while trying to save " + acc.debugName() + " account file", e); - } - } - } - - @Override - public void unloadAccount(PlayerAccount acc) { - loadedAccounts.remove(acc.index); - unloadedAccounts.put(acc.index, acc); - Utils.runAsync(() -> { - try { - savePlayerFile(acc); - }catch (IOException e) { - BeautyQuests.logger.warning("An error ocurred while saving player file " + acc.debugName(), e); - } - }); - } - -} +package fr.skytasul.quests.players; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.TimeUnit; +import org.apache.commons.lang.Validate; +import org.bukkit.Bukkit; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import fr.skytasul.quests.BeautyQuests; +import fr.skytasul.quests.api.data.SavableData; +import fr.skytasul.quests.players.accounts.AbstractAccount; +import fr.skytasul.quests.players.accounts.GhostAccount; +import fr.skytasul.quests.structure.Quest; +import fr.skytasul.quests.structure.pools.QuestPool; +import fr.skytasul.quests.utils.DebugUtils; +import fr.skytasul.quests.utils.Utils; + +public class PlayersManagerYAML extends PlayersManager { + + private static final int ACCOUNTS_THRESHOLD = 1000; + + private final Cache unloadedAccounts = CacheBuilder.newBuilder().expireAfterWrite(2, TimeUnit.MINUTES).build(); + + protected final Map loadedAccounts = new HashMap<>(); + private final Map identifiersIndex = Collections.synchronizedMap(new HashMap<>()); + + private final File directory = new File(BeautyQuests.getInstance().getDataFolder(), "players"); + + private int lastAccountID = 0; + + public File getDirectory() { + return directory; + } + + @Override + public void load(AccountFetchRequest request) { + String identifier = super.getIdentifier(request.getOfflinePlayer()); + if (identifiersIndex.containsValue(identifier)) { + int id = Utils.getKeyByValue(identifiersIndex, identifier); + PlayerAccount acc; + + // 1. get the account if it's already loaded + acc = loadedAccounts.get(id); + if (acc != null) { + request.loaded(acc, "cached accounts"); + } else { + + // 2. get the account from the "pending unload" list + acc = request.shouldCache() ? unloadedAccounts.asMap().remove(id) : unloadedAccounts.getIfPresent(id); + if (acc != null) { + if (request.shouldCache()) + loadedAccounts.put(id, acc); + request.loaded(acc, "cached accounts pending unload"); + } else { + + // 3. load the account from the corresponding file + acc = loadFromFile(id, true); + if (acc != null) { + if (request.shouldCache()) + loadedAccounts.put(id, acc); + request.loaded(acc, "file from index"); + } else { + + // 4. that's pretty bizarre: the account's identifier + // has an associated index, but no saved datas can be found. + if (request.mustCreateMissing()) { + acc = createPlayerAccount(identifier, id); + if (request.shouldCache()) + addAccount(acc); + request.created(acc); + } else { + request.notLoaded(); + } + } + } + } + } else if (request.mustCreateMissing()) { + AbstractAccount absacc = super.createAbstractAccount(request.getOnlinePlayer()); + PlayerAccount acc = new PlayerAccount(absacc, lastAccountID + 1); + if (request.shouldCache()) + addAccount(acc); + + request.created(acc); + } else { + request.notLoaded(); + } + } + + @Override + protected void removeAccount(PlayerAccount acc) { + loadedAccounts.remove(acc.index); + identifiersIndex.remove(acc.index); + removePlayerFile(acc.index); + } + + @Override + public PlayerQuestDatas createPlayerQuestDatas(PlayerAccount acc, Quest quest) { + return new PlayerQuestDatas(acc, quest.getID()); + } + + @Override + public PlayerPoolDatas createPlayerPoolDatas(PlayerAccount acc, QuestPool pool) { + return new PlayerPoolDatas(acc, pool.getID()); + } + + @Override + public int removeQuestDatas(Quest quest) { + loadAllAccounts(); + int amount = 0; + + for (PlayerAccount account : loadedAccounts.values()) { + if (account.removeQuestDatas(quest) != null) amount++; + } + + return amount; + } + + public boolean hasAccounts(Player p) { + return identifiersIndex.containsValue(getIdentifier(p)); + } + + private synchronized PlayerAccount createPlayerAccount(String identifier, int index) { + Validate.notNull(identifier, "Identifier cannot be null (index: " + index + ")"); + AbstractAccount abs = super.createAccountFromIdentifier(identifier); + if (abs == null) { + BeautyQuests.logger.info("Player account with identifier " + identifier + " is not enabled, but will be kept in the data file."); + return new PlayerAccount(new GhostAccount(identifier), index); + } + return new PlayerAccount(abs, index); + } + + void loadAllAccounts() { + BeautyQuests.getInstance().getLogger().warning("CAUTION - BeautyQuests will now load every single player data into the server's memory. We HIGHLY recommend the server to be restarted at the end of the operation. Be prepared to experience some lags."); + for (Entry entry : identifiersIndex.entrySet()) { + if (loadedAccounts.containsKey(entry.getKey())) continue; + try { + PlayerAccount acc = loadFromFile(entry.getKey(), false); + if (acc == null) + acc = createPlayerAccount(entry.getValue(), entry.getKey()); + addAccount(acc); + }catch (Exception ex) { + BeautyQuests.logger.severe("An error occured when loading player account " + entry.getKey(), ex); + } + } + BeautyQuests.getInstance().getLogger().info("Total loaded accounts: " + loadedAccounts.size()); + } + + public void debugDuplicate() { + for (Player p : Bukkit.getOnlinePlayers()) { + p.kickPlayer("§cCleanup operation."); + } + PlayersManager.cachedAccounts.clear(); + + loadAllAccounts(); + int amount = 0; + + Map> playerAccounts = new HashMap<>(); + for (PlayerAccount acc : loadedAccounts.values()) { + List list = playerAccounts.get(acc.abstractAcc.getIdentifier()); + if (list == null) { + list = new ArrayList<>(); + playerAccounts.put(acc.abstractAcc.getIdentifier(), list); + } + list.add(acc); + } + BeautyQuests.getInstance().getLogger().info(playerAccounts.size() + " unique identifiers."); + + List removed = new ArrayList<>(); + for (Entry> en : playerAccounts.entrySet()) { + if (removed.contains(en.getKey())) System.out.println("CRITICAL - Already removed " + en.getKey()); + + List list = en.getValue(); + + int maxID = 0; + int maxSize = 0; + for (int i = 0; i < list.size(); i++) { + PlayerAccount acc = list.get(i); + if (acc.questDatas.size() > maxSize) { + maxID = i; + maxSize = acc.questDatas.size(); + } + } + for (int i = 0; i < list.size(); i++) { + if (i != maxID) { + PlayerAccount acc = list.get(i); + int index = Utils.getKeyByValue(loadedAccounts, acc); + loadedAccounts.remove(index); + identifiersIndex.remove(index); + removePlayerFile(index); + amount++; + } + } + removed.add(en.getKey()); + } + + BeautyQuests.getInstance().getLogger().info(amount + " duplicated accounts removeds. Total loaded accounts/identifiers: " + loadedAccounts.size() + "/" + identifiersIndex.size()); + BeautyQuests.getInstance().getLogger().info("Now scanning for remaining duplicated accounts..."); + boolean dup = false; + for (String id : identifiersIndex.values()) { + int size = Utils.getKeysByValue(identifiersIndex, id).size(); + if (size != 1) { + dup = true; + System.out.println(size + " accounts with identifier " + id); + } + } + if (dup) BeautyQuests.getInstance().getLogger().warning("There is still duplicated accounts."); + BeautyQuests.getInstance().getLogger().info("Operation complete."); + } + + private synchronized void addAccount(PlayerAccount acc) { + loadedAccounts.put(acc.index, acc); + identifiersIndex.put(acc.index, acc.abstractAcc.getIdentifier()); + if (acc.index >= lastAccountID) lastAccountID = acc.index; + } + + private PlayerAccount loadFromFile(int index, boolean msg) { + File file = new File(directory, index + ".yml"); + if (!file.exists()) return null; + DebugUtils.logMessage("Loading account #" + index + ". Last file edition: " + new Date(file.lastModified()).toString()); + YamlConfiguration playerConfig = YamlConfiguration.loadConfiguration(file); + return loadFromConfig(index, playerConfig); + } + + private PlayerAccount loadFromConfig(int index, ConfigurationSection datas) { + String identifier = datas.getString("identifier"); + if (identifier == null) { + BeautyQuests.logger.warning("No identifier found in file for index " + index + "."); + identifier = identifiersIndex.get(index); + } + PlayerAccount acc = createPlayerAccount(identifier, index); + for (Map questConfig : datas.getMapList("quests")) { + PlayerQuestDatas questDatas = PlayerQuestDatas.deserialize(acc, (Map) questConfig); + acc.questDatas.put(questDatas.questID, questDatas); + } + for (Map poolConfig : datas.getMapList("pools")) { + PlayerPoolDatas questDatas = PlayerPoolDatas.deserialize(acc, (Map) poolConfig); + acc.poolDatas.put(questDatas.getPoolID(), questDatas); + } + for (SavableData data : accountDatas) { + if (datas.contains(data.getId())) { + acc.additionalDatas.put(data, datas.getObject(data.getId(), data.getDataType())); + } + } + return acc; + } + + public void savePlayerFile(PlayerAccount acc) throws IOException { + File file = new File(directory, acc.index + ".yml"); + file.createNewFile(); + YamlConfiguration playerConfig = YamlConfiguration.loadConfiguration(file); + acc.serialize(playerConfig); + playerConfig.save(file); + } + + public void removePlayerFile(int index) { + File file = new File(directory, index + ".yml"); + if (file.exists()) { + try { + Files.delete(file.toPath()); + DebugUtils.logMessage("Removed " + file.getName()); + }catch (IOException e) { + e.printStackTrace(); + } + }else DebugUtils.logMessage("Can't remove " + file.getName() + ": file does not exist"); + } + + @Override + public void load() { + super.load(); + if (!directory.exists()) directory.mkdirs(); + + FileConfiguration config = BeautyQuests.getInstance().getDataFile(); + if (config.isConfigurationSection("players")) { + for (String key : config.getConfigurationSection("players").getKeys(false)) { + try { + String path = "players." + key; + int index = Integer.parseInt(key); + identifiersIndex.put(index, config.getString(path)); + if (index >= lastAccountID) lastAccountID = index; + }catch (Exception ex) { + BeautyQuests.logger.severe("An error occured while loading player account. Data: " + config.get(key), ex); + } + } + } + DebugUtils.logMessage(loadedAccounts.size() + " accounts loaded and " + identifiersIndex.size() + " identifiers."); + + if (identifiersIndex.size() >= ACCOUNTS_THRESHOLD) { + BeautyQuests.logger.warning( + "⚠ WARNING - " + identifiersIndex.size() + " players are registered on this server." + + " It is recommended to switch to a SQL database setup in order to keep proper performances and scalability." + + " In order to do that, setup your database credentials in config.yml (without enabling it) and run the command" + + " /quests migrateDatas. Then follow steps on screen."); + } + } + + @Override + public synchronized void save() { + DebugUtils.logMessage("Saving " + loadedAccounts.size() + " loaded accounts and " + identifiersIndex.size() + " identifiers."); + + BeautyQuests.getInstance().getDataFile().set("players", identifiersIndex); + + // as the save can take a few seconds and MAY be done asynchronously, + // it is possible that the "loadedAccounts" map is being edited concurrently. + // therefore, we create a new list to avoid this issue. + ArrayList accountToSave = new ArrayList<>(loadedAccounts.values()); + for (PlayerAccount acc : accountToSave) { + try { + savePlayerFile(acc); + }catch (Exception e) { + BeautyQuests.logger.severe("An error ocurred while trying to save " + acc.debugName() + " account file", e); + } + } + } + + @Override + public void unloadAccount(PlayerAccount acc) { + loadedAccounts.remove(acc.index); + unloadedAccounts.put(acc.index, acc); + Utils.runAsync(() -> { + try { + savePlayerFile(acc); + }catch (IOException e) { + BeautyQuests.logger.warning("An error ocurred while saving player file " + acc.debugName(), e); + } + }); + } + +} diff --git a/core/src/main/java/fr/skytasul/quests/players/events/PlayerAccountEvent.java b/core/src/main/java/fr/skytasul/quests/players/events/PlayerAccountEvent.java deleted file mode 100644 index ed0961ce..00000000 --- a/core/src/main/java/fr/skytasul/quests/players/events/PlayerAccountEvent.java +++ /dev/null @@ -1,20 +0,0 @@ -package fr.skytasul.quests.players.events; - -import org.bukkit.entity.Player; -import org.bukkit.event.player.PlayerEvent; - -import fr.skytasul.quests.players.PlayerAccount; - -public abstract class PlayerAccountEvent extends PlayerEvent { - - protected PlayerAccount account; - - public PlayerAccountEvent(Player who) { - super(who); - } - - public PlayerAccount getPlayerAccount() { - return account; - } - -} \ No newline at end of file diff --git a/core/src/main/java/fr/skytasul/quests/requirements/ClassRequirement.java b/core/src/main/java/fr/skytasul/quests/requirements/ClassRequirement.java index e3a4c4ec..fc9e142c 100644 --- a/core/src/main/java/fr/skytasul/quests/requirements/ClassRequirement.java +++ b/core/src/main/java/fr/skytasul/quests/requirements/ClassRequirement.java @@ -2,10 +2,11 @@ import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.function.Function; +import java.util.stream.Collectors; import org.bukkit.DyeColor; +import org.bukkit.configuration.ConfigurationSection; import org.bukkit.entity.Player; import org.bukkit.event.inventory.ClickType; import org.bukkit.inventory.ItemStack; @@ -20,8 +21,6 @@ import fr.skytasul.quests.gui.templates.ListGUI; import fr.skytasul.quests.gui.templates.PagedGUI; import fr.skytasul.quests.utils.Lang; -import fr.skytasul.quests.utils.compatibility.DependenciesManager; -import fr.skytasul.quests.utils.compatibility.MissingDependencyException; import fr.skytasul.quests.utils.compatibility.SkillAPI; public class ClassRequirement extends AbstractRequirement { @@ -33,7 +32,6 @@ public ClassRequirement() { } public ClassRequirement(List classes) { - if (!DependenciesManager.skapi.isEnabled()) throw new MissingDependencyException("SkillAPI"); this.classes = classes; } @@ -95,7 +93,6 @@ public void click(RPGClass existing, ItemStack item, ClickType clickType) { @Override public void finish(List objects) { classes = objects; - event.updateItemLore(getLore()); event.reopenGUI(); } @@ -108,19 +105,15 @@ public AbstractRequirement clone() { } @Override - protected void save(Map datas) { - if (classes.isEmpty()) return; - List ls = new ArrayList<>(); - for (RPGClass cl : classes) { - ls.add(cl.getName()); - } - datas.put("classes", ls); + public void save(ConfigurationSection section) { + if (!classes.isEmpty()) + section.set("classes", classes.stream().map(RPGClass::getName).collect(Collectors.toList())); } @Override - protected void load(Map savedDatas) { - if (!savedDatas.containsKey("classes")) return; - for (String s : (List) savedDatas.get("classes")) { + public void load(ConfigurationSection section) { + if (!section.contains("classes")) return; + for (String s : section.getStringList("classes")) { RPGClass classe = com.sucy.skill.SkillAPI.getClasses().get(s.toLowerCase()); if (classe == null) { BeautyQuests.getInstance().getLogger().warning("Class with name " + s + " no longer exists."); diff --git a/core/src/main/java/fr/skytasul/quests/requirements/EquipmentRequirement.java b/core/src/main/java/fr/skytasul/quests/requirements/EquipmentRequirement.java new file mode 100644 index 00000000..d6ea7c5b --- /dev/null +++ b/core/src/main/java/fr/skytasul/quests/requirements/EquipmentRequirement.java @@ -0,0 +1,110 @@ +package fr.skytasul.quests.requirements; + +import java.util.Map; +import java.util.function.Consumer; + +import org.bukkit.DyeColor; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.entity.Player; +import org.bukkit.inventory.EquipmentSlot; +import org.bukkit.inventory.ItemStack; + +import com.google.common.collect.ImmutableMap; + +import fr.skytasul.quests.api.comparison.ItemComparisonMap; +import fr.skytasul.quests.api.objects.QuestObjectClickEvent; +import fr.skytasul.quests.api.options.QuestOption; +import fr.skytasul.quests.api.requirements.AbstractRequirement; +import fr.skytasul.quests.gui.ItemUtils; +import fr.skytasul.quests.gui.misc.ItemComparisonGUI; +import fr.skytasul.quests.gui.misc.ItemGUI; +import fr.skytasul.quests.gui.templates.StaticPagedGUI; +import fr.skytasul.quests.utils.Lang; +import fr.skytasul.quests.utils.XMaterial; + +public class EquipmentRequirement extends AbstractRequirement { + + private EquipmentSlot slot; + private ItemStack item; + private ItemComparisonMap comparisons; + + public EquipmentRequirement() {} + + public EquipmentRequirement(EquipmentSlot slot, ItemStack item, ItemComparisonMap comparisons) { + this.slot = slot; + this.item = item; + this.comparisons = comparisons; + } + + @Override + public boolean test(Player p) { + return comparisons.isSimilar(p.getInventory().getItem(slot), item); + } + + @Override + public AbstractRequirement clone() { + return new EquipmentRequirement(slot, item, comparisons); + } + + @Override + public String[] getLore() { + if (slot == null) return null; + return new String[] { + QuestOption.formatNullableValue(slot.name() + " > " + ItemUtils.getName(item)), + "", + Lang.RemoveMid.toString() }; + } + + @Override + public void itemClick(QuestObjectClickEvent event) { + if (event.isInCreation()) comparisons = new ItemComparisonMap(); + + new EquipmentSlotGUI(newSlot -> { + if (newSlot == null) { + event.cancel(); + return; + } + + new ItemGUI(newItem -> { + slot = newSlot; + item = newItem; + + new ItemComparisonGUI(comparisons, event::reopenGUI).create(event.getPlayer()); + + }, event::cancel).create(event.getPlayer()); + + }).allowCancel().create(event.getPlayer()); + } + + @Override + public void save(ConfigurationSection section) { + section.set("slot", slot.name()); + section.set("item", item); + if (!comparisons.isDefault()) section.set("comparisons", comparisons.getNotDefault()); + } + + @Override + public void load(ConfigurationSection section) { + slot = EquipmentSlot.valueOf(section.getString("slot")); + item = section.getItemStack("item"); + comparisons = section.contains("comparisons") ? new ItemComparisonMap(section.getConfigurationSection("comparisons")) : new ItemComparisonMap(); + } + + public static class EquipmentSlotGUI extends StaticPagedGUI{ + + private static final Map OBJECTS = ImmutableMap.builder() + .put(EquipmentSlot.HAND, ItemUtils.item(XMaterial.GOLDEN_SWORD, "§6Main hand")) + .put(EquipmentSlot.OFF_HAND, ItemUtils.item(XMaterial.SHIELD, "§eOff hand")) + .put(EquipmentSlot.FEET, ItemUtils.item(XMaterial.IRON_BOOTS, "§bFeet")) + .put(EquipmentSlot.LEGS, ItemUtils.item(XMaterial.IRON_LEGGINGS, "§bLegs")) + .put(EquipmentSlot.CHEST, ItemUtils.item(XMaterial.ELYTRA, "§bChest")) + .put(EquipmentSlot.HEAD, ItemUtils.item(XMaterial.TURTLE_HELMET.or(XMaterial.IRON_HELMET), "§bHead")) + .build(); + + public EquipmentSlotGUI(Consumer clicked) { + super(Lang.INVENTORY_EQUIPMENT_SLOTS.toString(), DyeColor.BROWN, OBJECTS, clicked, EquipmentSlot::name); + } + + } + +} diff --git a/core/src/main/java/fr/skytasul/quests/requirements/FactionRequirement.java b/core/src/main/java/fr/skytasul/quests/requirements/FactionRequirement.java index f28fed67..f0a76ecd 100644 --- a/core/src/main/java/fr/skytasul/quests/requirements/FactionRequirement.java +++ b/core/src/main/java/fr/skytasul/quests/requirements/FactionRequirement.java @@ -2,10 +2,11 @@ import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.function.Function; +import java.util.stream.Collectors; import org.bukkit.DyeColor; +import org.bukkit.configuration.ConfigurationSection; import org.bukkit.entity.Player; import org.bukkit.event.inventory.ClickType; import org.bukkit.inventory.ItemStack; @@ -23,9 +24,7 @@ import fr.skytasul.quests.gui.templates.PagedGUI; import fr.skytasul.quests.utils.Lang; import fr.skytasul.quests.utils.XMaterial; -import fr.skytasul.quests.utils.compatibility.DependenciesManager; import fr.skytasul.quests.utils.compatibility.Factions; -import fr.skytasul.quests.utils.compatibility.MissingDependencyException; public class FactionRequirement extends AbstractRequirement { @@ -36,7 +35,6 @@ public FactionRequirement() { } public FactionRequirement(List factions) { - if (!DependenciesManager.fac.isEnabled()) throw new MissingDependencyException("Factions"); this.factions = factions; } @@ -91,7 +89,6 @@ public void click(Faction existing, ItemStack item, ClickType clickType) { @Override public void finish(List objects) { factions = objects; - event.updateItemLore(getLore()); event.reopenGUI(); } @@ -104,17 +101,13 @@ public AbstractRequirement clone() { } @Override - protected void save(Map datas) { - List ls = new ArrayList<>(); - for (Faction fac : factions) { - ls.add(fac.getId()); - } - datas.put("factions", factions); + public void save(ConfigurationSection section) { + section.set("factions", factions.stream().map(Faction::getId).collect(Collectors.toList())); } @Override - protected void load(Map savedDatas) { - for (String s : (List) savedDatas.get("factions")) { + public void load(ConfigurationSection section) { + for (String s : section.getStringList("factions")) { if (!FactionColl.get().containsId(s)) { BeautyQuests.getInstance().getLogger().warning("Faction with ID " + s + " no longer exists."); continue; diff --git a/core/src/main/java/fr/skytasul/quests/requirements/JobLevelRequirement.java b/core/src/main/java/fr/skytasul/quests/requirements/JobLevelRequirement.java index d439b03c..1ffb32b5 100644 --- a/core/src/main/java/fr/skytasul/quests/requirements/JobLevelRequirement.java +++ b/core/src/main/java/fr/skytasul/quests/requirements/JobLevelRequirement.java @@ -1,7 +1,6 @@ package fr.skytasul.quests.requirements; -import java.util.Map; - +import org.bukkit.configuration.ConfigurationSection; import org.bukkit.entity.Player; import fr.skytasul.quests.api.objects.QuestObjectClickEvent; @@ -10,9 +9,7 @@ import fr.skytasul.quests.editors.TextEditor; import fr.skytasul.quests.utils.ComparisonMethod; import fr.skytasul.quests.utils.Lang; -import fr.skytasul.quests.utils.compatibility.DependenciesManager; import fr.skytasul.quests.utils.compatibility.Jobs; -import fr.skytasul.quests.utils.compatibility.MissingDependencyException; public class JobLevelRequirement extends TargetNumberRequirement { @@ -24,7 +21,6 @@ public JobLevelRequirement() { public JobLevelRequirement(String jobName, double target, ComparisonMethod comparison) { super(target, comparison); - if (!DependenciesManager.jobs.isEnabled()) throw new MissingDependencyException("Jobs"); this.jobName = jobName; } @@ -66,27 +62,22 @@ public String[] getLore() { @Override public void itemClick(QuestObjectClickEvent event) { Lang.CHOOSE_JOB_REQUIRED.send(event.getPlayer()); - new TextEditor(event.getPlayer(), () -> { - if (jobName == null) event.getGUI().remove(this); - event.reopenGUI(); - }, obj -> { + new TextEditor(event.getPlayer(), event::cancel, obj -> { jobName = obj; - event.updateItemLore(getLore()); super.itemClick(event); }).useStrippedMessage().enter(); } @Override - protected void save(Map datas) { - super.save(datas); - datas.put("jobName", jobName); + public void save(ConfigurationSection section) { + super.save(section); + section.set("jobName", jobName); } @Override - protected void load(Map savedDatas) { - super.load(savedDatas); - jobName = (String) savedDatas.get("jobName"); - if (savedDatas.containsKey("level")) super.target = (int) savedDatas.get("level"); + public void load(ConfigurationSection section) { + super.load(section); + jobName = section.getString("jobName"); } } \ No newline at end of file diff --git a/core/src/main/java/fr/skytasul/quests/requirements/LevelRequirement.java b/core/src/main/java/fr/skytasul/quests/requirements/LevelRequirement.java index ad2b67af..13198c6a 100644 --- a/core/src/main/java/fr/skytasul/quests/requirements/LevelRequirement.java +++ b/core/src/main/java/fr/skytasul/quests/requirements/LevelRequirement.java @@ -1,7 +1,5 @@ package fr.skytasul.quests.requirements; -import java.util.Map; - import org.bukkit.entity.Player; import fr.skytasul.quests.api.requirements.AbstractRequirement; @@ -48,11 +46,5 @@ public String getDescription(Player p) { public AbstractRequirement clone() { return new LevelRequirement(target, comparison); } - - @Override - protected void load(Map savedDatas) { - super.load(savedDatas); - if (savedDatas.containsKey("level")) super.target = (int) savedDatas.get("level"); - } } diff --git a/core/src/main/java/fr/skytasul/quests/requirements/McCombatLevelRequirement.java b/core/src/main/java/fr/skytasul/quests/requirements/McCombatLevelRequirement.java index 28f4fa1b..61ba636f 100644 --- a/core/src/main/java/fr/skytasul/quests/requirements/McCombatLevelRequirement.java +++ b/core/src/main/java/fr/skytasul/quests/requirements/McCombatLevelRequirement.java @@ -1,16 +1,12 @@ package fr.skytasul.quests.requirements; -import java.util.Map; - import org.bukkit.entity.Player; import fr.skytasul.quests.api.requirements.AbstractRequirement; import fr.skytasul.quests.api.requirements.TargetNumberRequirement; import fr.skytasul.quests.utils.ComparisonMethod; import fr.skytasul.quests.utils.Lang; -import fr.skytasul.quests.utils.compatibility.DependenciesManager; import fr.skytasul.quests.utils.compatibility.McCombatLevel; -import fr.skytasul.quests.utils.compatibility.MissingDependencyException; public class McCombatLevelRequirement extends TargetNumberRequirement { @@ -20,7 +16,6 @@ public McCombatLevelRequirement(){ public McCombatLevelRequirement(double target, ComparisonMethod comparison) { super(target, comparison); - if (!DependenciesManager.mmo.isEnabled()) throw new MissingDependencyException("McCombatLevel"); } @Override @@ -52,16 +47,5 @@ public void sendHelpString(Player p) { public AbstractRequirement clone() { return new McCombatLevelRequirement(target, comparison); } - - @Override - protected void save(Map datas) { - super.save(datas); - } - - @Override - protected void load(Map savedDatas) { - super.load(savedDatas); - if (savedDatas.containsKey("level")) super.target = (int) savedDatas.get("level"); - } } diff --git a/core/src/main/java/fr/skytasul/quests/requirements/McMMOSkillRequirement.java b/core/src/main/java/fr/skytasul/quests/requirements/McMMOSkillRequirement.java index e1972186..78f04261 100644 --- a/core/src/main/java/fr/skytasul/quests/requirements/McMMOSkillRequirement.java +++ b/core/src/main/java/fr/skytasul/quests/requirements/McMMOSkillRequirement.java @@ -1,7 +1,6 @@ package fr.skytasul.quests.requirements; -import java.util.Map; - +import org.bukkit.configuration.ConfigurationSection; import org.bukkit.entity.Player; import fr.skytasul.quests.api.objects.QuestObjectClickEvent; @@ -10,9 +9,7 @@ import fr.skytasul.quests.editors.TextEditor; import fr.skytasul.quests.utils.ComparisonMethod; import fr.skytasul.quests.utils.Lang; -import fr.skytasul.quests.utils.compatibility.DependenciesManager; import fr.skytasul.quests.utils.compatibility.McMMO; -import fr.skytasul.quests.utils.compatibility.MissingDependencyException; public class McMMOSkillRequirement extends TargetNumberRequirement { @@ -24,7 +21,6 @@ public McMMOSkillRequirement(){ public McMMOSkillRequirement(double target, ComparisonMethod comparison) { super(target, comparison); - if (!DependenciesManager.mmo.isEnabled()) throw new MissingDependencyException("mcMMO"); } @Override @@ -70,16 +66,15 @@ public void itemClick(QuestObjectClickEvent event) { } @Override - protected void save(Map datas) { - super.save(datas); - datas.put("skillName", skillName); + public void save(ConfigurationSection section) { + super.save(section); + section.set("skillName", skillName); } @Override - protected void load(Map savedDatas) { - super.load(savedDatas); - skillName = (String) savedDatas.get("skillName"); - if (savedDatas.containsKey("level")) super.target = (int) savedDatas.get("level"); + public void load(ConfigurationSection section) { + super.load(section); + skillName = section.getString("skillName"); } @Override diff --git a/core/src/main/java/fr/skytasul/quests/requirements/MoneyRequirement.java b/core/src/main/java/fr/skytasul/quests/requirements/MoneyRequirement.java index 790fe2c7..8790ec67 100644 --- a/core/src/main/java/fr/skytasul/quests/requirements/MoneyRequirement.java +++ b/core/src/main/java/fr/skytasul/quests/requirements/MoneyRequirement.java @@ -1,7 +1,6 @@ package fr.skytasul.quests.requirements; -import java.util.Map; - +import org.bukkit.configuration.ConfigurationSection; import org.bukkit.entity.Player; import fr.skytasul.quests.api.objects.QuestObjectClickEvent; @@ -10,20 +9,15 @@ import fr.skytasul.quests.editors.TextEditor; import fr.skytasul.quests.editors.checkers.NumberParser; import fr.skytasul.quests.utils.Lang; -import fr.skytasul.quests.utils.compatibility.DependenciesManager; -import fr.skytasul.quests.utils.compatibility.MissingDependencyException; import fr.skytasul.quests.utils.compatibility.Vault; public class MoneyRequirement extends AbstractRequirement implements Actionnable { public double money = 0; - - public MoneyRequirement() { - if (!DependenciesManager.vault.isEnabled()) throw new MissingDependencyException("Vault"); - } + + public MoneyRequirement() {} public MoneyRequirement(double money) { - this(); this.money = money; } @@ -60,24 +54,20 @@ public String[] getLore() { @Override public void itemClick(QuestObjectClickEvent event) { Lang.CHOOSE_MONEY_REQUIRED.send(event.getPlayer()); - new TextEditor<>(event.getPlayer(), () -> { - if (money == 0) event.getGUI().remove(this); - event.reopenGUI(); - }, obj -> { + new TextEditor<>(event.getPlayer(), event::cancel, obj -> { this.money = obj; - event.updateItemLore(getLore()); event.reopenGUI(); - }, new NumberParser<>(Double.class, true, true)).enter(); + }, NumberParser.DOUBLE_PARSER_STRICT_POSITIVE).enter(); } @Override - protected void save(Map datas) { - datas.put("money", money); + public void save(ConfigurationSection section) { + section.set("money", money); } @Override - protected void load(Map savedDatas) { - money = (double) savedDatas.get("money"); + public void load(ConfigurationSection section) { + money = section.getDouble("money"); } } diff --git a/core/src/main/java/fr/skytasul/quests/requirements/PermissionsRequirement.java b/core/src/main/java/fr/skytasul/quests/requirements/PermissionsRequirement.java index dfd14682..c44ba0b5 100644 --- a/core/src/main/java/fr/skytasul/quests/requirements/PermissionsRequirement.java +++ b/core/src/main/java/fr/skytasul/quests/requirements/PermissionsRequirement.java @@ -2,11 +2,11 @@ import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; import org.bukkit.DyeColor; +import org.bukkit.configuration.ConfigurationSection; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; @@ -48,7 +48,7 @@ public void sendReason(Player p){ @Override public String[] getLore() { - return new String[] { "§8> §7" + permissions.size() + " permission(s)", "§8> Message: §7" + (message == null ? Lang.NotSet.toString() : message), "", Lang.RemoveMid.toString() }; + return new String[] { "§8> §7" + Lang.AmountPermissions.format(permissions.size()), "§8> Message: §7" + (message == null ? Lang.NotSet.toString() : message), "", Lang.RemoveMid.toString() }; } @Override @@ -74,7 +74,6 @@ public void finish(List objects) { Lang.CHOOSE_PERM_REQUIRED_MESSAGE.send(p); new TextEditor(p, event::reopenGUI, obj -> { message = obj; - event.updateItemLore(getLore()); event.reopenGUI(); }).passNullIntoEndConsumer().enter(); } @@ -88,15 +87,15 @@ public AbstractRequirement clone() { } @Override - protected void save(Map datas) { - datas.put("permissions", permissions.stream().map(Permission::toString).collect(Collectors.toList())); - if (message != null) datas.put("message", message); + public void save(ConfigurationSection section) { + section.set("permissions", permissions.stream().map(Permission::toString).collect(Collectors.toList())); + if (message != null) section.set("message", message); } @Override - protected void load(Map savedDatas) { - permissions = ((List) savedDatas.get("permissions")).stream().map(Permission::fromString).collect(Collectors.toList()); - if (savedDatas.containsKey("message")) message = (String) savedDatas.get("message"); + public void load(ConfigurationSection section) { + permissions = section.getStringList("permissions").stream().map(Permission::fromString).collect(Collectors.toList()); + if (section.contains("message")) message = section.getString("message"); } public static class Permission { diff --git a/core/src/main/java/fr/skytasul/quests/requirements/PlaceholderRequirement.java b/core/src/main/java/fr/skytasul/quests/requirements/PlaceholderRequirement.java index 5d40f131..b2886129 100644 --- a/core/src/main/java/fr/skytasul/quests/requirements/PlaceholderRequirement.java +++ b/core/src/main/java/fr/skytasul/quests/requirements/PlaceholderRequirement.java @@ -1,8 +1,8 @@ package fr.skytasul.quests.requirements; import java.math.BigDecimal; -import java.util.Map; +import org.bukkit.configuration.ConfigurationSection; import org.bukkit.entity.Player; import fr.skytasul.quests.BeautyQuests; @@ -13,8 +13,6 @@ import fr.skytasul.quests.utils.DebugUtils; import fr.skytasul.quests.utils.Lang; import fr.skytasul.quests.utils.Utils; -import fr.skytasul.quests.utils.compatibility.DependenciesManager; -import fr.skytasul.quests.utils.compatibility.MissingDependencyException; import fr.skytasul.quests.utils.compatibility.QuestsPlaceholders; import me.clip.placeholderapi.PlaceholderAPIPlugin; @@ -36,7 +34,6 @@ public PlaceholderRequirement(){ } public PlaceholderRequirement(String placeholder, String value, ComparisonMethod comparison) { - if (!DependenciesManager.papi.isEnabled()) throw new MissingDependencyException("PlaceholderAPI"); if (placeholder != null) setPlaceholder(placeholder); this.value = value; this.comparison = comparison; @@ -104,19 +101,19 @@ public String getValue(){ } @Override - protected void save(Map datas){ - datas.put("placeholder", rawPlaceholder); - datas.put("value", value); - datas.put("comparison", comparison.name()); - datas.put("parseValue", parseValue); + public void save(ConfigurationSection section) { + section.set("placeholder", rawPlaceholder); + section.set("value", value); + section.set("comparison", comparison.name()); + section.set("parseValue", parseValue); } @Override - protected void load(Map savedDatas){ - setPlaceholder((String) savedDatas.get("placeholder")); - this.value = (String) savedDatas.get("value"); - if (savedDatas.containsKey("comparison")) this.comparison = ComparisonMethod.valueOf((String) savedDatas.get("comparison")); - if (savedDatas.containsKey("parseValue")) this.parseValue = (boolean) savedDatas.get("parseValue"); + public void load(ConfigurationSection section){ + setPlaceholder(section.getString("placeholder")); + this.value = section.getString("value"); + if (section.contains("comparison")) this.comparison = ComparisonMethod.valueOf(section.getString("comparison")); + if (section.contains("parseValue")) this.parseValue = section.getBoolean("parseValue"); } @Override @@ -127,10 +124,7 @@ public String[] getLore() { @Override public void itemClick(QuestObjectClickEvent event) { Lang.CHOOSE_PLACEHOLDER_REQUIRED_IDENTIFIER.send(event.getPlayer()); - new TextEditor(event.getPlayer(), () -> { - if (rawPlaceholder == null) event.getGUI().remove(this); - event.reopenGUI(); - }, id -> { + new TextEditor(event.getPlayer(), event::cancel, id -> { setPlaceholder(id); Lang.CHOOSE_PLACEHOLDER_REQUIRED_VALUE.send(event.getPlayer(), id); new TextEditor(event.getPlayer(), () -> { @@ -143,11 +137,9 @@ public void itemClick(QuestObjectClickEvent event) { Lang.COMPARISON_TYPE.send(event.getPlayer(), ComparisonMethod.getComparisonParser().getNames(), ComparisonMethod.EQUALS.name().toLowerCase()); new TextEditor<>(event.getPlayer(), null, comp -> { this.comparison = comp == null ? ComparisonMethod.EQUALS : comp; - event.updateItemLore(getLore()); event.reopenGUI(); }, ComparisonMethod.getComparisonParser()).passNullIntoEndConsumer().enter(); }catch (NumberFormatException __) { - event.updateItemLore(getLore()); event.reopenGUI(); } }).enter(); diff --git a/core/src/main/java/fr/skytasul/quests/requirements/QuestRequirement.java b/core/src/main/java/fr/skytasul/quests/requirements/QuestRequirement.java index 7ecc3305..b337c8d1 100644 --- a/core/src/main/java/fr/skytasul/quests/requirements/QuestRequirement.java +++ b/core/src/main/java/fr/skytasul/quests/requirements/QuestRequirement.java @@ -1,7 +1,6 @@ package fr.skytasul.quests.requirements; -import java.util.Map; - +import org.bukkit.configuration.ConfigurationSection; import org.bukkit.entity.Player; import fr.skytasul.quests.api.QuestsAPI; @@ -62,15 +61,11 @@ public void itemClick(QuestObjectClickEvent event) { new ChooseQuestGUI(QuestsAPI.getQuests().getQuests(), quest -> { this.questId = quest.getID(); - event.updateItemLore(getLore()); event.reopenGUI(); }) { @Override public fr.skytasul.quests.gui.CustomInventory.CloseBehavior onClose(Player p, org.bukkit.inventory.Inventory inv) { - Utils.runSync(() -> { - event.getGUI().remove(QuestRequirement.this); - event.reopenGUI(); - }); + Utils.runSync(event::remove); return CloseBehavior.NOTHING; } }.create(event.getPlayer()); @@ -82,13 +77,13 @@ public AbstractRequirement clone() { } @Override - protected void save(Map datas) { - datas.put("questID", questId); + public void save(ConfigurationSection section) { + section.set("questID", questId); } @Override - protected void load(Map savedDatas) { - questId = (int) savedDatas.get("questID"); + public void load(ConfigurationSection section) { + questId = section.getInt("questID"); } } diff --git a/core/src/main/java/fr/skytasul/quests/requirements/RegionRequirement.java b/core/src/main/java/fr/skytasul/quests/requirements/RegionRequirement.java index d1cae08c..e941737c 100644 --- a/core/src/main/java/fr/skytasul/quests/requirements/RegionRequirement.java +++ b/core/src/main/java/fr/skytasul/quests/requirements/RegionRequirement.java @@ -1,8 +1,7 @@ package fr.skytasul.quests.requirements; -import java.util.Map; - import org.bukkit.Bukkit; +import org.bukkit.configuration.ConfigurationSection; import org.bukkit.entity.Player; import com.sk89q.worldguard.protection.regions.ProtectedRegion; @@ -13,8 +12,6 @@ import fr.skytasul.quests.editors.TextEditor; import fr.skytasul.quests.utils.Lang; import fr.skytasul.quests.utils.Utils; -import fr.skytasul.quests.utils.compatibility.DependenciesManager; -import fr.skytasul.quests.utils.compatibility.MissingDependencyException; import fr.skytasul.quests.utils.compatibility.worldguard.BQWorldGuard; public class RegionRequirement extends AbstractRequirement { @@ -28,8 +25,6 @@ public RegionRequirement() { } public RegionRequirement(String worldName, String regionName) { - if (!DependenciesManager.wg.isEnabled()) throw new MissingDependencyException("WorldGuard"); - this.worldName = worldName; setRegionName(regionName); } @@ -59,12 +54,11 @@ public void itemClick(QuestObjectClickEvent event) { if (region != null) { this.worldName = p.getWorld().getName(); this.regionName = region.getId(); - event.updateItemLore(getLore()); + event.reopenGUI(); }else { Utils.sendMessage(p, Lang.REGION_DOESNT_EXIST.toString()); - event.getGUI().remove(this); + event.remove(); } - event.reopenGUI(); }).useStrippedMessage().enter(); } @@ -86,15 +80,15 @@ public AbstractRequirement clone() { } @Override - protected void save(Map datas) { - datas.put("world", worldName); - datas.put("region", regionName); + public void save(ConfigurationSection section) { + section.set("world", worldName); + section.set("region", regionName); } @Override - protected void load(Map savedDatas) { - worldName = (String) savedDatas.get("world"); - setRegionName((String) savedDatas.get("region")); + public void load(ConfigurationSection section) { + worldName = section.getString("world"); + setRegionName(section.getString("region")); } } diff --git a/core/src/main/java/fr/skytasul/quests/requirements/ScoreboardRequirement.java b/core/src/main/java/fr/skytasul/quests/requirements/ScoreboardRequirement.java index 207d1d5f..e0ee7a89 100644 --- a/core/src/main/java/fr/skytasul/quests/requirements/ScoreboardRequirement.java +++ b/core/src/main/java/fr/skytasul/quests/requirements/ScoreboardRequirement.java @@ -1,8 +1,7 @@ package fr.skytasul.quests.requirements; -import java.util.Map; - import org.bukkit.Bukkit; +import org.bukkit.configuration.ConfigurationSection; import org.bukkit.entity.Player; import org.bukkit.scoreboard.Objective; @@ -70,15 +69,15 @@ public void itemClick(QuestObjectClickEvent event) { } @Override - protected void save(Map datas) { - super.save(datas); - datas.put("objective", objectiveName); + public void save(ConfigurationSection section) { + super.save(section); + section.set("objective", objectiveName); } @Override - protected void load(Map savedDatas) { - super.load(savedDatas); - setObjectiveName((String) savedDatas.get("objective")); + public void load(ConfigurationSection section) { + super.load(section); + setObjectiveName(section.getString("objective")); } @Override diff --git a/core/src/main/java/fr/skytasul/quests/requirements/SkillAPILevelRequirement.java b/core/src/main/java/fr/skytasul/quests/requirements/SkillAPILevelRequirement.java index 53ed6036..4f7a821c 100644 --- a/core/src/main/java/fr/skytasul/quests/requirements/SkillAPILevelRequirement.java +++ b/core/src/main/java/fr/skytasul/quests/requirements/SkillAPILevelRequirement.java @@ -1,7 +1,5 @@ package fr.skytasul.quests.requirements; -import java.util.Map; - import org.bukkit.entity.Player; import fr.skytasul.quests.api.requirements.AbstractRequirement; @@ -49,11 +47,5 @@ public String getDescription(Player p) { public AbstractRequirement clone() { return new SkillAPILevelRequirement(target, comparison); } - - @Override - protected void load(Map savedDatas) { - super.load(savedDatas); - if (savedDatas.containsKey("level")) super.target = (int) savedDatas.get("level"); - } } diff --git a/core/src/main/java/fr/skytasul/quests/requirements/logical/LogicalOrRequirement.java b/core/src/main/java/fr/skytasul/quests/requirements/logical/LogicalOrRequirement.java index babee3a7..c603a7cc 100644 --- a/core/src/main/java/fr/skytasul/quests/requirements/logical/LogicalOrRequirement.java +++ b/core/src/main/java/fr/skytasul/quests/requirements/logical/LogicalOrRequirement.java @@ -2,18 +2,17 @@ import java.util.ArrayList; import java.util.List; -import java.util.Map; +import org.bukkit.configuration.ConfigurationSection; import org.bukkit.entity.Player; import fr.skytasul.quests.api.QuestsAPI; -import fr.skytasul.quests.api.objects.QuestObject; import fr.skytasul.quests.api.objects.QuestObjectClickEvent; import fr.skytasul.quests.api.objects.QuestObjectLocation; import fr.skytasul.quests.api.requirements.AbstractRequirement; +import fr.skytasul.quests.api.serializable.SerializableObject; import fr.skytasul.quests.structure.Quest; import fr.skytasul.quests.utils.Lang; -import fr.skytasul.quests.utils.Utils; public class LogicalOrRequirement extends AbstractRequirement { @@ -48,7 +47,6 @@ public String[] getLore() { public void itemClick(QuestObjectClickEvent event) { QuestsAPI.getRequirements().createGUI(QuestObjectLocation.OTHER, requirements -> { this.requirements = requirements; - event.updateItemLore(getLore()); event.reopenGUI(); }, requirements).create(event.getPlayer()); } @@ -64,13 +62,13 @@ public AbstractRequirement clone() { } @Override - protected void save(Map datas) { - datas.put("requirements", Utils.serializeList(requirements, AbstractRequirement::serialize)); + public void save(ConfigurationSection section) { + section.set("requirements", SerializableObject.serializeList(requirements)); } @Override - protected void load(Map savedDatas) { - requirements = QuestObject.deserializeList((List>) savedDatas.get("requirements"), AbstractRequirement::deserialize); + public void load(ConfigurationSection section) { + requirements = SerializableObject.deserializeList(section.getMapList("requirements"), AbstractRequirement::deserialize); } } diff --git a/core/src/main/java/fr/skytasul/quests/rewards/CheckpointReward.java b/core/src/main/java/fr/skytasul/quests/rewards/CheckpointReward.java index 9d2007f2..4e393d7f 100644 --- a/core/src/main/java/fr/skytasul/quests/rewards/CheckpointReward.java +++ b/core/src/main/java/fr/skytasul/quests/rewards/CheckpointReward.java @@ -2,16 +2,16 @@ import java.util.ArrayList; import java.util.List; -import java.util.Map; +import org.bukkit.configuration.ConfigurationSection; import org.bukkit.entity.Player; import fr.skytasul.quests.api.QuestsAPI; -import fr.skytasul.quests.api.objects.QuestObject; import fr.skytasul.quests.api.objects.QuestObjectClickEvent; import fr.skytasul.quests.api.objects.QuestObjectLocation; import fr.skytasul.quests.api.options.QuestOption; import fr.skytasul.quests.api.rewards.AbstractReward; +import fr.skytasul.quests.api.serializable.SerializableObject; import fr.skytasul.quests.structure.Quest; import fr.skytasul.quests.utils.Lang; import fr.skytasul.quests.utils.Utils; @@ -64,19 +64,18 @@ public String[] getLore() { public void itemClick(QuestObjectClickEvent event) { QuestsAPI.getRewards().createGUI(Lang.INVENTORY_CHECKPOINT_ACTIONS.toString(), QuestObjectLocation.CHECKPOINT, rewards -> { actions = rewards; - event.updateItemLore(getLore()); event.reopenGUI(); - }, actions).create(event.getPlayer()); + }, actions, null).create(event.getPlayer()); } @Override - protected void save(Map datas) { - datas.put("actions", Utils.serializeList(actions, AbstractReward::serialize)); + public void save(ConfigurationSection section) { + section.set("actions", SerializableObject.serializeList(actions)); } @Override - protected void load(Map savedDatas) { - actions = QuestObject.deserializeList((List>) savedDatas.get("actions"), AbstractReward::deserialize); + public void load(ConfigurationSection section) { + actions = SerializableObject.deserializeList(section.getMapList("actions"), AbstractReward::deserialize); } } diff --git a/core/src/main/java/fr/skytasul/quests/rewards/CommandReward.java b/core/src/main/java/fr/skytasul/quests/rewards/CommandReward.java index 218f647c..9d244794 100644 --- a/core/src/main/java/fr/skytasul/quests/rewards/CommandReward.java +++ b/core/src/main/java/fr/skytasul/quests/rewards/CommandReward.java @@ -2,10 +2,10 @@ import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.function.Function; import org.bukkit.DyeColor; +import org.bukkit.configuration.ConfigurationSection; import org.bukkit.entity.Player; import org.bukkit.event.inventory.ClickType; import org.bukkit.inventory.ItemStack; @@ -75,7 +75,6 @@ public ItemStack getObjectItemStack(Command cmd) { @Override public void finish(List objects) { commands = objects; - event.updateItemLore(getLore()); event.reopenGUI(); } @@ -83,13 +82,13 @@ public void finish(List objects) { } @Override - protected void save(Map datas){ - datas.put("commands", Utils.serializeList(commands, Command::serialize)); + public void save(ConfigurationSection section) { + section.set("commands", Utils.serializeList(commands, Command::serialize)); } @Override - protected void load(Map savedDatas){ - commands.addAll(Utils.deserializeList((List>) savedDatas.get("commands"), Command::deserialize)); + public void load(ConfigurationSection section){ + commands.addAll(Utils.deserializeList(section.getMapList("commands"), Command::deserialize)); } } diff --git a/core/src/main/java/fr/skytasul/quests/rewards/ItemReward.java b/core/src/main/java/fr/skytasul/quests/rewards/ItemReward.java index 8a8cf4ec..667b6868 100644 --- a/core/src/main/java/fr/skytasul/quests/rewards/ItemReward.java +++ b/core/src/main/java/fr/skytasul/quests/rewards/ItemReward.java @@ -3,8 +3,8 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.Map; +import org.bukkit.configuration.ConfigurationSection; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; @@ -52,19 +52,18 @@ public String[] getLore() { public void itemClick(QuestObjectClickEvent event) { new ItemsGUI(items -> { this.items = items; - event.updateItemLore(getLore()); - event.getGUI().reopen(); + event.reopenGUI(); }, items).create(event.getPlayer()); } @Override - protected void save(Map datas){ - datas.put("items", Utils.serializeList(items, ItemStack::serialize)); + public void save(ConfigurationSection section) { + section.set("items", Utils.serializeList(items, ItemStack::serialize)); } @Override - protected void load(Map savedDatas){ - items.addAll(Utils.deserializeList((List>) savedDatas.get("items"), ItemStack::deserialize)); + public void load(ConfigurationSection section){ + items.addAll(Utils.deserializeList(section.getMapList("items"), ItemStack::deserialize)); } } diff --git a/core/src/main/java/fr/skytasul/quests/rewards/MessageReward.java b/core/src/main/java/fr/skytasul/quests/rewards/MessageReward.java index 2be81e39..5f9d17d5 100644 --- a/core/src/main/java/fr/skytasul/quests/rewards/MessageReward.java +++ b/core/src/main/java/fr/skytasul/quests/rewards/MessageReward.java @@ -1,8 +1,8 @@ package fr.skytasul.quests.rewards; import java.util.List; -import java.util.Map; +import org.bukkit.configuration.ConfigurationSection; import org.bukkit.entity.Player; import fr.skytasul.quests.api.objects.QuestObjectClickEvent; @@ -40,24 +40,20 @@ public String[] getLore() { @Override public void itemClick(QuestObjectClickEvent event) { Lang.WRITE_MESSAGE.send(event.getPlayer()); - new TextEditor(event.getPlayer(), () -> { - if (text == null) event.getGUI().remove(this); - event.reopenGUI(); - }, obj -> { + new TextEditor(event.getPlayer(), event::cancel, obj -> { this.text = obj; - event.updateItemLore(getLore()); event.reopenGUI(); }).enter(); } @Override - protected void save(Map datas) { - datas.put("text", text); + public void save(ConfigurationSection section) { + section.set("text", text); } @Override - protected void load(Map savedDatas) { - text = (String) savedDatas.get("text"); + public void load(ConfigurationSection section) { + text = section.getString("text"); } } diff --git a/core/src/main/java/fr/skytasul/quests/rewards/MoneyReward.java b/core/src/main/java/fr/skytasul/quests/rewards/MoneyReward.java index 5008ce4d..6e8db9af 100644 --- a/core/src/main/java/fr/skytasul/quests/rewards/MoneyReward.java +++ b/core/src/main/java/fr/skytasul/quests/rewards/MoneyReward.java @@ -2,8 +2,8 @@ import java.util.Arrays; import java.util.List; -import java.util.Map; +import org.bukkit.configuration.ConfigurationSection; import org.bukkit.entity.Player; import fr.skytasul.quests.api.objects.QuestObjectClickEvent; @@ -11,20 +11,15 @@ import fr.skytasul.quests.editors.TextEditor; import fr.skytasul.quests.editors.checkers.NumberParser; import fr.skytasul.quests.utils.Lang; -import fr.skytasul.quests.utils.compatibility.DependenciesManager; -import fr.skytasul.quests.utils.compatibility.MissingDependencyException; import fr.skytasul.quests.utils.compatibility.Vault; public class MoneyReward extends AbstractReward { public double money = 0; - public MoneyReward(){ - if (!DependenciesManager.vault.isEnabled()) throw new MissingDependencyException("Vault"); - } + public MoneyReward() {} public MoneyReward(double money) { - this(); this.money = money; } @@ -54,24 +49,20 @@ public String[] getLore() { @Override public void itemClick(QuestObjectClickEvent event) { Lang.CHOOSE_MONEY_REWARD.send(event.getPlayer()); - new TextEditor<>(event.getPlayer(), () -> { - if (money == 0) event.getGUI().remove(this); - event.reopenGUI(); - }, obj -> { + new TextEditor<>(event.getPlayer(), event::cancel, obj -> { money = obj; - event.updateItemLore(getLore()); event.reopenGUI(); }, new NumberParser<>(Double.class, false, true)).enter(); } @Override - protected void save(Map datas) { - datas.put("money", money); + public void save(ConfigurationSection section) { + section.set("money", money); } @Override - protected void load(Map savedDatas) { - money = (double) savedDatas.get("money"); + public void load(ConfigurationSection section) { + money = section.getDouble("money"); } } diff --git a/core/src/main/java/fr/skytasul/quests/rewards/PermissionReward.java b/core/src/main/java/fr/skytasul/quests/rewards/PermissionReward.java index d6497c0f..d777329c 100644 --- a/core/src/main/java/fr/skytasul/quests/rewards/PermissionReward.java +++ b/core/src/main/java/fr/skytasul/quests/rewards/PermissionReward.java @@ -2,8 +2,8 @@ import java.util.ArrayList; import java.util.List; -import java.util.Map; +import org.bukkit.configuration.ConfigurationSection; import org.bukkit.entity.Player; import fr.skytasul.quests.api.objects.QuestObjectClickEvent; @@ -11,8 +11,6 @@ import fr.skytasul.quests.gui.permissions.PermissionListGUI; import fr.skytasul.quests.utils.Lang; import fr.skytasul.quests.utils.Utils; -import fr.skytasul.quests.utils.compatibility.DependenciesManager; -import fr.skytasul.quests.utils.compatibility.MissingDependencyException; import fr.skytasul.quests.utils.types.Permission; public class PermissionReward extends AbstractReward { @@ -24,7 +22,6 @@ public PermissionReward(){ } public PermissionReward(List permissions) { - if (!DependenciesManager.vault.isEnabled()) throw new MissingDependencyException("Vault"); this.permissions = permissions; } @@ -50,19 +47,18 @@ public String[] getLore() { public void itemClick(QuestObjectClickEvent event) { new PermissionListGUI(permissions, permissions -> { PermissionReward.this.permissions = permissions; - event.updateItemLore(getLore()); event.reopenGUI(); }).create(event.getPlayer()); } @Override - protected void save(Map datas){ - datas.put("perms", Utils.serializeList(permissions, Permission::serialize)); + public void save(ConfigurationSection section) { + section.set("perms", Utils.serializeList(permissions, Permission::serialize)); } @Override - protected void load(Map savedDatas){ - permissions.addAll(Utils.deserializeList((List>) savedDatas.get("perms"), Permission::deserialize)); + public void load(ConfigurationSection section){ + permissions.addAll(Utils.deserializeList(section.getMapList("perms"), Permission::deserialize)); } } diff --git a/core/src/main/java/fr/skytasul/quests/rewards/QuestStopReward.java b/core/src/main/java/fr/skytasul/quests/rewards/QuestStopReward.java index 1fa4fcf1..8e4def64 100644 --- a/core/src/main/java/fr/skytasul/quests/rewards/QuestStopReward.java +++ b/core/src/main/java/fr/skytasul/quests/rewards/QuestStopReward.java @@ -1,7 +1,6 @@ package fr.skytasul.quests.rewards; import java.util.List; -import java.util.Map; import org.bukkit.entity.Player; @@ -29,10 +28,4 @@ public AbstractReward clone() { return this; } - @Override - protected void save(Map datas) {} - - @Override - protected void load(Map savedDatas) {} - } diff --git a/core/src/main/java/fr/skytasul/quests/rewards/RandomReward.java b/core/src/main/java/fr/skytasul/quests/rewards/RandomReward.java index 4553e109..8a639d64 100644 --- a/core/src/main/java/fr/skytasul/quests/rewards/RandomReward.java +++ b/core/src/main/java/fr/skytasul/quests/rewards/RandomReward.java @@ -2,24 +2,23 @@ import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.concurrent.ThreadLocalRandom; import java.util.stream.Collectors; +import org.bukkit.configuration.ConfigurationSection; import org.bukkit.entity.Player; import fr.skytasul.quests.BeautyQuests; import fr.skytasul.quests.api.QuestsAPI; -import fr.skytasul.quests.api.objects.QuestObject; import fr.skytasul.quests.api.objects.QuestObjectClickEvent; import fr.skytasul.quests.api.objects.QuestObjectLocation; import fr.skytasul.quests.api.options.QuestOption; import fr.skytasul.quests.api.rewards.AbstractReward; +import fr.skytasul.quests.api.serializable.SerializableObject; import fr.skytasul.quests.editors.TextEditor; import fr.skytasul.quests.editors.checkers.NumberParser; import fr.skytasul.quests.utils.Lang; -import fr.skytasul.quests.utils.Utils; public class RandomReward extends AbstractReward { @@ -32,7 +31,8 @@ public RandomReward() { public RandomReward(List rewards, int min, int max) { this.rewards = rewards; - setMinMax(min, max); + this.min = min; + this.max = max; } public void setMinMax(int min, int max) { @@ -40,7 +40,7 @@ public void setMinMax(int min, int max) { this.max = Math.max(min, max); if (max > rewards.size()) - BeautyQuests.logger.warning("Random reward with max amount (" + max + ") greater than amount of rewards available (" + rewards.size() + ")"); + BeautyQuests.logger.warning("Random reward with max amount (" + max + ") greater than amount of rewards available (" + rewards.size() + ") in " + debugName()); } @Override @@ -100,7 +100,6 @@ public void itemClick(QuestObjectClickEvent event) { if (event.isInCreation() || event.getClick().isLeftClick()) { QuestsAPI.getRewards().createGUI(QuestObjectLocation.OTHER, rewards -> { this.rewards = rewards; - event.updateItemLore(getLore()); event.reopenGUI(); }, rewards).create(event.getPlayer()); }else if (event.getClick().isRightClick()) { @@ -109,7 +108,6 @@ public void itemClick(QuestObjectClickEvent event) { Lang.REWARD_EDITOR_RANDOM_MAX.send(event.getPlayer()); new TextEditor<>(event.getPlayer(), event::reopenGUI, max -> { setMinMax(min, max == null ? min : max); - event.updateItemLore(getLore()); event.reopenGUI(); }, NumberParser.INTEGER_PARSER_STRICT_POSITIVE).passNullIntoEndConsumer().enter(); }, NumberParser.INTEGER_PARSER_POSITIVE).enter(); @@ -117,16 +115,16 @@ public void itemClick(QuestObjectClickEvent event) { } @Override - protected void save(Map datas) { - datas.put("rewards", Utils.serializeList(rewards, AbstractReward::serialize)); - datas.put("min", min); - datas.put("max", max); + public void save(ConfigurationSection section) { + section.set("rewards", SerializableObject.serializeList(rewards)); + section.set("min", min); + section.set("max", max); } @Override - protected void load(Map savedDatas) { - rewards = QuestObject.deserializeList((List>) savedDatas.get("rewards"), AbstractReward::deserialize); - setMinMax((int) savedDatas.get("min"), (int) savedDatas.get("max")); + public void load(ConfigurationSection section) { + rewards = SerializableObject.deserializeList(section.getMapList("rewards"), AbstractReward::deserialize); + setMinMax(section.getInt("min"), section.getInt("max")); } } diff --git a/core/src/main/java/fr/skytasul/quests/rewards/RemoveItemsReward.java b/core/src/main/java/fr/skytasul/quests/rewards/RemoveItemsReward.java index dd124bae..e09ba2b9 100644 --- a/core/src/main/java/fr/skytasul/quests/rewards/RemoveItemsReward.java +++ b/core/src/main/java/fr/skytasul/quests/rewards/RemoveItemsReward.java @@ -3,8 +3,8 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.Map; +import org.bukkit.configuration.ConfigurationSection; import org.bukkit.entity.Player; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemStack; @@ -55,8 +55,8 @@ public String getDescription(Player p) { @Override public String[] getLore() { return new String[] { - "§7" + items.size() + " " + Lang.Item.toString(), - "§7" + comparisons.getEffective().size() + " comparison(s)", + "§7" + Lang.AmountItems.format(items.size()), + "§7" + Lang.AmountComparisons.format(comparisons.getEffective().size()), "", "§7" + Lang.ClickLeft.toString() + " > " + Lang.stageItems.toString(), "§7" + Lang.ClickRight.toString() + " > " + Lang.stageItemsComparison.toString(), @@ -68,27 +68,23 @@ public void itemClick(QuestObjectClickEvent event) { if (event.isInCreation() || event.getClick().isLeftClick()) { new ItemsGUI(items -> { this.items = items; - event.updateItemLore(getLore()); event.reopenGUI(); }, items).create(event.getPlayer()); }else if (event.getClick().isRightClick()) { - new ItemComparisonGUI(comparisons, () -> { - event.updateItemLore(getLore()); - event.reopenGUI(); - }).create(event.getPlayer()); + new ItemComparisonGUI(comparisons, event::reopenGUI).create(event.getPlayer()); } } @Override - protected void save(Map datas){ - datas.put("items", Utils.serializeList(items, ItemStack::serialize)); - if (!comparisons.getNotDefault().isEmpty()) datas.put("comparisons", comparisons.getNotDefault()); + public void save(ConfigurationSection section) { + section.set("items", Utils.serializeList(items, ItemStack::serialize)); + if (!comparisons.getNotDefault().isEmpty()) section.createSection("comparisons", comparisons.getNotDefault()); } @Override - protected void load(Map savedDatas){ - items.addAll(Utils.deserializeList((List>) savedDatas.get("items"), ItemStack::deserialize)); - if (savedDatas.containsKey("comparisons")) comparisons.setNotDefaultComparisons((Map) savedDatas.get("comparisons")); + public void load(ConfigurationSection section){ + items.addAll(Utils.deserializeList(section.getMapList("items"), ItemStack::deserialize)); + if (section.contains("comparisons")) comparisons.setNotDefaultComparisons(section.getConfigurationSection("comparisons")); } } diff --git a/core/src/main/java/fr/skytasul/quests/rewards/RequirementDependentReward.java b/core/src/main/java/fr/skytasul/quests/rewards/RequirementDependentReward.java index 1a9fd0c9..6055e1e5 100644 --- a/core/src/main/java/fr/skytasul/quests/rewards/RequirementDependentReward.java +++ b/core/src/main/java/fr/skytasul/quests/rewards/RequirementDependentReward.java @@ -2,11 +2,11 @@ import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; import org.bukkit.Bukkit; +import org.bukkit.configuration.ConfigurationSection; import org.bukkit.entity.Player; import org.bukkit.event.inventory.ClickType; import org.bukkit.event.inventory.InventoryType; @@ -14,12 +14,12 @@ import org.bukkit.inventory.ItemStack; import fr.skytasul.quests.api.QuestsAPI; -import fr.skytasul.quests.api.objects.QuestObject; import fr.skytasul.quests.api.objects.QuestObjectClickEvent; import fr.skytasul.quests.api.objects.QuestObjectLocation; import fr.skytasul.quests.api.options.QuestOption; import fr.skytasul.quests.api.requirements.AbstractRequirement; import fr.skytasul.quests.api.rewards.AbstractReward; +import fr.skytasul.quests.api.serializable.SerializableObject; import fr.skytasul.quests.gui.CustomInventory; import fr.skytasul.quests.gui.Inventories; import fr.skytasul.quests.gui.ItemUtils; @@ -129,7 +129,6 @@ public boolean onClick(Player p, Inventory inv, ItemStack current, int slot, Cli }, rewards).create(p); break; case 4: - event.updateItemLore(getLore()); event.reopenGUI(); break; } @@ -139,15 +138,15 @@ public boolean onClick(Player p, Inventory inv, ItemStack current, int slot, Cli } @Override - protected void save(Map datas) { - datas.put("requirements", Utils.serializeList(requirements, AbstractRequirement::serialize)); - datas.put("rewards", Utils.serializeList(rewards, AbstractReward::serialize)); + public void save(ConfigurationSection section) { + section.set("requirements", SerializableObject.serializeList(requirements)); + section.set("rewards", SerializableObject.serializeList(rewards)); } @Override - protected void load(Map savedDatas) { - requirements = QuestObject.deserializeList((List>) savedDatas.get("requirements"), AbstractRequirement::deserialize); - rewards = QuestObject.deserializeList((List>) savedDatas.get("rewards"), AbstractReward::deserialize); + public void load(ConfigurationSection section) { + requirements = SerializableObject.deserializeList(section.getMapList("requirements"), AbstractRequirement::deserialize); + rewards = SerializableObject.deserializeList(section.getMapList("rewards"), AbstractReward::deserialize); } } diff --git a/core/src/main/java/fr/skytasul/quests/rewards/TeleportationReward.java b/core/src/main/java/fr/skytasul/quests/rewards/TeleportationReward.java index 64d40c76..7e737be7 100644 --- a/core/src/main/java/fr/skytasul/quests/rewards/TeleportationReward.java +++ b/core/src/main/java/fr/skytasul/quests/rewards/TeleportationReward.java @@ -1,9 +1,9 @@ package fr.skytasul.quests.rewards; import java.util.List; -import java.util.Map; import org.bukkit.Location; +import org.bukkit.configuration.ConfigurationSection; import org.bukkit.entity.Player; import fr.skytasul.quests.api.objects.QuestObjectClickEvent; @@ -42,24 +42,20 @@ public String[] getLore() { @Override public void itemClick(QuestObjectClickEvent event) { Lang.MOVE_TELEPORT_POINT.send(event.getPlayer()); - new WaitClick(event.getPlayer(), () -> { - if (teleportation == null) event.getGUI().remove(this); - event.reopenGUI(); - }, NPCGUI.validMove.clone(), () -> { + new WaitClick(event.getPlayer(), event::cancel, NPCGUI.validMove.clone(), () -> { teleportation = event.getPlayer().getLocation(); - event.updateItemLore(getLore()); event.reopenGUI(); }).enter(); } @Override - protected void save(Map datas) { - datas.put("tp", teleportation.serialize()); + public void save(ConfigurationSection section) { + section.set("tp", teleportation.serialize()); } @Override - protected void load(Map savedDatas) { - teleportation = Location.deserialize((Map) savedDatas.get("tp")); + public void load(ConfigurationSection section) { + teleportation = Location.deserialize(section.getConfigurationSection("tp").getValues(false)); } } diff --git a/core/src/main/java/fr/skytasul/quests/rewards/TitleReward.java b/core/src/main/java/fr/skytasul/quests/rewards/TitleReward.java index fb94eb84..cdd3e2c2 100644 --- a/core/src/main/java/fr/skytasul/quests/rewards/TitleReward.java +++ b/core/src/main/java/fr/skytasul/quests/rewards/TitleReward.java @@ -1,8 +1,8 @@ package fr.skytasul.quests.rewards; import java.util.List; -import java.util.Map; +import org.bukkit.configuration.ConfigurationSection; import org.bukkit.entity.Player; import fr.skytasul.quests.api.objects.QuestObjectClickEvent; @@ -30,9 +30,11 @@ public String[] getLore() { public void itemClick(QuestObjectClickEvent event) { new TitleGUI(newTitle -> { if (newTitle == null) { - if (title == null) event.getGUI().remove(this); - }else title = newTitle; - event.updateItemLore(getLore()); + event.cancel(); + return; + } + + title = newTitle; event.reopenGUI(); }).edit(title).create(event.getPlayer()); } @@ -49,13 +51,13 @@ public AbstractReward clone() { } @Override - protected void save(Map datas) { - if (title != null) datas.put("title", title.serialize()); + public void save(ConfigurationSection section) { + if (title != null) title.serialize(section.createSection("title")); } @Override - protected void load(Map savedDatas) { - title = savedDatas.containsKey("title") ? Title.deserialize((Map) savedDatas.get("title")) : null; + public void load(ConfigurationSection section) { + title = section.contains("title") ? Title.deserialize(section.getConfigurationSection("title")) : null; } } diff --git a/core/src/main/java/fr/skytasul/quests/rewards/WaitReward.java b/core/src/main/java/fr/skytasul/quests/rewards/WaitReward.java index 2b814a27..12df3bbb 100644 --- a/core/src/main/java/fr/skytasul/quests/rewards/WaitReward.java +++ b/core/src/main/java/fr/skytasul/quests/rewards/WaitReward.java @@ -1,8 +1,8 @@ package fr.skytasul.quests.rewards; import java.util.List; -import java.util.Map; +import org.bukkit.configuration.ConfigurationSection; import org.bukkit.entity.Player; import fr.skytasul.quests.api.objects.QuestObjectClickEvent; @@ -36,12 +36,8 @@ public String[] getLore() { @Override public void itemClick(QuestObjectClickEvent event) { Lang.REWARD_EDITOR_WAIT.send(event.getPlayer()); - new TextEditor<>(event.getPlayer(), () -> { - if (delay == 0) event.getGUI().remove(this); - event.reopenGUI(); - }, obj -> { + new TextEditor<>(event.getPlayer(), event::cancel, obj -> { delay = obj; - event.updateItemLore(getLore()); event.reopenGUI(); }, NumberParser.INTEGER_PARSER_STRICT_POSITIVE).enter(); } @@ -62,13 +58,13 @@ public AbstractReward clone() { } @Override - protected void save(Map datas) { - datas.put("delay", delay); + public void save(ConfigurationSection section) { + section.set("delay", delay); } @Override - protected void load(Map savedDatas) { - delay = (int) savedDatas.get("delay"); + public void load(ConfigurationSection section) { + delay = section.getInt("delay"); } } diff --git a/core/src/main/java/fr/skytasul/quests/rewards/XPReward.java b/core/src/main/java/fr/skytasul/quests/rewards/XPReward.java index 609d816c..94f1cd29 100644 --- a/core/src/main/java/fr/skytasul/quests/rewards/XPReward.java +++ b/core/src/main/java/fr/skytasul/quests/rewards/XPReward.java @@ -2,8 +2,8 @@ import java.util.Arrays; import java.util.List; -import java.util.Map; +import org.bukkit.configuration.ConfigurationSection; import org.bukkit.entity.Player; import fr.skytasul.quests.QuestsConfiguration; @@ -52,25 +52,21 @@ public String[] getLore() { @Override public void itemClick(QuestObjectClickEvent event) { Utils.sendMessage(event.getPlayer(), Lang.XP_GAIN.toString(), exp); - new TextEditor<>(event.getPlayer(), () -> { - if (exp == 0) event.getGUI().remove(this); - event.reopenGUI(); - }, obj -> { + new TextEditor<>(event.getPlayer(), event::cancel, obj -> { Utils.sendMessage(event.getPlayer(), Lang.XP_EDITED.toString(), exp, obj); exp = obj; - event.updateItemLore(getLore()); event.reopenGUI(); }, NumberParser.INTEGER_PARSER_STRICT_POSITIVE).enter(); } @Override - protected void save(Map datas) { - datas.put("xp", exp); + public void save(ConfigurationSection section) { + section.set("xp", exp); } @Override - protected void load(Map savedDatas) { - exp = (int) savedDatas.get("xp"); + public void load(ConfigurationSection section) { + exp = section.getInt("xp"); } } diff --git a/core/src/main/java/fr/skytasul/quests/scoreboards/Scoreboard.java b/core/src/main/java/fr/skytasul/quests/scoreboards/Scoreboard.java index 00cd51f3..fd216c72 100644 --- a/core/src/main/java/fr/skytasul/quests/scoreboards/Scoreboard.java +++ b/core/src/main/java/fr/skytasul/quests/scoreboards/Scoreboard.java @@ -3,17 +3,22 @@ import java.util.ArrayList; import java.util.LinkedList; import java.util.List; - +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.entity.Player; import org.bukkit.event.HandlerList; import org.bukkit.event.Listener; import org.bukkit.scheduler.BukkitRunnable; - import fr.mrmicky.fastboard.FastBoard; import fr.skytasul.quests.BeautyQuests; +import fr.skytasul.quests.QuestsConfiguration; import fr.skytasul.quests.api.QuestsAPI; +import fr.skytasul.quests.api.options.description.QuestDescriptionContext; +import fr.skytasul.quests.api.options.description.QuestDescriptionProvider; +import fr.skytasul.quests.gui.quests.PlayerListGUI; import fr.skytasul.quests.players.PlayerAccount; import fr.skytasul.quests.players.PlayersManager; import fr.skytasul.quests.structure.Quest; @@ -25,6 +30,7 @@ public class Scoreboard extends BukkitRunnable implements Listener { + private static final Pattern QUEST_PLACEHOLDER = Pattern.compile("\\{quest_(.+)\\}"); private static final int maxLength = NMS.getMCVersion() >= 13 ? 128 : 30; private PlayerAccount acc; @@ -50,7 +56,7 @@ public class Scoreboard extends BukkitRunnable implements Listener { lines.add(new Line(line)); } - launched = QuestsAPI.getQuests().getQuestsStarted(acc, true); + launched = QuestsAPI.getQuests().getQuestsStarted(acc, false, true); hid = !manager.isWorldAllowed(p.getWorld().getName()); @@ -179,7 +185,7 @@ public void setShownQuest(Quest quest, boolean errorWhenUnknown) { if (!quest.isScoreboardEnabled()) return; if (!launched.contains(quest)) { if (errorWhenUnknown) { - launched = QuestsAPI.getQuests().getQuestsStarted(acc, true); + launched = QuestsAPI.getQuests().getQuestsStarted(acc, false, true); if (!launched.contains(quest)) throw new IllegalArgumentException("Quest is not running for player."); }else return; } @@ -189,9 +195,7 @@ public void setShownQuest(Quest quest, boolean errorWhenUnknown) { public void refreshQuestsLines(boolean updateBoard) { if (!manager.refreshLines()) return; - for (Line line : lines) { - if (line.getValue().contains("{questName}") || line.getValue().contains("{questDescription}")) line.willRefresh = true; - } + lines.stream().filter(line -> line.hasQuestPlaceholders).forEach(line -> line.willRefresh = true); if (board == null || launched.isEmpty()) { changeTime = 1; run(); @@ -201,6 +205,7 @@ public void refreshQuestsLines(boolean updateBoard) { } private void updateBoard(boolean update, boolean time) { + if (board == null && !time) return; List linesStrings = new ArrayList<>(lines.size()); for (int i = 0; i < lines.size(); i++) { Line line = lines.get(i); @@ -218,7 +223,7 @@ private void updateBoard(boolean update, boolean time) { linesStrings.add("§c§lline error"); } } - if (update) board.updateLines(linesStrings); + if (update && board != null) board.updateLines(linesStrings); } public void setCustomLine(int id, String value){ @@ -228,7 +233,7 @@ public void setCustomLine(int id, String value){ lines.add(line); }else { Line line = lines.get(id); - line.customValue = value; + line.setCustomValue(value); line.willRefresh = true; } updateBoard(true, false); @@ -240,7 +245,7 @@ public boolean resetLine(int id){ if (line.createdLine){ lines.remove(id); }else { - line.customValue = null; + line.setCustomValue(null); line.willRefresh = true; } updateBoard(true, false); @@ -267,18 +272,22 @@ public void initScoreboard(){ } class Line{ + ScoreboardLine param; int timeLeft = 0; - String customValue = null; + private String customValue = null; boolean createdLine = false; boolean willRefresh = false; String lastValue = null; List lines; + boolean hasQuestPlaceholders; + private Line(ScoreboardLine param) { this.param = param; + computeHasQuestPlaceholders(); } private boolean tryRefresh(boolean time) { @@ -287,12 +296,8 @@ private boolean tryRefresh(boolean time) { willRefresh = false; timeLeft = param.getRefreshTime(); String text = getValue(); - if (text.contains("{questName}")) { - text = shown == null ? Lang.SCOREBOARD_NONE_NAME.toString() : text.replace("{questName}", shown.getName()); - } - if (text.contains("{questDescription}")) { - text = shown == null ? Lang.SCOREBOARD_NONE_DESC.toString() : text.replace("{questDescription}", shown.getDescriptionLine(acc, Source.SCOREBOARD)); - } + if (hasQuestPlaceholders) + text = formatQuestPlaceholders(text); text = Utils.finalFormat(p, text, true); if (text.equals(lastValue)) return false; @@ -315,6 +320,49 @@ public String getValue(){ return customValue == null ? param.getValue() : customValue; } + public void setCustomValue(String value) { + customValue = value; + computeHasQuestPlaceholders(); + } + + private void computeHasQuestPlaceholders() { + hasQuestPlaceholders = QUEST_PLACEHOLDER.matcher(getValue()).find(); + } + + private String formatQuestPlaceholders(String text) { + StringBuffer textBuffer = new StringBuffer(); + Matcher matcher = QUEST_PLACEHOLDER.matcher(text); + QuestDescriptionContext lazyContext = null; + while (matcher.find()) { + String descriptionId = matcher.group(1); + String replacement; + if (descriptionId.equals("name")) { + replacement = shown == null ? Lang.SCOREBOARD_NONE_NAME.toString() : shown.getName(); + } else { + if (shown == null) { + replacement = descriptionId.equals("advancement") + ? Lang.SCOREBOARD_NONE_DESC.toString() // kept for consistency with pre-0.20 + : Lang.SCOREBOARD_NONE.toString(); + } else { + Optional optionalDescription = shown.getDescriptions().stream() + .filter(description -> description.getDescriptionId().equals(descriptionId)) + .findFirst(); + if (optionalDescription.isPresent()) { + if (lazyContext == null) + lazyContext = new QuestDescriptionContext(QuestsConfiguration.getQuestDescription(), + shown, acc, PlayerListGUI.Category.IN_PROGRESS, Source.SCOREBOARD); + replacement = String.join("\n", optionalDescription.get().provideDescription(lazyContext)); + } else { + replacement = descriptionId; + } + } + } + matcher.appendReplacement(textBuffer, replacement); + } + matcher.appendTail(textBuffer); + return textBuffer.toString(); + } + } } \ No newline at end of file diff --git a/core/src/main/java/fr/skytasul/quests/scoreboards/ScoreboardLine.java b/core/src/main/java/fr/skytasul/quests/scoreboards/ScoreboardLine.java index d8d73177..f1329933 100644 --- a/core/src/main/java/fr/skytasul/quests/scoreboards/ScoreboardLine.java +++ b/core/src/main/java/fr/skytasul/quests/scoreboards/ScoreboardLine.java @@ -12,7 +12,10 @@ public class ScoreboardLine { public ScoreboardLine(String value){ Validate.notNull(value); - this.value = value.replace("&", "§"); + this.value = value + .replace("&", "§") + .replace("{questName}", "{quest_name}") + .replace("{questDescription}", "{quest_advancement}"); } public String getValue(){ diff --git a/core/src/main/java/fr/skytasul/quests/scoreboards/ScoreboardManager.java b/core/src/main/java/fr/skytasul/quests/scoreboards/ScoreboardManager.java index a899cfb6..39daefa7 100644 --- a/core/src/main/java/fr/skytasul/quests/scoreboards/ScoreboardManager.java +++ b/core/src/main/java/fr/skytasul/quests/scoreboards/ScoreboardManager.java @@ -5,6 +5,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.UUID; import java.util.function.Consumer; import org.bukkit.Bukkit; @@ -20,9 +21,9 @@ import fr.skytasul.quests.BeautyQuests; import fr.skytasul.quests.QuestsConfiguration; import fr.skytasul.quests.api.QuestsHandler; +import fr.skytasul.quests.api.events.accounts.PlayerAccountJoinEvent; +import fr.skytasul.quests.api.events.accounts.PlayerAccountLeaveEvent; import fr.skytasul.quests.players.PlayerAccount; -import fr.skytasul.quests.players.events.PlayerAccountJoinEvent; -import fr.skytasul.quests.players.events.PlayerAccountLeaveEvent; import fr.skytasul.quests.structure.Quest; import fr.skytasul.quests.utils.DebugUtils; @@ -30,6 +31,7 @@ public class ScoreboardManager implements Listener, QuestsHandler { private final File file; private Map scoreboards; + private Map forceHiddenState; // Parameters private final List lines = new ArrayList<>(); @@ -78,13 +80,22 @@ public Scoreboard getPlayerScoreboard(Player p){ } public void removePlayerScoreboard(Player p){ - if (scoreboards.containsKey(p)) scoreboards.remove(p).cancel(); + Scoreboard scoreboard = scoreboards.remove(p); + if (scoreboard != null) { + scoreboard.cancel(); + forceHiddenState.put(p.getUniqueId(), scoreboard.isForceHidden()); + } } public void create(Player p){ if (!QuestsConfiguration.showScoreboards()) return; removePlayerScoreboard(p); - scoreboards.put(p, new Scoreboard(p, this)); + + Scoreboard scoreboard = new Scoreboard(p, this); + scoreboards.put(p, scoreboard); + + Boolean forceHidden = forceHiddenState.remove(p.getUniqueId()); + if (forceHidden != null && forceHidden.booleanValue()) scoreboard.hide(true); } @Override @@ -120,6 +131,7 @@ public void load() { DebugUtils.logMessage("Registered " + lines.size() + " lines in scoreboard"); scoreboards = new HashMap<>(); + forceHiddenState = new HashMap<>(); Bukkit.getPluginManager().registerEvents(this, BeautyQuests.getInstance()); } @@ -130,6 +142,8 @@ public void unload(){ if (!scoreboards.isEmpty()) BeautyQuests.getInstance().getLogger().info(scoreboards.size() + " scoreboards deleted."); scoreboards.clear(); scoreboards = null; + forceHiddenState.clear(); + forceHiddenState = null; } @EventHandler diff --git a/core/src/main/java/fr/skytasul/quests/stages/StageArea.java b/core/src/main/java/fr/skytasul/quests/stages/StageArea.java index d87cf97c..37aa8be8 100644 --- a/core/src/main/java/fr/skytasul/quests/stages/StageArea.java +++ b/core/src/main/java/fr/skytasul/quests/stages/StageArea.java @@ -2,16 +2,22 @@ import org.apache.commons.lang.Validate; import org.bukkit.Bukkit; +import org.bukkit.Location; import org.bukkit.World; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.player.PlayerMoveEvent; +import com.sk89q.worldedit.bukkit.BukkitAdapter; +import com.sk89q.worldguard.protection.regions.GlobalProtectedRegion; import com.sk89q.worldguard.protection.regions.ProtectedRegion; import fr.skytasul.quests.api.stages.AbstractStage; import fr.skytasul.quests.api.stages.StageCreation; +import fr.skytasul.quests.api.stages.types.Locatable; +import fr.skytasul.quests.api.stages.types.Locatable.LocatableType; +import fr.skytasul.quests.api.stages.types.Locatable.LocatedType; import fr.skytasul.quests.editors.TextEditor; import fr.skytasul.quests.gui.ItemUtils; import fr.skytasul.quests.gui.creation.stages.Line; @@ -25,12 +31,18 @@ import fr.skytasul.quests.utils.compatibility.worldguard.BQWorldGuard; import fr.skytasul.quests.utils.compatibility.worldguard.WorldGuardEntryEvent; -public class StageArea extends AbstractStage{ +@LocatableType (types = LocatedType.OTHER) +public class StageArea extends AbstractStage implements Locatable.PreciseLocatable { + + private static final long REFRESH_CENTER = 60 * 1000L; private final ProtectedRegion region; private final boolean exit; private final World world; + private Locatable.Located center = null; + private long lastCenter = 0; + public StageArea(QuestBranch branch, String regionName, String worldName, boolean exit) { super(branch); @@ -59,7 +71,7 @@ public void onPlayerMove(PlayerMoveEvent e){ @EventHandler public void onRegionEntry(WorldGuardEntryEvent e) { if (region == null) { - DebugUtils.printError("No region for " + debugName(), "area" + debugName(), 5); + DebugUtils.printError("No region for " + toString(), "area" + toString(), 5); return; } if (e.getRegionsEntered().stream().anyMatch(eventRegion -> eventRegion.getId().equals(region.getId()))) { @@ -76,6 +88,24 @@ public String descriptionLine(PlayerAccount acc, Source source){ protected Object[] descriptionFormat(PlayerAccount acc, Source source){ return new String[]{region.getId()}; } + + @Override + public Located getLocated() { + if (region instanceof GlobalProtectedRegion) return null; + + if (System.currentTimeMillis() - lastCenter > REFRESH_CENTER) { + Location centerLoc = BukkitAdapter.adapt(world, + region.getMaximumPoint() + .subtract(region.getMinimumPoint()) + .divide(2) + .add(region.getMinimumPoint())) // midpoint + .add(0.5, 0.5, 0.5); + + center = Locatable.Located.create(centerLoc); + lastCenter = System.currentTimeMillis(); + } + return center; + } public ProtectedRegion getRegion(){ return region; diff --git a/core/src/main/java/fr/skytasul/quests/stages/StageBreed.java b/core/src/main/java/fr/skytasul/quests/stages/StageBreed.java index 64a5d9b7..42475175 100644 --- a/core/src/main/java/fr/skytasul/quests/stages/StageBreed.java +++ b/core/src/main/java/fr/skytasul/quests/stages/StageBreed.java @@ -7,7 +7,7 @@ import org.bukkit.event.EventHandler; import org.bukkit.event.entity.EntityBreedEvent; -import fr.skytasul.quests.api.stages.AbstractEntityStage; +import fr.skytasul.quests.api.stages.types.AbstractEntityStage; import fr.skytasul.quests.gui.creation.stages.Line; import fr.skytasul.quests.players.PlayerAccount; import fr.skytasul.quests.structure.QuestBranch; diff --git a/core/src/main/java/fr/skytasul/quests/stages/StageBringBack.java b/core/src/main/java/fr/skytasul/quests/stages/StageBringBack.java index 42dc18e7..36590fac 100644 --- a/core/src/main/java/fr/skytasul/quests/stages/StageBringBack.java +++ b/core/src/main/java/fr/skytasul/quests/stages/StageBringBack.java @@ -6,11 +6,9 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; - import org.bukkit.configuration.ConfigurationSection; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; - import fr.skytasul.quests.QuestsConfiguration; import fr.skytasul.quests.api.comparison.ItemComparisonMap; import fr.skytasul.quests.editors.TextEditor; @@ -68,9 +66,15 @@ public boolean checkItems(Player p, boolean msg){ } } if (done) return true; - if (msg) Lang.NpcText.sendWP(p, npcName(), Utils.format(getMessage(), line), 1, 1); + if (msg) sendNeedMessage(p); return false; } + + public void sendNeedMessage(Player p) { + String message = getMessage(); + if (message != null && !message.isEmpty()) + Lang.NpcText.sendWP(p, npcName(), Utils.format(message, line), 1, 1); + } public void removeItems(Player p){ for(ItemStack is : items){ @@ -100,7 +104,8 @@ protected Object[] descriptionFormat(PlayerAccount acc, Source source){ @Override public void start(PlayerAccount acc) { super.start(acc); - if (acc.isCurrent() && sendStartMessage()) Lang.NpcText.sendWP(acc.getPlayer(), npcName(), Lang.NEED_OBJECTS.format(line), 1, 1); + if (acc.isCurrent() && sendStartMessage()) + sendNeedMessage(acc.getPlayer()); } @Override @@ -146,7 +151,7 @@ public static StageBringBack deserialize(ConfigurationSection section, QuestBran String customMessage = section.getString("customMessage", null); ItemComparisonMap comparisons; if (section.contains("itemComparisons")) { - comparisons = new ItemComparisonMap((Map) section.getConfigurationSection("itemComparisons").getValues(false)); + comparisons = new ItemComparisonMap(section.getConfigurationSection("itemComparisons")); }else comparisons = new ItemComparisonMap(); StageBringBack st = new StageBringBack(branch, items, customMessage, comparisons); st.loadDatas(section); @@ -188,7 +193,7 @@ public AbstractCreator(Line line, boolean ending) { public void setItems(List items) { this.items = Utils.combineItems(items); - line.editItem(5, ItemUtils.lore(line.getItem(5), Lang.optionValue.format(this.items.size() + " item(s)"))); + line.editItem(5, ItemUtils.lore(line.getItem(5), Lang.optionValue.format(Lang.AmountItems.format(this.items.size())))); } public void setMessage(String message) { @@ -198,7 +203,7 @@ public void setMessage(String message) { public void setComparisons(ItemComparisonMap comparisons) { this.comparisons = comparisons; - line.editItem(10, ItemUtils.lore(line.getItem(10), Lang.optionValue.format(this.comparisons.getEffective().size() + " comparison(s)"))); + line.editItem(10, ItemUtils.lore(line.getItem(10), Lang.optionValue.format(Lang.AmountComparisons.format(this.comparisons.getEffective().size())))); } @Override @@ -206,7 +211,7 @@ public void start(Player p) { new ItemsGUI(items -> { setItems(items); super.start(p); - }, Collections.EMPTY_LIST).create(p); + }, Collections.emptyList()).create(p); } @Override diff --git a/core/src/main/java/fr/skytasul/quests/stages/StageChat.java b/core/src/main/java/fr/skytasul/quests/stages/StageChat.java index f8227bdf..b5c193da 100644 --- a/core/src/main/java/fr/skytasul/quests/stages/StageChat.java +++ b/core/src/main/java/fr/skytasul/quests/stages/StageChat.java @@ -77,7 +77,7 @@ private boolean check(String message, Player p) { if (placeholders) message = Utils.finalFormat(p, message, true); if (!(ignoreCase ? message.equalsIgnoreCase(text) : message.equals(text))) return false; if (!hasStarted(p)) return false; - if (canUpdate(p)) Utils.runSync(() -> finishStage(p)); + if (canUpdate(p)) finishStage(p); return true; } diff --git a/core/src/main/java/fr/skytasul/quests/stages/StageCraft.java b/core/src/main/java/fr/skytasul/quests/stages/StageCraft.java index d6491244..60f72069 100644 --- a/core/src/main/java/fr/skytasul/quests/stages/StageCraft.java +++ b/core/src/main/java/fr/skytasul/quests/stages/StageCraft.java @@ -8,14 +8,13 @@ import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; -import org.bukkit.event.inventory.CraftItemEvent; -import org.bukkit.inventory.ComplexRecipe; -import org.bukkit.inventory.CraftingInventory; +import org.bukkit.event.inventory.FurnaceExtractEvent; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemStack; import fr.skytasul.quests.QuestsConfiguration; import fr.skytasul.quests.api.comparison.ItemComparisonMap; +import fr.skytasul.quests.api.events.BQCraftEvent; import fr.skytasul.quests.api.stages.AbstractStage; import fr.skytasul.quests.api.stages.StageCreation; import fr.skytasul.quests.gui.ItemUtils; @@ -48,34 +47,42 @@ public StageCraft(QuestBranch branch, ItemStack result, ItemComparisonMap compar public ItemStack getItem(){ return result; } - - @EventHandler (priority = EventPriority.MONITOR) - public void onCraft(CraftItemEvent e){ - Player p = (Player) e.getView().getPlayer(); + + @EventHandler (priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onFurnaceExtract(FurnaceExtractEvent event) { + Player p = event.getPlayer(); PlayerAccount acc = PlayersManager.getPlayerAccount(p); - ItemStack item = e.getRecipe().getResult(); - if (item.getType() == Material.AIR && e.getRecipe() instanceof ComplexRecipe) { - String key = ((ComplexRecipe) e.getRecipe()).getKey().toString(); - if (key.equals("minecraft:suspicious_stew")) { - item = XMaterial.SUSPICIOUS_STEW.parseItem(); + + if (comparisons.isSimilar(result, new ItemStack(event.getItemType())) && branch.hasStageLaunched(acc, this) && canUpdate(p, true)) { + int amount = getPlayerAmount(acc) - event.getItemAmount(); + if (amount <= 0) { + finishStage(p); + }else { + updateObjective(acc, p, "amount", amount); } } - + } + + @EventHandler + public void onCraft(BQCraftEvent event) { + Player p = event.getPlayer(); + PlayerAccount acc = PlayersManager.getPlayerAccount(p); if (branch.hasStageLaunched(acc, this) && canUpdate(p)) { + ItemStack item = event.getResult(); if (comparisons.isSimilar(result, item)) { int recipeAmount = item.getAmount(); - switch (e.getClick()) { + switch (event.getClickEvent().getClick()) { case NUMBER_KEY: // If hotbar slot selected is full, crafting fails (vanilla behavior, even when items match) - if (e.getWhoClicked().getInventory().getItem(e.getHotbarButton()) != null) recipeAmount = 0; + if (p.getInventory().getItem(event.getClickEvent().getHotbarButton()) != null) recipeAmount = 0; break; case DROP: case CONTROL_DROP: // If we are holding items, craft-via-drop fails (vanilla behavior) - ItemStack cursor = e.getCursor(); + ItemStack cursor = event.getClickEvent().getCursor(); if (cursor != null && cursor.getType() != Material.AIR) recipeAmount = 0; break; @@ -83,20 +90,24 @@ public void onCraft(CraftItemEvent e){ case SHIFT_LEFT: if (recipeAmount == 0) break; - int maxCraftable = getMaxCraftAmount(e.getInventory()); - int capacity = fits(item, e.getView().getBottomInventory()); + int capacity = fits(item, p.getInventory()); // If we can't fit everything, increase "space" to include the items dropped by crafting // (Think: Uncrafting 8 iron blocks into 1 slot) - if (capacity < maxCraftable) - maxCraftable = ((capacity + recipeAmount - 1) / recipeAmount) * recipeAmount; - - recipeAmount = maxCraftable; + if (capacity < event.getMaxCraftable()) { + recipeAmount = ((capacity + recipeAmount - 1) / recipeAmount) * recipeAmount; + }else recipeAmount = event.getMaxCraftable(); break; default: - cursor = e.getCursor(); - if (cursor != null && cursor.getType() != Material.AIR && !cursor.isSimilar(item)) recipeAmount = 0; + cursor = event.getClickEvent().getCursor(); + if (cursor != null && cursor.getType() != Material.AIR) { + if (cursor.isSimilar(item)) { + if (cursor.getAmount() + item.getAmount() > cursor.getMaxStackSize()) recipeAmount = 0; + }else { + recipeAmount = 0; + } + } break; } @@ -142,20 +153,7 @@ protected void serialize(ConfigurationSection section) { } public static StageCraft deserialize(ConfigurationSection section, QuestBranch branch) { - return new StageCraft(branch, ItemStack.deserialize(section.getConfigurationSection("result").getValues(false)), section.contains("itemComparisons") ? new ItemComparisonMap((Map) section.getConfigurationSection("itemComparisons").getValues(false)) : new ItemComparisonMap()); - } - - public static int getMaxCraftAmount(CraftingInventory inv) { - if (inv.getResult() == null) return 0; - - int resultCount = inv.getResult().getAmount(); - int materialCount = Integer.MAX_VALUE; - - for (ItemStack is : inv.getMatrix()) - if (is != null && is.getAmount() < materialCount) - materialCount = is.getAmount(); - - return resultCount * materialCount; + return new StageCraft(branch, ItemStack.deserialize(section.getConfigurationSection("result").getValues(false)), section.contains("itemComparisons") ? new ItemComparisonMap(section.getConfigurationSection("itemComparisons")) : new ItemComparisonMap()); } public static int fits(ItemStack stack, Inventory inv) { @@ -201,7 +199,7 @@ public void setItem(ItemStack item) { public void setComparisons(ItemComparisonMap comparisons) { this.comparisons = comparisons; - line.editItem(COMPARISONS_SLOT, ItemUtils.lore(line.getItem(COMPARISONS_SLOT), Lang.optionValue.format(this.comparisons.getEffective().size() + " comparison(s)"))); + line.editItem(COMPARISONS_SLOT, ItemUtils.lore(line.getItem(COMPARISONS_SLOT), Lang.optionValue.format(Lang.AmountComparisons.format(this.comparisons.getEffective().size())))); } @Override diff --git a/core/src/main/java/fr/skytasul/quests/stages/StageDealDamage.java b/core/src/main/java/fr/skytasul/quests/stages/StageDealDamage.java new file mode 100644 index 00000000..16e6e2b6 --- /dev/null +++ b/core/src/main/java/fr/skytasul/quests/stages/StageDealDamage.java @@ -0,0 +1,191 @@ +package fr.skytasul.quests.stages; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import org.bukkit.DyeColor; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.entity.Player; +import org.bukkit.entity.Projectile; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.entity.EntityDamageByEntityEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.projectiles.ProjectileSource; + +import fr.skytasul.quests.api.mobs.Mob; +import fr.skytasul.quests.api.options.QuestOption; +import fr.skytasul.quests.api.stages.AbstractStage; +import fr.skytasul.quests.api.stages.StageCreation; +import fr.skytasul.quests.editors.TextEditor; +import fr.skytasul.quests.editors.checkers.NumberParser; +import fr.skytasul.quests.gui.ItemUtils; +import fr.skytasul.quests.gui.creation.stages.Line; +import fr.skytasul.quests.gui.mobs.MobSelectionGUI; +import fr.skytasul.quests.gui.templates.ListGUI; +import fr.skytasul.quests.players.PlayerAccount; +import fr.skytasul.quests.players.PlayersManager; +import fr.skytasul.quests.structure.QuestBranch; +import fr.skytasul.quests.structure.QuestBranch.Source; +import fr.skytasul.quests.utils.Lang; +import fr.skytasul.quests.utils.XMaterial; + +@SuppressWarnings ("rawtypes") +public class StageDealDamage extends AbstractStage { + + private final double damage; + private final List targetMobs; + + private final String targetMobsString; + + public StageDealDamage(QuestBranch branch, double damage, List targetMobs) { + super(branch); + this.damage = damage; + this.targetMobs = targetMobs; + + targetMobsString = getTargetMobsString(targetMobs); + } + + private static String getTargetMobsString(List targetMobs) { + if (targetMobs == null || targetMobs.isEmpty()) return Lang.EntityTypeAny.toString(); + return targetMobs.stream().map(Mob::getName).collect(Collectors.joining(", ")); + } + + @Override + protected void initPlayerDatas(PlayerAccount acc, Map datas) { + datas.put("amount", damage); + } + + @EventHandler (priority = EventPriority.MONITOR) + public void onDamage(EntityDamageByEntityEvent event) { + Player player; + if (event.getDamager() instanceof Projectile) { + ProjectileSource projectileShooter = ((Projectile) event.getDamager()).getShooter(); + if (!(projectileShooter instanceof Player)) return; + player = (Player) projectileShooter; + }else if (event.getDamager() instanceof Player) { + player = (Player) event.getDamager(); + }else return; + + if (targetMobs != null && !targetMobs.isEmpty() + && targetMobs.stream().noneMatch(mob -> mob.appliesEntity(event.getEntity()))) return; + + PlayerAccount account = PlayersManager.getPlayerAccount(player); + + if (!branch.hasStageLaunched(account, this)) return; + if (!canUpdate(player)) return; + + double amount = getData(account, "amount"); + amount -= event.getFinalDamage(); + if (amount <= 0) { + finishStage(player); + }else { + updateObjective(account, player, "amount", amount); + } + } + + @Override + protected String descriptionLine(PlayerAccount acc, Source source) { + return (targetMobs == null || targetMobs.isEmpty() ? Lang.SCOREBOARD_DEAL_DAMAGE_ANY : Lang.SCOREBOARD_DEAL_DAMAGE_MOBS).format(descriptionFormat(acc, source)); + } + + @Override + protected Object[] descriptionFormat(PlayerAccount acc, Source source) { + return new Object[] { (Supplier) () -> Integer.toString(super.getData(acc, "amount").intValue()), targetMobsString }; + } + + @Override + protected void serialize(ConfigurationSection section) { + section.set("damage", damage); + if (targetMobs != null && !targetMobs.isEmpty()) + section.set("targetMobs", targetMobs.stream().map(Mob::serialize).collect(Collectors.toList())); + } + + public static StageDealDamage deserialize(ConfigurationSection section, QuestBranch branch) { + return new StageDealDamage(branch, + section.getDouble("damage"), + section.contains("targetMobs") ? section.getMapList("targetMobs").stream().map(map -> Mob.deserialize((Map) map)).collect(Collectors.toList()) : null); + } + + public static class Creator extends StageCreation { + + private static final int SLOT_DAMAGE = 6; + private static final int SLOT_MOBS = 7; + + private double damage; + private List targetMobs; + + public Creator(Line line, boolean ending) { + super(line, ending); + + line.setItem(SLOT_DAMAGE, ItemUtils.item(XMaterial.REDSTONE, Lang.stageDealDamageValue.toString()), (p, item) -> { + Lang.DAMAGE_AMOUNT.send(p); + new TextEditor<>(p, () -> reopenGUI(p, false), newDamage -> { + setDamage(newDamage); + reopenGUI(p, false); + }, NumberParser.DOUBLE_PARSER_STRICT_POSITIVE).enter(); + }); + + line.setItem(SLOT_MOBS, ItemUtils.item(XMaterial.BLAZE_SPAWN_EGG, Lang.stageDealDamageMobs.toString(), QuestOption.formatNullableValue(Lang.EntityTypeAny.toString(), true)), (p, item) -> { + new ListGUI(Lang.stageDealDamageMobs.toString(), DyeColor.RED, targetMobs == null ? Collections.emptyList() : targetMobs) { + + @Override + public void finish(List objects) { + setTargetMobs(objects.isEmpty() ? null : objects); + reopenGUI(p, true); + } + + @Override + public ItemStack getObjectItemStack(Mob object) { + return ItemUtils.item(object.getMobItem(), object.getName(), Lang.RemoveMid.toString()); + } + + @Override + public void createObject(Function callback) { + new MobSelectionGUI(callback::apply).create(p); + } + + }.create(p); + }); + } + + public void setDamage(double damage) { + this.damage = damage; + line.editItem(SLOT_DAMAGE, ItemUtils.lore(line.getItem(SLOT_DAMAGE), QuestOption.formatNullableValue(Double.toString(damage)))); + } + + public void setTargetMobs(List targetMobs) { + this.targetMobs = targetMobs; + boolean noMobs = targetMobs == null || targetMobs.isEmpty(); + line.editItem(SLOT_MOBS, ItemUtils.lore(line.getItem(SLOT_MOBS), QuestOption.formatNullableValue(getTargetMobsString(targetMobs), noMobs))); + } + + @Override + public void edit(StageDealDamage stage) { + super.edit(stage); + setDamage(stage.damage); + setTargetMobs(stage.targetMobs); + } + + @Override + public void start(Player p) { + super.start(p); + Lang.DAMAGE_AMOUNT.send(p); + new TextEditor<>(p, removeAndReopen(p, false), newDamage -> { + setDamage(newDamage); + reopenGUI(p, false); + }, NumberParser.DOUBLE_PARSER_STRICT_POSITIVE).enter(); + } + + @Override + protected StageDealDamage finishStage(QuestBranch branch) { + return new StageDealDamage(branch, damage, targetMobs); + } + + } + +} diff --git a/core/src/main/java/fr/skytasul/quests/stages/StageDeath.java b/core/src/main/java/fr/skytasul/quests/stages/StageDeath.java new file mode 100644 index 00000000..95d63aca --- /dev/null +++ b/core/src/main/java/fr/skytasul/quests/stages/StageDeath.java @@ -0,0 +1,109 @@ +package fr.skytasul.quests.stages; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.entity.EntityDamageEvent; +import org.bukkit.event.entity.EntityDamageEvent.DamageCause; +import org.bukkit.event.entity.PlayerDeathEvent; + +import fr.skytasul.quests.api.stages.AbstractStage; +import fr.skytasul.quests.api.stages.StageCreation; +import fr.skytasul.quests.gui.ItemUtils; +import fr.skytasul.quests.gui.creation.stages.Line; +import fr.skytasul.quests.gui.misc.DamageCausesGUI; +import fr.skytasul.quests.players.PlayerAccount; +import fr.skytasul.quests.structure.QuestBranch; +import fr.skytasul.quests.structure.QuestBranch.Source; +import fr.skytasul.quests.utils.Lang; +import fr.skytasul.quests.utils.XMaterial; + +public class StageDeath extends AbstractStage { + + private List causes; + + public StageDeath(QuestBranch branch, List causes) { + super(branch); + this.causes = causes; + } + + @EventHandler + public void onPlayerDeath(PlayerDeathEvent event) { + Player p = event.getEntity(); + if (!hasStarted(p)) return; + + if (!causes.isEmpty()) { + EntityDamageEvent lastDamage = p.getLastDamageCause(); + if (lastDamage == null) return; + if (!causes.contains(lastDamage.getCause())) return; + } + + if (canUpdate(p, true)) finishStage(p); + } + + @Override + protected String descriptionLine(PlayerAccount acc, Source source) { + return Lang.SCOREBOARD_DIE.toString(); + } + + @Override + protected void serialize(ConfigurationSection section) { + if (!causes.isEmpty()) section.set("causes", causes.stream().map(DamageCause::name).collect(Collectors.toList())); + } + + public static StageDeath deserialize(ConfigurationSection section, QuestBranch branch) { + List causes; + if (section.contains("causes")) { + causes = section.getStringList("causes").stream().map(DamageCause::valueOf).collect(Collectors.toList()); + }else { + causes = Collections.emptyList(); + } + return new StageDeath(branch, causes); + } + + public static class Creator extends StageCreation { + + private static final int CAUSES_SLOT = 7; + + private List causes; + + public Creator(Line line, boolean ending) { + super(line, ending); + + line.setItem(CAUSES_SLOT, ItemUtils.item(XMaterial.SKELETON_SKULL, Lang.stageDeathCauses.toString()), (p, item) -> { + new DamageCausesGUI(causes, newCauses -> { + setCauses(newCauses); + reopenGUI(p, true); + }).create(p); + }); + } + + public void setCauses(List causes) { + this.causes = causes; + line.editItem(CAUSES_SLOT, ItemUtils.lore(line.getItem(CAUSES_SLOT), Lang.optionValue.format(causes.isEmpty() ? Lang.stageDeathCauseAny : Lang.stageDeathCausesSet.format(causes.size())))); + } + + @Override + public void start(Player p) { + super.start(p); + setCauses(Collections.emptyList()); + } + + @Override + public void edit(StageDeath stage) { + super.edit(stage); + setCauses(stage.causes); + } + + @Override + protected StageDeath finishStage(QuestBranch branch) { + return new StageDeath(branch, causes); + } + + } + +} diff --git a/core/src/main/java/fr/skytasul/quests/stages/StageEatDrink.java b/core/src/main/java/fr/skytasul/quests/stages/StageEatDrink.java new file mode 100644 index 00000000..50a96201 --- /dev/null +++ b/core/src/main/java/fr/skytasul/quests/stages/StageEatDrink.java @@ -0,0 +1,62 @@ +package fr.skytasul.quests.stages; + +import java.util.Map; +import java.util.Map.Entry; + +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.event.EventHandler; +import org.bukkit.event.player.PlayerItemConsumeEvent; +import org.bukkit.inventory.ItemStack; + +import fr.skytasul.quests.api.comparison.ItemComparisonMap; +import fr.skytasul.quests.api.stages.types.AbstractItemStage; +import fr.skytasul.quests.gui.ItemUtils; +import fr.skytasul.quests.gui.creation.stages.Line; +import fr.skytasul.quests.players.PlayerAccount; +import fr.skytasul.quests.players.PlayersManager; +import fr.skytasul.quests.structure.QuestBranch; +import fr.skytasul.quests.structure.QuestBranch.Source; +import fr.skytasul.quests.utils.Lang; +import fr.skytasul.quests.utils.XMaterial; + +public class StageEatDrink extends AbstractItemStage { + + public StageEatDrink(QuestBranch branch, Map> objects, ItemComparisonMap comparisons) { + super(branch, objects, comparisons); + } + + public StageEatDrink(ConfigurationSection section, QuestBranch branch) { + super(branch, section); + } + + @Override + protected String descriptionLine(PlayerAccount acc, Source source) { + return Lang.SCOREBOARD_EAT_DRINK.format(super.descriptionLine(acc, source)); + } + + @EventHandler + public void onItemConsume(PlayerItemConsumeEvent event) { + event(PlayersManager.getPlayerAccount(event.getPlayer()), event.getPlayer(), event.getItem(), 1); + } + + public static class Creator extends AbstractItemStage.Creator { + + private static final ItemStack editItems = ItemUtils.item(XMaterial.COOKED_PORKCHOP, Lang.stageEatDrinkItems.toString()); + + public Creator(Line line, boolean ending) { + super(line, ending); + } + + @Override + protected ItemStack getEditItem() { + return editItems; + } + + @Override + protected StageEatDrink finishStage(QuestBranch branch, Map> itemsMap, ItemComparisonMap comparisons) { + return new StageEatDrink(branch, itemsMap, comparisons); + } + + } + +} diff --git a/core/src/main/java/fr/skytasul/quests/stages/StageEnchant.java b/core/src/main/java/fr/skytasul/quests/stages/StageEnchant.java index aa5c9327..35ea649f 100644 --- a/core/src/main/java/fr/skytasul/quests/stages/StageEnchant.java +++ b/core/src/main/java/fr/skytasul/quests/stages/StageEnchant.java @@ -11,7 +11,7 @@ import org.bukkit.inventory.meta.ItemMeta; import fr.skytasul.quests.api.comparison.ItemComparisonMap; -import fr.skytasul.quests.api.stages.AbstractItemStage; +import fr.skytasul.quests.api.stages.types.AbstractItemStage; import fr.skytasul.quests.gui.ItemUtils; import fr.skytasul.quests.gui.creation.stages.Line; import fr.skytasul.quests.players.PlayerAccount; diff --git a/core/src/main/java/fr/skytasul/quests/stages/StageFish.java b/core/src/main/java/fr/skytasul/quests/stages/StageFish.java index ccdda3d7..326772b5 100644 --- a/core/src/main/java/fr/skytasul/quests/stages/StageFish.java +++ b/core/src/main/java/fr/skytasul/quests/stages/StageFish.java @@ -13,7 +13,7 @@ import org.bukkit.inventory.ItemStack; import fr.skytasul.quests.api.comparison.ItemComparisonMap; -import fr.skytasul.quests.api.stages.AbstractItemStage; +import fr.skytasul.quests.api.stages.types.AbstractItemStage; import fr.skytasul.quests.gui.ItemUtils; import fr.skytasul.quests.gui.creation.stages.Line; import fr.skytasul.quests.players.PlayerAccount; diff --git a/core/src/main/java/fr/skytasul/quests/stages/StageInteract.java b/core/src/main/java/fr/skytasul/quests/stages/StageInteract.java index b40ca5c9..51ff5015 100644 --- a/core/src/main/java/fr/skytasul/quests/stages/StageInteract.java +++ b/core/src/main/java/fr/skytasul/quests/stages/StageInteract.java @@ -1,7 +1,10 @@ package fr.skytasul.quests.stages; +import java.util.Collections; +import java.util.Spliterator; import org.bukkit.Bukkit; import org.bukkit.Location; +import org.bukkit.block.Block; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; @@ -9,12 +12,16 @@ import org.bukkit.event.inventory.ClickType; import org.bukkit.event.inventory.InventoryType; import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.inventory.EquipmentSlot; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemStack; - +import fr.skytasul.quests.BeautyQuests; import fr.skytasul.quests.api.options.QuestOption; import fr.skytasul.quests.api.stages.AbstractStage; import fr.skytasul.quests.api.stages.StageCreation; +import fr.skytasul.quests.api.stages.types.Locatable; +import fr.skytasul.quests.api.stages.types.Locatable.LocatableType; +import fr.skytasul.quests.api.stages.types.Locatable.LocatedType; import fr.skytasul.quests.editors.WaitBlockClick; import fr.skytasul.quests.gui.CustomInventory; import fr.skytasul.quests.gui.ItemUtils; @@ -26,27 +33,36 @@ import fr.skytasul.quests.utils.Lang; import fr.skytasul.quests.utils.Utils; import fr.skytasul.quests.utils.XMaterial; +import fr.skytasul.quests.utils.nms.NMS; import fr.skytasul.quests.utils.types.BQBlock; +import fr.skytasul.quests.utils.types.BQLocation; -public class StageInteract extends AbstractStage { +@LocatableType (types = { LocatedType.BLOCK, LocatedType.OTHER }) +public class StageInteract extends AbstractStage implements Locatable.MultipleLocatable, Locatable.PreciseLocatable { - private boolean left; - private Location lc; - private BQBlock block; + private final boolean left; + private final BQLocation lc; + private final BQBlock block; - public StageInteract(QuestBranch branch, boolean leftClick, Location location) { + private Located.LocatedBlock locatedBlock; + + public StageInteract(QuestBranch branch, boolean leftClick, BQLocation location) { super(branch); this.left = leftClick; - this.lc = location.getBlock().getLocation(); + this.lc = new BQLocation(location.getWorldName(), location.getBlockX(), location.getBlockY(), location.getBlockZ()); + + this.block = null; } public StageInteract(QuestBranch branch, boolean leftClick, BQBlock block) { super(branch); this.left = leftClick; this.block = block; + + this.lc = null; } - public Location getLocation(){ + public BQLocation getLocation() { return lc; } @@ -58,15 +74,42 @@ public boolean needLeftClick(){ return left; } + @Override + public Located getLocated() { + if (lc == null) + return null; + if (locatedBlock == null) { + Block realBlock = lc.getMatchingBlock(); + if (realBlock != null) + locatedBlock = Located.LocatedBlock.create(realBlock); + } + return locatedBlock; + } + + @Override + public Spliterator getNearbyLocated(NearbyFetcher fetcher) { + if (block == null) return null; + + return BQBlock.getNearbyBlocks(fetcher, Collections.singleton(block)); + } + @EventHandler public void onInteract(PlayerInteractEvent e){ if (e.getClickedBlock() == null) return; + if (NMS.getMCVersion() >= 9 && e.getHand() != EquipmentSlot.HAND) return; + if (left){ if (e.getAction() != Action.LEFT_CLICK_BLOCK) return; }else if (e.getAction() != Action.RIGHT_CLICK_BLOCK) return; + if (lc != null) { - if (!e.getClickedBlock().getLocation().equals(lc)) return; - }else if (!block.applies(e.getClickedBlock())) return; + if (!lc.equals(e.getClickedBlock().getLocation())) return; + }else if (block != null) { + if (!block.applies(e.getClickedBlock())) return; + }else { + BeautyQuests.logger.warning("No block nor location set for " + toString()); + return; + } Player p = e.getPlayer(); if (hasStarted(p) && canUpdate(p)) { @@ -90,7 +133,7 @@ protected void serialize(ConfigurationSection section) { public static StageInteract deserialize(ConfigurationSection section, QuestBranch branch) { if (section.contains("location")) { - return new StageInteract(branch, section.getBoolean("leftClick"), Location.deserialize(section.getConfigurationSection("location").getValues(false))); + return new StageInteract(branch, section.getBoolean("leftClick"), BQLocation.deserialize(section.getConfigurationSection("location").getValues(false))); }else { BQBlock block; if (section.contains("material")) { @@ -178,7 +221,7 @@ public void edit(StageInteract stage) { @Override public StageInteract finishStage(QuestBranch branch) { if (location != null) { - return new StageInteract(branch, leftClick, location); + return new StageInteract(branch, leftClick, new BQLocation(location)); }else return new StageInteract(branch, leftClick, block); } diff --git a/core/src/main/java/fr/skytasul/quests/stages/StageLocation.java b/core/src/main/java/fr/skytasul/quests/stages/StageLocation.java index 99c21a69..cce46378 100644 --- a/core/src/main/java/fr/skytasul/quests/stages/StageLocation.java +++ b/core/src/main/java/fr/skytasul/quests/stages/StageLocation.java @@ -11,8 +11,10 @@ import fr.skytasul.quests.QuestsConfiguration; import fr.skytasul.quests.api.options.QuestOption; import fr.skytasul.quests.api.stages.AbstractStage; -import fr.skytasul.quests.api.stages.Locatable; import fr.skytasul.quests.api.stages.StageCreation; +import fr.skytasul.quests.api.stages.types.Locatable; +import fr.skytasul.quests.api.stages.types.Locatable.LocatableType; +import fr.skytasul.quests.api.stages.types.Locatable.LocatedType; import fr.skytasul.quests.editors.TextEditor; import fr.skytasul.quests.editors.WaitClick; import fr.skytasul.quests.editors.checkers.NumberParser; @@ -29,7 +31,8 @@ import fr.skytasul.quests.utils.compatibility.GPS; import fr.skytasul.quests.utils.types.BQLocation; -public class StageLocation extends AbstractStage implements Locatable { +@LocatableType (types = LocatedType.OTHER) +public class StageLocation extends AbstractStage implements Locatable.PreciseLocatable { private final BQLocation lc; private final int radius; @@ -48,16 +51,20 @@ public StageLocation(QuestBranch branch, BQLocation lc, int radius, boolean gps) this.descMessage = Lang.SCOREBOARD_LOCATION.format(lc.getBlockX(), lc.getBlockY(), lc.getBlockZ(), lc.getWorldName()); } - @Override public BQLocation getLocation() { return lc; } @Override - public boolean isShown() { + public boolean isShown(Player player) { return isGPSEnabled(); } + @Override + public Located getLocated() { + return lc; + } + public int getRadius(){ return radius; } diff --git a/core/src/main/java/fr/skytasul/quests/stages/StageMelt.java b/core/src/main/java/fr/skytasul/quests/stages/StageMelt.java index 4fa13d8f..1d677674 100644 --- a/core/src/main/java/fr/skytasul/quests/stages/StageMelt.java +++ b/core/src/main/java/fr/skytasul/quests/stages/StageMelt.java @@ -10,7 +10,7 @@ import org.bukkit.inventory.ItemStack; import fr.skytasul.quests.api.comparison.ItemComparisonMap; -import fr.skytasul.quests.api.stages.AbstractItemStage; +import fr.skytasul.quests.api.stages.types.AbstractItemStage; import fr.skytasul.quests.gui.ItemUtils; import fr.skytasul.quests.gui.creation.stages.Line; import fr.skytasul.quests.players.PlayerAccount; diff --git a/core/src/main/java/fr/skytasul/quests/stages/StageMine.java b/core/src/main/java/fr/skytasul/quests/stages/StageMine.java index fc859adb..75267c75 100644 --- a/core/src/main/java/fr/skytasul/quests/stages/StageMine.java +++ b/core/src/main/java/fr/skytasul/quests/stages/StageMine.java @@ -3,22 +3,25 @@ import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; - +import java.util.Spliterator; +import java.util.stream.Collectors; import org.bukkit.block.Block; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.inventory.ItemStack; import org.bukkit.metadata.FixedMetadataValue; - +import com.gestankbratwurst.playerblocktracker.PlayerBlockTracker; import fr.skytasul.quests.BeautyQuests; +import fr.skytasul.quests.QuestsConfiguration; import fr.skytasul.quests.api.events.BQBlockBreakEvent; -import fr.skytasul.quests.api.stages.AbstractCountableStage; -import fr.skytasul.quests.api.stages.StageCreation; -import fr.skytasul.quests.gui.Inventories; +import fr.skytasul.quests.api.stages.types.AbstractCountableBlockStage; +import fr.skytasul.quests.api.stages.types.Locatable; +import fr.skytasul.quests.api.stages.types.Locatable.LocatableType; +import fr.skytasul.quests.api.stages.types.Locatable.LocatedType; import fr.skytasul.quests.gui.ItemUtils; -import fr.skytasul.quests.gui.blocks.BlocksGUI; import fr.skytasul.quests.gui.creation.stages.Line; import fr.skytasul.quests.players.PlayerAccount; import fr.skytasul.quests.players.PlayersManager; @@ -28,7 +31,8 @@ import fr.skytasul.quests.utils.XMaterial; import fr.skytasul.quests.utils.types.BQBlock; -public class StageMine extends AbstractCountableStage { +@LocatableType (types = LocatedType.BLOCK) +public class StageMine extends AbstractCountableBlockStage implements Locatable.MultipleLocatable { private boolean placeCancelled; @@ -55,8 +59,15 @@ public void onMine(BQBlockBreakEvent e) { PlayerAccount acc = PlayersManager.getPlayerAccount(p); if (branch.hasStageLaunched(acc, this)){ for (Block block : e.getBlocks()) { - if (placeCancelled && block.hasMetadata("playerInStage")) { - if (block.getMetadata("playerInStage").get(0).asString().equals(p.getName())) return; + if (placeCancelled) { + if (QuestsConfiguration.usePlayerBlockTracker()) { + if (PlayerBlockTracker.isTracked(block)) return; + } else { + if (block.hasMetadata("playerInStage")) { + if (block.getMetadata("playerInStage").get(0).asString().equals(p.getName())) + return; + } + } } if (event(acc, p, block, 1)) return; } @@ -65,11 +76,15 @@ public void onMine(BQBlockBreakEvent e) { @EventHandler (priority = EventPriority.MONITOR) public void onPlace(BlockPlaceEvent e){ + if (QuestsConfiguration.usePlayerBlockTracker()) + return; + if (e.isCancelled() || !placeCancelled) return; Player p = e.getPlayer(); PlayerAccount acc = PlayersManager.getPlayerAccount(p); if (!branch.hasStageLaunched(acc, this)) return; - Map playerBlocks = getPlayerRemainings(acc); + Map playerBlocks = getPlayerRemainings(acc, true); + if (playerBlocks == null) return; for (Integer id : playerBlocks.keySet()) { if (objectApplies(super.objects.get(id).getKey(), e.getBlock())) { e.getBlock().setMetadata("playerInStage", new FixedMetadataValue(BeautyQuests.getInstance(), p.getName())); @@ -79,26 +94,10 @@ public void onPlace(BlockPlaceEvent e){ } @Override - protected boolean objectApplies(BQBlock object, Object other) { - if (other instanceof Block) return object.applies((Block) other); - return super.objectApplies(object, other); + public Spliterator getNearbyLocated(NearbyFetcher fetcher) { + return BQBlock.getNearbyBlocks(fetcher, objects.values().stream().map(Entry::getKey).collect(Collectors.toList())); } - @Override - protected String getName(BQBlock object) { - return object.getName(); - } - - @Override - protected Object serialize(BQBlock object) { - return object.getAsString(); - } - - @Override - protected BQBlock deserialize(Object object) { - return BQBlock.fromString((String) object); - } - @Override protected void serialize(ConfigurationSection section) { super.serialize(section); @@ -113,28 +112,19 @@ public static StageMine deserialize(ConfigurationSection section, QuestBranch br return stage; } - public static class Creator extends StageCreation { + public static class Creator extends AbstractCountableBlockStage.AbstractCreator { - private Map> blocks; private boolean prevent = false; public Creator(Line line, boolean ending) { super(line, ending); - line.setItem(7, ItemUtils.item(XMaterial.STONE_PICKAXE, Lang.editBlocksMine.toString()), (p, item) -> { - BlocksGUI blocksGUI = Inventories.create(p, new BlocksGUI()); - blocksGUI.setBlocksFromMap(blocks); - blocksGUI.run = obj -> { - setBlocks(obj); - reopenGUI(p, true); - }; - }); line.setItem(6, ItemUtils.itemSwitch(Lang.preventBlockPlace.toString(), prevent), (p, item) -> setPrevent(ItemUtils.toggle(item))); } - public void setBlocks(Map> blocks) { - this.blocks = blocks; - line.editItem(7, ItemUtils.lore(line.getItem(7), Lang.optionValue.format(blocks.size() + " block(s)"))); + @Override + protected ItemStack getBlocksItem() { + return ItemUtils.item(XMaterial.STONE_PICKAXE, Lang.editBlocksMine.toString()); } public void setPrevent(boolean prevent) { @@ -143,21 +133,10 @@ public void setPrevent(boolean prevent) { line.editItem(6, ItemUtils.set(line.getItem(6), prevent)); } } - - @Override - public void start(Player p) { - super.start(p); - BlocksGUI blocksGUI = Inventories.create(p, new BlocksGUI()); - blocksGUI.run = obj -> { - setBlocks(obj); - reopenGUI(p, true); - }; - } @Override public void edit(StageMine stage) { super.edit(stage); - setBlocks(stage.cloneObjects()); setPrevent(stage.isPlaceCancelled()); } diff --git a/core/src/main/java/fr/skytasul/quests/stages/StageMobs.java b/core/src/main/java/fr/skytasul/quests/stages/StageMobs.java index 4cdd031f..14931075 100644 --- a/core/src/main/java/fr/skytasul/quests/stages/StageMobs.java +++ b/core/src/main/java/fr/skytasul/quests/stages/StageMobs.java @@ -1,17 +1,24 @@ package fr.skytasul.quests.stages; +import java.util.AbstractMap; +import java.util.Comparator; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; - +import java.util.Objects; +import java.util.Spliterator; +import java.util.Spliterators; import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.entity.Entity; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.entity.EntityDamageEvent.DamageCause; - import fr.skytasul.quests.api.mobs.Mob; -import fr.skytasul.quests.api.stages.AbstractCountableStage; import fr.skytasul.quests.api.stages.StageCreation; +import fr.skytasul.quests.api.stages.types.AbstractCountableStage; +import fr.skytasul.quests.api.stages.types.Locatable; +import fr.skytasul.quests.api.stages.types.Locatable.LocatableType; +import fr.skytasul.quests.api.stages.types.Locatable.LocatedType; import fr.skytasul.quests.gui.Inventories; import fr.skytasul.quests.gui.ItemUtils; import fr.skytasul.quests.gui.creation.stages.Line; @@ -24,7 +31,8 @@ import fr.skytasul.quests.utils.XMaterial; import fr.skytasul.quests.utils.compatibility.mobs.CompatMobDeathEvent; -public class StageMobs extends AbstractCountableStage> { +@LocatableType (types = LocatedType.ENTITY) +public class StageMobs extends AbstractCountableStage> implements Locatable.MultipleLocatable { private boolean shoot = false; @@ -42,18 +50,28 @@ public void setShoot(boolean shoot) { @EventHandler public void onMobKilled(CompatMobDeathEvent e){ - if (shoot && e.getBukkitEntity().getLastDamageCause().getCause() != DamageCause.PROJECTILE) return; + if (shoot && e.getBukkitEntity() != null && e.getBukkitEntity().getLastDamageCause().getCause() != DamageCause.PROJECTILE) return; Player p = e.getKiller(); if (p == e.getBukkitEntity()) return; // player suicidal PlayerAccount acc = PlayersManager.getPlayerAccount(p); if (branch.hasStageLaunched(acc, this)){ - event(acc, p, e.getPluginMob(), 1); + event(acc, p, new KilledMob(e.getPluginMob(), e.getBukkitEntity()), e.getAmount()); } } @Override protected boolean objectApplies(Mob object, Object other) { - return object.applies(other); + KilledMob otherMob = (KilledMob) other; + + if (!object.applies(otherMob.pluginMob)) + return false; + + if (object.getMinLevel() != null) { + if (object.getLevel(otherMob.bukkitEntity) < object.getMinLevel()) + return false; + } + + return true; } @Override @@ -65,7 +83,7 @@ public String descriptionLine(PlayerAccount acc, Source source){ public void start(PlayerAccount acc) { super.start(acc); if (acc.isCurrent() && sendStartMessage()) { - Lang.STAGE_MOBSLIST.send(acc.getPlayer(), super.descriptionFormat(acc, Source.FORCELINE)); + Lang.STAGE_MOBSLIST.send(acc.getPlayer(), (Object[]) super.descriptionFormat(acc, Source.FORCELINE)); } } @@ -95,6 +113,29 @@ protected void serialize(ConfigurationSection section) { if (shoot) section.set("shoot", true); } + @Override + public boolean canBeFetchedAsynchronously() { + return false; + } + + @Override + public Spliterator getNearbyLocated(NearbyFetcher fetcher) { + if (!fetcher.isTargeting(LocatedType.ENTITY)) return Spliterators.emptySpliterator(); + return fetcher.getCenter().getWorld() + .getEntities() + .stream() + .filter(entity -> objects.values().stream().anyMatch(entry -> entry.getKey().appliesEntity(entity))) + .map(x -> { + double ds = x.getLocation().distanceSquared(fetcher.getCenter()); + if (ds > fetcher.getMaxDistanceSquared()) return null; + return new AbstractMap.SimpleEntry<>(x, ds); + }) + .filter(Objects::nonNull) + .sorted(Comparator.comparing(Entry::getValue)) + .map(entry -> Located.LocatedEntity.create(entry.getKey())) + .spliterator(); + } + public static StageMobs deserialize(ConfigurationSection section, QuestBranch branch) { StageMobs stage = new StageMobs(branch, new HashMap<>()); stage.deserialize(section); @@ -124,7 +165,7 @@ public Creator(Line line, boolean ending) { public void setMobs(Map, Integer>> mobs) { this.mobs = mobs; - line.editItem(7, ItemUtils.lore(line.getItem(7), Lang.optionValue.format(mobs.size() + " mob(s)"))); + line.editItem(7, ItemUtils.lore(line.getItem(7), Lang.optionValue.format(Lang.AmountMobs.format(mobs.size())))); } public void setShoot(boolean shoot) { @@ -159,4 +200,14 @@ public void edit(StageMobs stage) { } } + private class KilledMob { + final Object pluginMob; + final Entity bukkitEntity; + + KilledMob(Object pluginMob, Entity bukkitEntity) { + this.pluginMob = pluginMob; + this.bukkitEntity = bukkitEntity; + } + } + } diff --git a/core/src/main/java/fr/skytasul/quests/stages/StageNPC.java b/core/src/main/java/fr/skytasul/quests/stages/StageNPC.java index b5ceef4e..8c525703 100644 --- a/core/src/main/java/fr/skytasul/quests/stages/StageNPC.java +++ b/core/src/main/java/fr/skytasul/quests/stages/StageNPC.java @@ -22,9 +22,11 @@ import fr.skytasul.quests.api.npcs.BQNPC; import fr.skytasul.quests.api.options.QuestOption; import fr.skytasul.quests.api.stages.AbstractStage; -import fr.skytasul.quests.api.stages.Dialogable; -import fr.skytasul.quests.api.stages.Locatable; import fr.skytasul.quests.api.stages.StageCreation; +import fr.skytasul.quests.api.stages.types.Dialogable; +import fr.skytasul.quests.api.stages.types.Locatable; +import fr.skytasul.quests.api.stages.types.Locatable.LocatableType; +import fr.skytasul.quests.api.stages.types.Locatable.LocatedType; import fr.skytasul.quests.editors.DialogEditor; import fr.skytasul.quests.gui.ItemUtils; import fr.skytasul.quests.gui.creation.stages.Line; @@ -39,7 +41,8 @@ import fr.skytasul.quests.utils.types.Dialog; import fr.skytasul.quests.utils.types.DialogRunner; -public class StageNPC extends AbstractStage implements Locatable, Dialogable { +@LocatableType (types = LocatedType.ENTITY) +public class StageNPC extends AbstractStage implements Locatable.PreciseLocatable, Dialogable { private BQNPC npc; private int npcID; @@ -81,7 +84,7 @@ public void run() { if (QuestsConfiguration.showTalkParticles()) { if (tmp.isEmpty()) return; - QuestsConfiguration.getParticleTalk().send((LivingEntity) en, tmp); + QuestsConfiguration.getParticleTalk().send(en, tmp); } } }.runTaskTimer(BeautyQuests.getInstance(), 20L, 6L); @@ -114,7 +117,7 @@ public void setNPC(int npcID) { this.npcID = npcID; if (npcID >= 0) this.npc = QuestsAPI.getNPCsManager().getById(npcID); if (npc == null) { - BeautyQuests.logger.warning("The NPC " + npcID + " does not exist for " + debugName()); + BeautyQuests.logger.warning("The NPC " + npcID + " does not exist for " + toString()); }else { initDialogRunner(); } @@ -124,11 +127,6 @@ public void setDialog(Dialog dialog){ this.dialog = dialog; } - @Override - public boolean hasDialog(){ - return dialog != null && !dialog.messages.isEmpty(); - } - @Override public Dialog getDialog(){ return dialog; @@ -148,15 +146,15 @@ public void setHid(boolean hide){ } @Override - public Location getLocation() { - return npc != null && npc.isSpawned() ? npc.getLocation() : null; - } - - @Override - public boolean isShown() { + public boolean isShown(Player player) { return !hide; } + @Override + public Located getLocated() { + return npc; + } + @Override public String descriptionLine(PlayerAccount acc, Source source){ return Utils.format(Lang.SCOREBOARD_NPC.toString(), descriptionFormat(acc, source)); @@ -181,7 +179,7 @@ protected void initDialogRunner() { public void onClick(BQNPCClickEvent e) { if (e.isCancelled()) return; if (e.getNPC() != npc) return; - if (!QuestsConfiguration.getNPCClick().applies(e.getClick())) return; + if (!QuestsConfiguration.getNPCClicks().contains(e.getClick())) return; Player p = e.getPlayer(); e.setCancelled(dialogRunner.onClick(p).shouldCancel()); @@ -267,7 +265,7 @@ protected void loadDatas(ConfigurationSection section) { if (section.contains("msg")) setDialog(Dialog.deserialize(section.getConfigurationSection("msg"))); if (section.contains("npcID")) { setNPC(section.getInt("npcID")); - }else BeautyQuests.logger.warning("No NPC specified for " + debugName()); + }else BeautyQuests.logger.warning("No NPC specified for " + toString()); if (section.contains("hid")) hide = section.getBoolean("hid"); } diff --git a/core/src/main/java/fr/skytasul/quests/stages/StagePlaceBlocks.java b/core/src/main/java/fr/skytasul/quests/stages/StagePlaceBlocks.java index ed2e9189..c47130d3 100644 --- a/core/src/main/java/fr/skytasul/quests/stages/StagePlaceBlocks.java +++ b/core/src/main/java/fr/skytasul/quests/stages/StagePlaceBlocks.java @@ -4,7 +4,6 @@ import java.util.Map; import java.util.Map.Entry; -import org.bukkit.block.Block; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; @@ -12,23 +11,18 @@ import org.bukkit.event.block.BlockPlaceEvent; import org.bukkit.inventory.ItemStack; -import fr.skytasul.quests.api.stages.AbstractCountableStage; -import fr.skytasul.quests.api.stages.StageCreation; -import fr.skytasul.quests.gui.Inventories; +import fr.skytasul.quests.api.stages.types.AbstractCountableBlockStage; import fr.skytasul.quests.gui.ItemUtils; -import fr.skytasul.quests.gui.blocks.BlocksGUI; import fr.skytasul.quests.gui.creation.stages.Line; -import fr.skytasul.quests.gui.creation.stages.StageRunnable; import fr.skytasul.quests.players.PlayerAccount; import fr.skytasul.quests.players.PlayersManager; import fr.skytasul.quests.structure.QuestBranch; import fr.skytasul.quests.structure.QuestBranch.Source; import fr.skytasul.quests.utils.Lang; -import fr.skytasul.quests.utils.MinecraftNames; import fr.skytasul.quests.utils.XMaterial; import fr.skytasul.quests.utils.types.BQBlock; -public class StagePlaceBlocks extends AbstractCountableStage { +public class StagePlaceBlocks extends AbstractCountableBlockStage { public StagePlaceBlocks(QuestBranch branch, Map> blocks) { super(branch, blocks); @@ -40,8 +34,8 @@ public String descriptionLine(PlayerAccount acc, Source source){ } @EventHandler (priority = EventPriority.MONITOR) - public void onMine(BlockPlaceEvent e) { - if (e.isCancelled() || e.getPlayer() == null) return; + public void onPlace(BlockPlaceEvent e) { + if (e.isCancelled()) return; Player p = e.getPlayer(); PlayerAccount acc = PlayersManager.getPlayerAccount(p); if (branch.hasStageLaunched(acc, this)){ @@ -49,74 +43,23 @@ public void onMine(BlockPlaceEvent e) { } } - @Override - protected boolean objectApplies(BQBlock object, Object other) { - if (other instanceof Block) return object.applies((Block) other); - return super.objectApplies(object, other); - } - - @Override - protected String getName(BQBlock object) { - return MinecraftNames.getMaterialName(object.getMaterial()); - } - - @Override - protected Object serialize(BQBlock object) { - return object.getAsString(); - } - - @Override - protected BQBlock deserialize(Object object) { - return BQBlock.fromString((String) object); - } - public static StagePlaceBlocks deserialize(ConfigurationSection section, QuestBranch branch) { StagePlaceBlocks stage = new StagePlaceBlocks(branch, new HashMap<>()); stage.deserialize(section); return stage; } - public static class Creator extends StageCreation { - - private Map> blocks; + public static class Creator extends AbstractCountableBlockStage.AbstractCreator { public Creator(Line line, boolean ending) { super(line, ending); - - line.setItem(7, ItemUtils.item(XMaterial.STONE, Lang.editBlocksPlace.toString()), new StageRunnable() { - @Override - public void run(Player p, ItemStack item) { - BlocksGUI blocksGUI = Inventories.create(p, new BlocksGUI()); - blocksGUI.setBlocksFromMap(blocks); - blocksGUI.run = (obj) -> { - setBlocks(obj); - reopenGUI(p, true); - }; - } - }); - } - - public void setBlocks(Map> blocks) { - this.blocks = blocks; - line.editItem(7, ItemUtils.lore(line.getItem(7), Lang.optionValue.format(blocks.size() + " blocks"))); - } - - @Override - public void start(Player p) { - super.start(p); - BlocksGUI blocks = Inventories.create(p, new BlocksGUI()); - blocks.run = (obj) -> { - setBlocks(obj); - reopenGUI(p, true); - }; } @Override - public void edit(StagePlaceBlocks stage) { - super.edit(stage); - setBlocks(stage.cloneObjects()); + protected ItemStack getBlocksItem() { + return ItemUtils.item(XMaterial.STONE, Lang.editBlocksPlace.toString()); } - + @Override public StagePlaceBlocks finishStage(QuestBranch branch) { return new StagePlaceBlocks(branch, blocks); diff --git a/core/src/main/java/fr/skytasul/quests/stages/StagePlayTime.java b/core/src/main/java/fr/skytasul/quests/stages/StagePlayTime.java index 02bc4cbe..df518d9f 100644 --- a/core/src/main/java/fr/skytasul/quests/stages/StagePlayTime.java +++ b/core/src/main/java/fr/skytasul/quests/stages/StagePlayTime.java @@ -57,7 +57,7 @@ private long getRemaining(PlayerAccount acc) { } private void launchTask(PlayerAccount acc, Player p, long remaining) { - tasks.put(acc, Bukkit.getScheduler().runTaskLater(BeautyQuests.getInstance(), () -> finishStage(p), remaining < 0 ? 0 : remaining)); + tasks.put(acc, Bukkit.getScheduler().runTaskLater(BeautyQuests.getInstance(), () -> branch.finishStage(p, this), remaining < 0 ? 0 : remaining)); } @Override @@ -75,7 +75,7 @@ public void leaves(PlayerAccount acc, Player p) { task.cancel(); updateObjective(acc, null, "remainingTime", getRemaining(acc)); }else { - BeautyQuests.logger.warning("Unavailable task in \"Play Time\" stage " + debugName() + " for player " + acc.getName()); + BeautyQuests.logger.warning("Unavailable task in \"Play Time\" stage " + toString() + " for player " + acc.getName()); } } diff --git a/core/src/main/java/fr/skytasul/quests/stages/StageTame.java b/core/src/main/java/fr/skytasul/quests/stages/StageTame.java index a7e0c341..1dcc890a 100644 --- a/core/src/main/java/fr/skytasul/quests/stages/StageTame.java +++ b/core/src/main/java/fr/skytasul/quests/stages/StageTame.java @@ -7,7 +7,7 @@ import org.bukkit.event.EventHandler; import org.bukkit.event.entity.EntityTameEvent; -import fr.skytasul.quests.api.stages.AbstractEntityStage; +import fr.skytasul.quests.api.stages.types.AbstractEntityStage; import fr.skytasul.quests.gui.creation.stages.Line; import fr.skytasul.quests.players.PlayerAccount; import fr.skytasul.quests.structure.QuestBranch; diff --git a/core/src/main/java/fr/skytasul/quests/structure/BranchesManager.java b/core/src/main/java/fr/skytasul/quests/structure/BranchesManager.java index a0b7d652..b65fb97e 100644 --- a/core/src/main/java/fr/skytasul/quests/structure/BranchesManager.java +++ b/core/src/main/java/fr/skytasul/quests/structure/BranchesManager.java @@ -15,6 +15,7 @@ import fr.skytasul.quests.BeautyQuests; import fr.skytasul.quests.api.QuestsAPI; import fr.skytasul.quests.players.PlayerAccount; +import fr.skytasul.quests.players.PlayerQuestDatas; public class BranchesManager{ @@ -76,7 +77,9 @@ public final void objectiveUpdated(Player p, PlayerAccount acc) { } public void startPlayer(PlayerAccount acc){ - acc.getQuestDatas(getQuest()).resetQuestFlow(); + PlayerQuestDatas datas = acc.getQuestDatas(getQuest()); + datas.resetQuestFlow(); + datas.setStartingTime(System.currentTimeMillis()); branches.get(0).start(acc); } diff --git a/core/src/main/java/fr/skytasul/quests/structure/Quest.java b/core/src/main/java/fr/skytasul/quests/structure/Quest.java index 6302371f..76c7cd0a 100644 --- a/core/src/main/java/fr/skytasul/quests/structure/Quest.java +++ b/core/src/main/java/fr/skytasul/quests/structure/Quest.java @@ -3,17 +3,19 @@ import java.io.File; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; +import java.util.Arrays; import java.util.Calendar; import java.util.Iterator; import java.util.List; - +import java.util.OptionalInt; +import java.util.regex.Pattern; import org.bukkit.Bukkit; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; - import fr.skytasul.quests.BeautyQuests; import fr.skytasul.quests.QuestsConfiguration; import fr.skytasul.quests.api.QuestsAPI; @@ -25,11 +27,15 @@ import fr.skytasul.quests.api.options.OptionSet; import fr.skytasul.quests.api.options.QuestOption; import fr.skytasul.quests.api.options.QuestOptionCreator; +import fr.skytasul.quests.api.options.description.QuestDescriptionContext; +import fr.skytasul.quests.api.options.description.QuestDescriptionProvider; import fr.skytasul.quests.api.requirements.AbstractRequirement; import fr.skytasul.quests.api.requirements.Actionnable; import fr.skytasul.quests.gui.Inventories; import fr.skytasul.quests.gui.misc.ConfirmGUI; +import fr.skytasul.quests.gui.quests.PlayerListGUI.Category; import fr.skytasul.quests.options.*; +import fr.skytasul.quests.options.OptionVisibility.VisibilityLocation; import fr.skytasul.quests.players.AdminMode; import fr.skytasul.quests.players.PlayerAccount; import fr.skytasul.quests.players.PlayerQuestDatas; @@ -40,13 +46,16 @@ import fr.skytasul.quests.utils.Lang; import fr.skytasul.quests.utils.Utils; -public class Quest implements Comparable, OptionSet { +public class Quest implements Comparable, OptionSet, QuestDescriptionProvider { + private static final Pattern PERMISSION_PATTERN = Pattern.compile("^beautyquests\\.start\\.(\\d+)$"); + private final int id; private final File file; private BranchesManager manager; private List> options = new ArrayList<>(); + private List descriptions = new ArrayList<>(); private boolean removed = false; public boolean asyncEnd = false; @@ -60,15 +69,20 @@ public Quest(int id, File file) { this.id = id; this.file = file; this.manager = new BranchesManager(this); + this.descriptions.add(this); } public void load() { QuestsAPI.propagateQuestsHandlers(handler -> handler.questLoaded(this)); } + public List getDescriptions() { + return descriptions; + } + @Override public Iterator iterator() { - return (Iterator) options; + return (Iterator) options.iterator(); } public D getOptionValueOrDef(Class> clazz) { @@ -153,8 +167,8 @@ public boolean isRepeatable() { return getOptionValueOrDef(OptionRepeatable.class); } - public boolean isHidden() { - return getOptionValueOrDef(OptionHide.class); + public boolean isHidden(VisibilityLocation location) { + return !getOptionValueOrDef(OptionVisibility.class).contains(location); } public boolean isHiddenWhenRequirementsNotMet() { @@ -184,13 +198,18 @@ public boolean hasFinished(PlayerAccount acc){ return acc.hasQuestDatas(this) && acc.getQuestDatas(this).isFinished(); } - public void cancelPlayer(PlayerAccount acc){ + public boolean cancelPlayer(PlayerAccount acc) { + PlayerQuestDatas datas = acc.getQuestDatasIfPresent(this); + if (datas == null || !datas.hasStarted()) + return false; + manager.remove(acc); QuestsAPI.propagateQuestsHandlers(handler -> handler.questReset(acc, this)); Bukkit.getPluginManager().callEvent(new PlayerQuestResetEvent(acc, this)); if (acc.isCurrent()) Utils.giveRewards(acc.getPlayer(), getOptionValueOrDef(OptionCancelRewards.class)); + return true; } public boolean resetPlayer(PlayerAccount acc){ @@ -218,12 +237,7 @@ public boolean isLauncheable(Player p, PlayerAccount acc, boolean sendMessage) { public boolean testRequirements(Player p, PlayerAccount acc, boolean sendMessage){ if (!p.hasPermission("beautyquests.start")) return false; - if (QuestsConfiguration.getMaxLaunchedQuests() != 0 && Boolean.FALSE.equals(getOptionValueOrDef(OptionBypassLimit.class))) { - if (QuestsAPI.getQuests().getStartedSize(acc) >= QuestsConfiguration.getMaxLaunchedQuests()) { - if (sendMessage) Lang.QUESTS_MAX_LAUNCHED.send(p, QuestsConfiguration.getMaxLaunchedQuests()); - return false; - } - } + if (!testQuestLimit(p, acc, sendMessage)) return false; sendMessage = sendMessage && (!hasOption(OptionStarterNPC.class) || (QuestsConfiguration.isRequirementReasonSentOnMultipleQuests() || getOption(OptionStarterNPC.class).getValue().getQuests().size() == 1)); for (AbstractRequirement ar : getOptionValueOrDef(OptionRequirements.class)) { if (!ar.test(p)) { @@ -234,6 +248,30 @@ public boolean testRequirements(Player p, PlayerAccount acc, boolean sendMessage return true; } + public boolean testQuestLimit(Player p, PlayerAccount acc, boolean sendMessage) { + if (Boolean.FALSE.equals(getOptionValueOrDef(OptionBypassLimit.class))) + return true; + int playerMaxLaunchedQuest; + OptionalInt playerMaxLaunchedQuestOpt = p.getEffectivePermissions().stream() + .filter(permission -> permission.getValue()) // all "active" permissions + .map(permission -> PERMISSION_PATTERN.matcher(permission.getPermission())) + .filter(matcher -> matcher.matches()) // all permissions that matches "beautyquests.start." + .mapToInt(matcher -> Integer.parseInt(matcher.group(1))) // get the effective number + .max(); + if (playerMaxLaunchedQuestOpt.isPresent()) { + playerMaxLaunchedQuest = playerMaxLaunchedQuestOpt.getAsInt(); + }else { + if (QuestsConfiguration.getMaxLaunchedQuests() == 0) return true; + playerMaxLaunchedQuest = QuestsConfiguration.getMaxLaunchedQuests(); + } + if (QuestsAPI.getQuests().getStartedSize(acc) >= playerMaxLaunchedQuest) { + if (sendMessage) + Lang.QUESTS_MAX_LAUNCHED.send(p, playerMaxLaunchedQuest); + return false; + } + return true; + } + public boolean testTimer(PlayerAccount acc, boolean sendMessage) { if (isRepeatable() && acc.hasQuestDatas(this)) { long time = acc.getQuestDatas(this).getTimer(); @@ -271,7 +309,25 @@ public String getDescriptionLine(PlayerAccount acc, Source source) { return branch.getDescriptionLine(acc, source); } + @Override + public List provideDescription(QuestDescriptionContext context) { + if (!context.getPlayerAccount().isCurrent()) return null; + if (context.getCategory() != Category.IN_PROGRESS) return null; + return Arrays.asList(getDescriptionLine(context.getPlayerAccount(), context.getSource())); + } + + @Override + public String getDescriptionId() { + return "advancement"; + } + + @Override + public double getDescriptionPriority() { + return 15; + } + public void attemptStart(Player p, Runnable atStart) { + if (!isLauncheable(p, PlayersManager.getPlayerAccount(p), true)) return; String confirm; if (QuestsConfiguration.questConfirmGUI() && !"none".equals(confirm = getOptionValueOrDef(OptionConfirmMessage.class))) { new ConfirmGUI(() -> { @@ -344,6 +400,7 @@ public void finish(Player p){ manager.remove(acc); questDatas.setBranch(-1); questDatas.incrementFinished(); + questDatas.setStartingTime(0); if (hasOption(OptionQuestPool.class)) getOptionValueOrDef(OptionQuestPool.class).questCompleted(acc, Quest.this); if (isRepeatable()) { Calendar cal = Calendar.getInstance(); @@ -397,7 +454,6 @@ public String toString(){ } public boolean saveToFile() throws Exception { - if (!file.exists()) file.createNewFile(); YamlConfiguration fc = new YamlConfiguration(); BeautyQuests.savingFailure = false; @@ -406,14 +462,19 @@ public boolean saveToFile() throws Exception { BeautyQuests.logger.warning("An error occurred while saving quest " + id); return false; } + + Path path = file.toPath(); + if (!Files.exists(path)) + Files.createFile(path); + String questData = fc.saveToString(); - String oldQuestDatas = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); + String oldQuestDatas = new String(Files.readAllBytes(path), StandardCharsets.UTF_8); if (questData.equals(oldQuestDatas)) { DebugUtils.logMessage("Quest " + id + " was up-to-date."); return false; }else { - DebugUtils.logMessage("Saving quest " + id + " into " + file.getPath()); - Files.write(file.toPath(), questData.getBytes(StandardCharsets.UTF_8)); + DebugUtils.logMessage("Saving quest " + id + " into " + path.toString()); + Files.write(path, questData.getBytes(StandardCharsets.UTF_8)); return true; } } diff --git a/core/src/main/java/fr/skytasul/quests/structure/QuestBranch.java b/core/src/main/java/fr/skytasul/quests/structure/QuestBranch.java index e4aab983..c84d6541 100644 --- a/core/src/main/java/fr/skytasul/quests/structure/QuestBranch.java +++ b/core/src/main/java/fr/skytasul/quests/structure/QuestBranch.java @@ -157,11 +157,11 @@ public void start(PlayerAccount acc){ } public void finishStage(Player p, AbstractStage stage){ - DebugUtils.logMessage("Next stage for player " + p.getName() + ", via " + DebugUtils.stackTraces(2, 4)); + DebugUtils.logMessage("Next stage for player " + p.getName() + " (coming from " + stage.toString() + ") via " + DebugUtils.stackTraces(1, 3)); PlayerAccount acc = PlayersManager.getPlayerAccount(p); PlayerQuestDatas datas = acc.getQuestDatas(getQuest()); if (datas.getBranch() != getID() || (datas.isInEndingStages() && isRegularStage(stage)) || (!datas.isInEndingStages() && datas.getStage() != stage.getID())) { - BeautyQuests.logger.warning("Trying to finish stage " + stage.debugName() + " for player " + p.getName() + ", but the player didn't have started it."); + BeautyQuests.logger.warning("Trying to finish stage " + stage.toString() + " for player " + p.getName() + ", but the player didn't have started it."); return; } AdminMode.broadcast("Player " + p.getName() + " has finished the stage " + getID(stage) + " of quest " + getQuest().getID()); diff --git a/core/src/main/java/fr/skytasul/quests/structure/QuestDescription.java b/core/src/main/java/fr/skytasul/quests/structure/QuestDescription.java deleted file mode 100644 index 7b782a5c..00000000 --- a/core/src/main/java/fr/skytasul/quests/structure/QuestDescription.java +++ /dev/null @@ -1,81 +0,0 @@ -package fr.skytasul.quests.structure; - -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -import org.bukkit.configuration.ConfigurationSection; -import org.bukkit.entity.Player; - -import fr.skytasul.quests.options.OptionEndRewards; -import fr.skytasul.quests.options.OptionRequirements; -import fr.skytasul.quests.utils.Lang; -import fr.skytasul.quests.utils.Utils; - -public class QuestDescription { - - private boolean requirements; - private String requirementsValid; - private String requirementsInvalid; - - private boolean rewards; - private String rewardsFormat; - - private Pattern splitPattern = Pattern.compile("\\{JOIN\\}"); - - public QuestDescription(ConfigurationSection config) { - requirements = config.getBoolean("requirements.display"); - requirementsValid = config.getString("requirements.valid"); - requirementsInvalid = config.getString("requirements.invalid"); - - rewards = config.getBoolean("rewards.display"); - rewardsFormat = config.getString("rewards.format"); - } - - public List formatDescription(Quest quest, Player p) { - List list = new ArrayList<>(); - - String desc = quest.getDescription(); - if (desc != null) list.add("§7" + desc); - - if (p == null) return list; - - if (this.rewards) { - List rewards = quest.getOptionValueOrDef(OptionEndRewards.class) - .stream() - .map(x -> x.getDescription(p)) - .filter(Objects::nonNull) - .flatMap(x -> splitPattern.splitAsStream(x)) - .filter(x -> !x.isEmpty()) - .map(x -> Utils.format(rewardsFormat, x)) - .collect(Collectors.toList()); - if (!rewards.isEmpty()) { - if (!list.isEmpty()) list.add(""); - list.add(Lang.RWDTitle.toString()); - list.addAll(rewards); - } - } - - if (this.requirements) { - List requirements = quest.getOptionValueOrDef(OptionRequirements.class) - .stream() - .map(x -> { - String description = x.getDescription(p); - if (description != null) description = Utils.format(x.test(p) ? requirementsValid : requirementsInvalid, description); - return description; - }) - .filter(Objects::nonNull) - .collect(Collectors.toList()); - if (!requirements.isEmpty()) { - if (!list.isEmpty()) list.add(""); - list.add(Lang.RDTitle.toString()); - list.addAll(requirements); - } - } - - return list; - } - -} diff --git a/core/src/main/java/fr/skytasul/quests/structure/QuestsManager.java b/core/src/main/java/fr/skytasul/quests/structure/QuestsManager.java index 3440b51e..e3a6f438 100644 --- a/core/src/main/java/fr/skytasul/quests/structure/QuestsManager.java +++ b/core/src/main/java/fr/skytasul/quests/structure/QuestsManager.java @@ -17,6 +17,7 @@ import fr.skytasul.quests.api.npcs.BQNPC; import fr.skytasul.quests.options.OptionStartable; import fr.skytasul.quests.options.OptionStarterNPC; +import fr.skytasul.quests.options.OptionVisibility.VisibilityLocation; import fr.skytasul.quests.players.PlayerAccount; import fr.skytasul.quests.players.PlayerQuestDatas; import fr.skytasul.quests.utils.Utils; @@ -122,26 +123,30 @@ public void addQuest(Quest quest) { } public List getQuestsStarted(PlayerAccount acc) { - return getQuestsStarted(acc, false); + return getQuestsStarted(acc, false, false); } - public List getQuestsStarted(PlayerAccount acc, boolean withoutScoreboard) { + public List getQuestsStarted(PlayerAccount acc, boolean hide, boolean withoutScoreboard) { return acc.getQuestsDatas() .stream() .filter(PlayerQuestDatas::hasStarted) .map(PlayerQuestDatas::getQuest) .filter(Objects::nonNull) + .filter(quest -> !quest.isRemoved()) + .filter(quest -> !hide || !quest.isHidden(VisibilityLocation.TAB_IN_PROGRESS)) .filter(quest -> !withoutScoreboard || quest.isScoreboardEnabled()) .collect(Collectors.toList()); } public void updateQuestsStarted(PlayerAccount acc, boolean withoutScoreboard, List list) { + for (Iterator iterator = list.iterator(); iterator.hasNext();) { + Quest existing = iterator.next(); + if (!existing.hasStarted(acc) || (withoutScoreboard && !existing.isScoreboardEnabled())) iterator.remove(); + } + for (Quest qu : quests) { if (withoutScoreboard && !qu.isScoreboardEnabled()) continue; - boolean contains = list.contains(qu); - if (qu.hasStarted(acc)) { - if (!list.contains(qu)) list.add(qu); - }else if (contains) list.remove(qu); + if (!list.contains(qu) && qu.hasStarted(acc)) list.add(qu); } } @@ -155,7 +160,7 @@ public int getStartedSize(PlayerAccount acc) { public List getQuestsFinished(PlayerAccount acc, boolean hide) { return quests .stream() - .filter(quest -> !(hide && quest.isHidden()) && quest.hasFinished(acc)) + .filter(quest -> !(hide && quest.isHidden(VisibilityLocation.TAB_FINISHED)) && quest.hasFinished(acc)) .collect(Collectors.toList()); } @@ -163,7 +168,7 @@ public List getQuestsNotStarted(PlayerAccount acc, boolean hide, boolean return quests .stream() .filter(quest -> { - if (hide && quest.isHidden()) return false; + if (hide && quest.isHidden(VisibilityLocation.TAB_NOT_STARTED)) return false; if (quest.hasStarted(acc)) return false; if (!quest.hasFinished(acc)) return true; return clickableAndRedoable && quest.isRepeatable() && quest.getOptionValueOrDef(OptionStartable.class) && quest.testTimer(acc, false); diff --git a/core/src/main/java/fr/skytasul/quests/structure/pools/QuestPool.java b/core/src/main/java/fr/skytasul/quests/structure/pools/QuestPool.java index 8d021d4c..21da45ca 100644 --- a/core/src/main/java/fr/skytasul/quests/structure/pools/QuestPool.java +++ b/core/src/main/java/fr/skytasul/quests/structure/pools/QuestPool.java @@ -13,8 +13,8 @@ import fr.skytasul.quests.BeautyQuests; import fr.skytasul.quests.api.QuestsAPI; import fr.skytasul.quests.api.npcs.BQNPC; -import fr.skytasul.quests.api.objects.QuestObject; import fr.skytasul.quests.api.requirements.AbstractRequirement; +import fr.skytasul.quests.api.serializable.SerializableObject; import fr.skytasul.quests.gui.ItemUtils; import fr.skytasul.quests.players.PlayerAccount; import fr.skytasul.quests.players.PlayerPoolDatas; @@ -225,7 +225,8 @@ public String give(Player p) { }); } } - return "started quest(s) #" + started.stream().map(x -> Integer.toString(x.getID())).collect(Collectors.joining(", ")); + //return "started quest(s) #" + started.stream().map(x -> Integer.toString(x.getID())).collect(Collectors.joining(", ")); + return null; } List replenishQuests(PlayerPoolDatas datas) { @@ -260,11 +261,11 @@ public void save(ConfigurationSection config) { config.set("timeDiff", timeDiff); config.set("npcID", npcID); config.set("avoidDuplicates", avoidDuplicates); - if (!requirements.isEmpty()) config.set("requirements", Utils.serializeList(requirements, AbstractRequirement::serialize)); + if (!requirements.isEmpty()) config.set("requirements", SerializableObject.serializeList(requirements)); } public static QuestPool deserialize(int id, ConfigurationSection config) { - List requirements = QuestObject.deserializeList(config.getMapList("requirements"), AbstractRequirement::deserialize); + List requirements = SerializableObject.deserializeList(config.getMapList("requirements"), AbstractRequirement::deserialize); return new QuestPool(id, config.getInt("npcID"), config.getString("hologram"), config.getInt("maxQuests"), config.getInt("questsPerLaunch", 1), config.getBoolean("redoAllowed"), config.getLong("timeDiff"), config.getBoolean("avoidDuplicates", true), requirements); } diff --git a/core/src/main/java/fr/skytasul/quests/utils/ComparisonMethod.java b/core/src/main/java/fr/skytasul/quests/utils/ComparisonMethod.java index ea5bcb32..7239a586 100644 --- a/core/src/main/java/fr/skytasul/quests/utils/ComparisonMethod.java +++ b/core/src/main/java/fr/skytasul/quests/utils/ComparisonMethod.java @@ -3,15 +3,26 @@ import fr.skytasul.quests.editors.checkers.EnumParser; public enum ComparisonMethod { - EQUALS(Lang.ComparisonEquals), DIFFERENT(Lang.ComparisonDifferent), LESS(Lang.ComparisonLess), LESS_OR_EQUAL(Lang.ComparisonLessOrEquals), GREATER(Lang.ComparisonGreater), GREATER_OR_EQUAL(Lang.ComparisonGreaterOrEquals); + EQUALS('=', Lang.ComparisonEquals), + DIFFERENT('≠', Lang.ComparisonDifferent), + LESS('<', Lang.ComparisonLess), + LESS_OR_EQUAL('≤', Lang.ComparisonLessOrEquals), + GREATER('>', Lang.ComparisonGreater), + GREATER_OR_EQUAL('≥', Lang.ComparisonGreaterOrEquals); + private char symbol; private Lang title; private static final EnumParser COMPARISON_PARSER = new EnumParser<>(ComparisonMethod.class); - private ComparisonMethod(Lang title) { + private ComparisonMethod(char symbol, Lang title) { + this.symbol = symbol; this.title = title; } + + public char getSymbol() { + return symbol; + } public Lang getTitle() { return title; diff --git a/core/src/main/java/fr/skytasul/quests/utils/CustomizedObjectTypeAdapter.java b/core/src/main/java/fr/skytasul/quests/utils/CustomizedObjectTypeAdapter.java index 13960119..5f8b0c04 100644 --- a/core/src/main/java/fr/skytasul/quests/utils/CustomizedObjectTypeAdapter.java +++ b/core/src/main/java/fr/skytasul/quests/utils/CustomizedObjectTypeAdapter.java @@ -15,10 +15,22 @@ public class CustomizedObjectTypeAdapter extends TypeAdapter { - private static final CustomizedObjectTypeAdapter adapter = new CustomizedObjectTypeAdapter(); - public static final GsonBuilder GSON_BUILDER = new GsonBuilder().registerTypeAdapter(Map.class, adapter).registerTypeAdapter(List.class, adapter); - public static final Gson GSON = GSON_BUILDER.create(); + public static final CustomizedObjectTypeAdapter CUSTOM_ADAPTER = new CustomizedObjectTypeAdapter(); + public static final Gson GSON = new GsonBuilder() + .registerTypeAdapter(Map.class, CUSTOM_ADAPTER) + .registerTypeAdapter(List.class, CUSTOM_ADAPTER) + .create(); + public static T deserializeNullable(String json, Class classOfT) { + if (json == null) return null; + return GSON.fromJson(json, classOfT); + } + + public static String serializeNullable(Object object) { + if (object == null) return null; + return GSON.toJson(object); + } + private final TypeAdapter delegate = new Gson().getAdapter(Object.class); @Override diff --git a/core/src/main/java/fr/skytasul/quests/utils/Database.java b/core/src/main/java/fr/skytasul/quests/utils/Database.java index a6fcef4f..aa29c1d3 100644 --- a/core/src/main/java/fr/skytasul/quests/utils/Database.java +++ b/core/src/main/java/fr/skytasul/quests/utils/Database.java @@ -8,61 +8,38 @@ import javax.sql.DataSource; import org.bukkit.configuration.ConfigurationSection; -import org.mariadb.jdbc.MariaDbPoolDataSource; -import com.mysql.cj.jdbc.MysqlDataSource; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; import fr.skytasul.quests.BeautyQuests; -public class Database { +public class Database implements Closeable { - private ConfigurationSection config; - private String databaseName; - - private DataSource source; + private final ConfigurationSection config; + private final String databaseName; + + private final DataSource source; - public Database(ConfigurationSection config) throws SQLException { + public Database(ConfigurationSection config) { this.config = config; this.databaseName = config.getString("database"); - try { - Class.forName("org.mariadb.jdbc.MariaDbPoolDataSource"); - MariaDbPoolDataSource msource = new MariaDbPoolDataSource(); - msource.setServerName(config.getString("host")); - msource.setPortNumber(config.getInt("port")); - msource.setDatabaseName(databaseName); - msource.setUser(config.getString("username")); - msource.setPassword(config.getString("password")); - - msource.setPoolName("beautyquests"); - msource.setMaxIdleTime(60); - msource.setLoginTimeout(20); - - source = msource; - }catch (ClassNotFoundException e) { - MysqlDataSource msource = new MysqlDataSource(); - msource.setServerName(config.getString("host")); - msource.setPortNumber(config.getInt("port")); - msource.setDatabaseName(databaseName); - msource.setUser(config.getString("username")); - msource.setPassword(config.getString("password")); - - msource.setConnectTimeout(20); - boolean ssl = config.getBoolean("ssl"); - msource.setVerifyServerCertificate(ssl); - msource.setUseSSL(ssl); - - source = msource; - } - DebugUtils.logMessage("Created SQL data source: " + source.getClass().getName()); - // Yes, I know there is literally the same code twice. - // Unfortunately, there is no common interface - // between MariaDB and MySQL pool data source - // which provides the configuration methods. + HikariConfig hikariConfig = new HikariConfig("/hikari.properties"); + hikariConfig.setJdbcUrl("jdbc:mysql://" + config.getString("host") + ":" + config.getInt("port") + "/" + databaseName); + hikariConfig.setUsername(config.getString("username")); + hikariConfig.setPassword(config.getString("password")); + hikariConfig.setPoolName("BeautyQuests-SQL-pool"); + hikariConfig.setConnectionTimeout(20_000); + + boolean ssl = config.getBoolean("ssl"); + hikariConfig.addDataSourceProperty("verifyServerCertificate", ssl); + hikariConfig.addDataSourceProperty("useSSL", ssl); + + source = new HikariDataSource(hikariConfig); } public void testConnection() throws SQLException { - DebugUtils.logMessage("Trying to connect to " + config.getString("host")); try (Connection connection = source.getConnection()) { if (!connection.isValid(0)) throw new SQLException("Could not establish database connection."); @@ -77,13 +54,13 @@ public ConfigurationSection getConfig() { return config; } - public void closeConnection() { - if (source instanceof Closeable) { - try { - ((Closeable) source).close(); - }catch (IOException e) { - e.printStackTrace(); - } + @Override + public void close() { + BeautyQuests.logger.info("Closing database pool..."); + try { + ((Closeable) source).close(); + }catch (IOException ex) { + BeautyQuests.logger.severe("An error occurred while closing database pool.", ex); } } diff --git a/core/src/main/java/fr/skytasul/quests/utils/DebugUtils.java b/core/src/main/java/fr/skytasul/quests/utils/DebugUtils.java index e23601f8..3ed9de98 100644 --- a/core/src/main/java/fr/skytasul/quests/utils/DebugUtils.java +++ b/core/src/main/java/fr/skytasul/quests/utils/DebugUtils.java @@ -12,7 +12,7 @@ private DebugUtils() {} private static Map errors = new HashMap<>(); public static void logMessage(String msg){ - BeautyQuests.getInstance().getLoggerHandler().write("[DEBUG]: " + msg); + BeautyQuests.getInstance().getLoggerHandler().write(msg, "DEBUG"); } public static String stackTraces(int from, int to){ diff --git a/core/src/main/java/fr/skytasul/quests/utils/Lang.java b/core/src/main/java/fr/skytasul/quests/utils/Lang.java index cd7dac3f..446bbddd 100644 --- a/core/src/main/java/fr/skytasul/quests/utils/Lang.java +++ b/core/src/main/java/fr/skytasul/quests/utils/Lang.java @@ -1,16 +1,11 @@ package fr.skytasul.quests.utils; -import java.util.function.Supplier; - -import org.bukkit.ChatColor; -import org.bukkit.command.CommandSender; -import org.bukkit.configuration.file.YamlConfiguration; - +import fr.skytasul.quests.api.Locale; /** * Stores all string paths and methods to format and send them to players. */ -public enum Lang{ +public enum Lang implements Locale { /* Formats (must be first to be used after) */ Prefix("misc.format.prefix"), @@ -20,6 +15,7 @@ public enum Lang{ EditorPrefix("misc.format.editorPrefix"), ErrorPrefix("misc.format.errorPrefix"), SuccessPrefix("misc.format.successPrefix"), + RequirementNotMetPrefix("misc.format.requirementNotMetPrefix"), /* Messages */ FINISHED_BASE("msg.quest.finished.base"), @@ -33,6 +29,7 @@ public enum Lang{ QUEST_INVALID("msg.quest.invalidID"), // 0: quest id POOL_INVALID("msg.quest.invalidPoolID"), // 0: pool id ALREADY_STARTED("msg.quest.alreadyStarted"), + QUEST_NOT_STARTED("msg.quest.notStarted"), QUESTS_MAX_LAUNCHED("msg.quests.maxLaunched"), // 0: max quests QUEST_NOSTEPS("msg.quests.nopStep"), @@ -100,6 +97,7 @@ public enum Lang{ NUMBER_NEGATIVE("msg.number.negative"), NUMBER_ZERO("msg.number.zero"), NUMBER_INVALID("msg.number.invalid"), + NUMBER_NOT_IN_BOUNDS("msg.number.notInBounds"), // 0: min, 1: max ERROR_OCCURED("msg.errorOccurred"), CANT_COMMAND("msg.commandsDisabled"), OUT_OF_BOUNDS("msg.indexOutOfBounds"), @@ -113,7 +111,7 @@ public enum Lang{ PLAYER_NOT_ONLINE("msg.playerNotOnline"), PLAYER_DATA_NOT_FOUND("msg.playerDataNotFound"), // 0: player name - VERSION_REQUIRED("msg.versionRequired"), // 0: version + VERSION_REQUIRED("msg.versionRequired", ErrorPrefix), // 0: version RESTART_SERVER("msg.restartServer"), @@ -211,6 +209,7 @@ public enum Lang{ BLOCK_TAGS("msg.editor.blockTag"), // 0: available block tags BUCKET_AMOUNT("msg.editor.typeBucketAmount"), + DAMAGE_AMOUNT("msg.editor.typeDamageAmount", EditorPrefix), LOCATION_GO("msg.editor.goToLocation"), LOCATION_RADIUS("msg.editor.typeLocationRadius"), @@ -218,7 +217,8 @@ public enum Lang{ GAME_TICKS("msg.editor.typeGameTicks"), - NO_SUCH_ELEMENT("msg.editor.noSuchElement"), // 0: available elements + AVAILABLE_ELEMENTS("msg.editor.availableElements"), // 0: available elements + NO_SUCH_ELEMENT("msg.editor.noSuchElement", EditorPrefix), // 0: available elements INVALID_PATTERN("msg.editor.invalidPattern"), // 0: pattern COMPARISON_TYPE("msg.editor.comparisonTypeDefault"), // 0: available comparisons, 1: default comparison @@ -230,11 +230,15 @@ public enum Lang{ POOL_QUESTS_PER_LAUNCH("msg.editor.pool.questsPerLaunch", EditorPrefix), POOL_TIME("msg.editor.pool.timeMsg", EditorPrefix), - TITLE_TITLE("msg.editor.title.title"), - TITLE_SUBTITLE("msg.editor.title.subtitle"), - TITLE_FADEIN("msg.editor.title.fadeIn"), - TITLE_STAY("msg.editor.title.stay"), - TITLE_FADEOUT("msg.editor.title.fadeOut"), + TITLE_TITLE("msg.editor.title.title", EditorPrefix), + TITLE_SUBTITLE("msg.editor.title.subtitle", EditorPrefix), + TITLE_FADEIN("msg.editor.title.fadeIn", EditorPrefix), + TITLE_STAY("msg.editor.title.stay", EditorPrefix), + TITLE_FADEOUT("msg.editor.title.fadeOut", EditorPrefix), + + COLOR_NAMED_EDITOR("msg.editor.colorNamed", EditorPrefix), + COLOR_EDITOR("msg.editor.color", EditorPrefix), + INVALID_COLOR("msg.editor.invalidColor", ErrorPrefix), FIREWORK_INVALID("msg.editor.firework.invalid", ErrorPrefix), FIREWORK_INVALID_HAND("msg.editor.firework.invalidHand", ErrorPrefix), @@ -306,7 +310,7 @@ public enum Lang{ MYTHICMOB_LIST("msg.editor.mythicmobs.list"), MYTHICMOB_NOT_EXISTS("msg.editor.mythicmobs.isntMythicMob"), MYTHICMOB_DISABLED("msg.editor.mythicmobs.disabled"), - EPICBOSS_NOT_EXISTS("msg.editor.epicBossDoesntExist"), + ADVANCED_SPAWNERS_MOB("msg.editor.advancedSpawnersMob", EditorPrefix), TEXTLIST_SYNTAX("msg.editor.textList.syntax"), TEXTLIST_TEXT_ADDED("msg.editor.textList.added"), @@ -355,6 +359,9 @@ public enum Lang{ stagePlayTime("inv.create.playTime"), stageBreedAnimals("inv.create.breedAnimals"), stageTameAnimals("inv.create.tameAnimals"), + stageDeath("inv.create.death"), + stageDealDamage("inv.create.dealDamage"), + stageEatDrink("inv.create.eatDrink"), stageText("inv.create.NPCText"), dialogLines("inv.create.dialogLines"), // 0: lines stageNPCSelect("inv.create.NPCSelect"), @@ -384,13 +391,23 @@ public enum Lang{ editItem("inv.create.editItem"), editBucketType("inv.create.editBucketType"), editBucketAmount("inv.create.editBucketAmount"), + changeTicksRequired("inv.create.changeTicksRequired"), + changeEntityType("inv.create.changeEntityType"), + stageLocationLocation("inv.create.editLocation"), stageLocationRadius("inv.create.editRadius"), stageLocationCurrentRadius("inv.create.currentRadius"), // 0: radius stageLocationWorldPattern("inv.create.stage.location.worldPattern"), stageLocationWorldPatternLore("inv.create.stage.location.worldPatternLore"), - changeTicksRequired("inv.create.changeTicksRequired"), - changeEntityType("inv.create.changeEntityType"), + + stageDeathCauses("inv.create.stage.death.causes"), + stageDeathCauseAny("inv.create.stage.death.anyCause"), + stageDeathCausesSet("inv.create.stage.death.setCauses"), // 0: causes amount + + stageDealDamageValue("inv.create.stage.dealDamage.damage"), + stageDealDamageMobs("inv.create.stage.dealDamage.targetMobs"), + + stageEatDrinkItems("inv.create.stage.eatDrink.items"), INVENTORY_STAGES("inv.stages.name"), nextPage("inv.stages.nextPage"), @@ -413,8 +430,6 @@ public enum Lang{ startableFromGUILore("inv.details.startableFromGUILore"), scoreboard("inv.details.scoreboardItem"), scoreboardLore("inv.details.scoreboardItemLore"), - hide("inv.details.hideItem"), - hideLore("inv.details.hideItemLore"), hideNoRequirements("inv.details.hideNoRequirementsItem"), hideNoRequirementsLore("inv.details.hideNoRequirementsItemLore"), bypass("inv.details.bypassLimit"), @@ -479,6 +494,8 @@ public enum Lang{ optionFirework("inv.details.firework"), optionFireworkLore("inv.details.fireworkLore"), optionFireworkDrop("inv.details.fireworkLoreDrop"), + optionVisibility("inv.details.visibility"), + optionVisibilityLore("inv.details.visibilityLore"), keepDatas("inv.details.keepDatas"), keepDatasLore("inv.details.keepDatasLore"), resetLore("inv.details.loreReset"), @@ -511,12 +528,14 @@ public enum Lang{ INVENTORY_MOBS("inv.mobs.name"), mobsNone("inv.mobs.none"), click("inv.mobs.clickLore"), + setLevel("inv.mobs.setLevel"), INVENTORY_MOBSELECT("inv.mobSelect.name"), bukkitMob("inv.mobSelect.bukkitEntityType"), mythicMob("inv.mobSelect.mythicMob"), epicBoss("inv.mobSelect.epicBoss"), boss("inv.mobSelect.boss"), + advancedSpawners("inv.mobSelect.advancedSpawners"), location("inv.stageEnding.locationTeleport"), command("inv.stageEnding.command"), @@ -613,8 +632,7 @@ public enum Lang{ poolItemAvoidDuplicates("inv.poolsManage.poolAvoidDuplicates"), poolItemQuestsList("inv.poolsManage.poolQuestsList"), // 0: size, 1: quests poolEdit("inv.poolsManage.edit"), - poolChoose( - "inv.poolsManage.choose"), + poolChoose("inv.poolsManage.choose"), poolCreate("inv.poolsManage.create"), INVENTORY_POOL_CREATE("inv.poolCreation.name"), @@ -652,6 +670,26 @@ public enum Lang{ title_stay("inv.editTitle.stay"), title_fadeOut("inv.editTitle.fadeOut"), + INVENTORY_PARTICLE_EFFECT("inv.particleEffect.name"), + particle_shape("inv.particleEffect.shape"), + particle_type("inv.particleEffect.type"), + particle_color("inv.particleEffect.color"), + + INVENTORY_PARTICLE_LIST("inv.particleList.name"), + particle_colored("inv.particleList.colored"), + + INVENTORY_DAMAGE_CAUSE("inv.damageCause.name"), + + INVENTORY_DAMAGE_CAUSES_LIST("inv.damageCausesList.name"), + + INVENTORY_VISIBILITY("inv.visibility.name"), + visibility_notStarted("inv.visibility.notStarted"), + visibility_inProgress("inv.visibility.inProgress"), + visibility_finished("inv.visibility.finished"), + visibility_maps("inv.visibility.maps"), + + INVENTORY_EQUIPMENT_SLOTS("inv.equipmentSlots.name"), + BOOK_NAME("inv.listBook.questName"), BOOK_STARTER("inv.listBook.questStarter"), BOOK_REWARDS("inv.listBook.questRewards"), @@ -686,6 +724,10 @@ public enum Lang{ SCOREBOARD_TAME("scoreboard.stage.tame"), // 0: animals to breed SCOREBOARD_LOCATION("scoreboard.stage.location"), // 0: x, 1: y, 2: z, 3: world SCOREBOARD_PLAY_TIME("scoreboard.stage.playTimeFormatted"), // 0: remaining time + SCOREBOARD_DIE("scoreboard.stage.die"), + SCOREBOARD_DEAL_DAMAGE_ANY("scoreboard.stage.dealDamage.any"), // 0: damage + SCOREBOARD_DEAL_DAMAGE_MOBS("scoreboard.stage.dealDamage.mobs"), // 0: damage, 1: mobs + SCOREBOARD_EAT_DRINK("scoreboard.stage.eatDrink"), // 0: items /* Indications */ @@ -732,6 +774,9 @@ public enum Lang{ PlayTime("misc.stageType.playTime"), Breed("misc.stageType.breedAnimals"), Tame("misc.stageType.tameAnimals"), + Death("misc.stageType.die"), + DealDamage("misc.stageType.dealDamage"), + EatDrink("misc.stageType.eatDrink"), ComparisonEquals("misc.comparison.equals"), ComparisonDifferent("misc.comparison.different"), @@ -754,6 +799,7 @@ public enum Lang{ RQuest("misc.requirement.quest"), RSkillLvl("misc.requirement.mcMMOSkillLevel"), RMoney("misc.requirement.money"), + REquipment("misc.requirement.equipment"), BucketWater("misc.bucket.water"), BucketLava("misc.bucket.lava"), @@ -766,6 +812,12 @@ public enum Lang{ ClickShiftLeft("misc.click.shift-left"), ClickMiddle("misc.click.middle"), + AmountItems("misc.amounts.items"), // 0: amount + AmountComparisons("misc.amounts.comparisons"), // 0: amount + AmountDialogLines("misc.amounts.dialogLines"), // 0: amount + AmountPermissions("misc.amounts.permissions"), // 0: amount + AmountMobs("misc.amounts.mobs"), // 0: amount + HologramText("misc.hologramText"), PoolHologramText("misc.poolHologramText"), MobsProgression("misc.mobsProgression"), @@ -806,45 +858,24 @@ private Lang(String path, Lang prefix) { this.prefix = prefix; } + @Override public String getPath(){ return path; } - private void setValue(String value){ + @Override + public void setValue(String value) { this.value = value; } @Override - public String toString(){ + public String getValue() { return prefix == null ? value : (prefix.toString() + value); } - public String format(Object... replace){ - return Utils.format(toString(), replace); - } - - public String format(Supplier... replace) { - return Utils.format(toString(), replace); - } - - public void send(CommandSender sender, Object... args){ - Utils.sendMessage(sender, toString(), args); - } - - public void sendWP(CommandSender p, Object... args){ - Utils.sendMessageWP(p, toString(), args); - } - - - public static void loadStrings(YamlConfiguration defaultConfig, YamlConfiguration config) { - for (Lang l : values()){ - String value = config.getString(l.path, null); - if (value == null) value = defaultConfig.getString(l.path, null); - if (value == null) DebugUtils.logMessage("Unavailable string in config for key " + l.path); - l.setValue(ChatUtils.translateHexColorCodes(ChatColor.translateAlternateColorCodes('&', value == null ? "§cunknown string" : value))); - } + @Override + public String toString() { + return getValue(); } - - } diff --git a/core/src/main/java/fr/skytasul/quests/utils/LevenshteinComparator.java b/core/src/main/java/fr/skytasul/quests/utils/LevenshteinComparator.java index b81edf16..4ba91530 100644 --- a/core/src/main/java/fr/skytasul/quests/utils/LevenshteinComparator.java +++ b/core/src/main/java/fr/skytasul/quests/utils/LevenshteinComparator.java @@ -16,6 +16,10 @@ public LevenshteinComparator setReference(String reference) { this.reference = reference; return this; } + + public Function getFunction() { + return function; + } @Override public int compare(T o1, T o2) { diff --git a/core/src/main/java/fr/skytasul/quests/utils/MinecraftNames.java b/core/src/main/java/fr/skytasul/quests/utils/MinecraftNames.java index 8400864b..36f7db51 100644 --- a/core/src/main/java/fr/skytasul/quests/utils/MinecraftNames.java +++ b/core/src/main/java/fr/skytasul/quests/utils/MinecraftNames.java @@ -7,7 +7,6 @@ import java.util.Map.Entry; import org.apache.commons.lang.WordUtils; -import org.apache.logging.log4j.core.util.FileUtils; import org.bukkit.entity.EntityType; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.PotionMeta; @@ -34,7 +33,11 @@ public static boolean intialize(String fileName){ BeautyQuests.logger.warning("File " + fileName + " not found for loading translations."); return false; } - if (!FileUtils.getFileExtension(file).toLowerCase().equals("json")) { + + int lastPoint = file.getName().lastIndexOf('.'); + String extension = lastPoint == -1 ? "" : file.getName().substring(lastPoint + 1); + + if (!extension.equalsIgnoreCase("json")) { BeautyQuests.logger.warning("File " + fileName + " is not a JSON file."); return false; } diff --git a/core/src/main/java/fr/skytasul/quests/utils/ParticleEffect.java b/core/src/main/java/fr/skytasul/quests/utils/ParticleEffect.java index 3c3052d4..8bd6fcd1 100644 --- a/core/src/main/java/fr/skytasul/quests/utils/ParticleEffect.java +++ b/core/src/main/java/fr/skytasul/quests/utils/ParticleEffect.java @@ -3,11 +3,12 @@ import java.util.List; import java.util.Random; +import org.apache.commons.lang.Validate; import org.bukkit.Color; import org.bukkit.Location; import org.bukkit.Particle; import org.bukkit.configuration.ConfigurationSection; -import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Entity; import org.bukkit.entity.Player; import fr.skytasul.quests.utils.compatibility.Post1_13; @@ -17,34 +18,53 @@ public class ParticleEffect { private static Random random = new Random(); - private ParticleType type; - private ParticleShape shape; + private final ParticleType type; + private final ParticleShape shape; + private final Color color; - private Color color; - private double[] colors; - private Object dustColor; + private final double[] colors; + private final Object dustColor; public ParticleEffect(Particle bukkitType, ParticleShape shape, Color color) { + Validate.notNull(bukkitType); this.type = new ParticleType(bukkitType); this.shape = shape; this.color = color; if (type.dustColored) { dustColor = Post1_13.getDustColor(color, 1); + colors = null; }else if (type.colored && bukkitType != Particle.NOTE) { + dustColor = null; colors = new double[3]; colors[0] = color.getRed() == 0 ? Float.MIN_NORMAL : color.getRed() / 255D; colors[1] = color.getGreen() / 255D; colors[2] = color.getBlue() / 255D; + }else { + dustColor = null; + colors = null; + color = null; } } + public Particle getParticle() { + return type.particle; + } + + public ParticleShape getShape() { + return shape; + } + + public Color getColor() { + return color; + } + @Override public String toString() { - return type.particle.name() + " in shape " + shape.name() + (type.colored ? " with color \"R" + (type.particle != Particle.NOTE ? color.getRed() + " G" + color.getGreen() + " B" + color.getBlue() : "random") + "\"" : ""); + return type.particle.name() + (shape == null ? "" : " in shape " + shape.name()) + (type.colored ? " with color " + (type.particle != Particle.NOTE ? "R" + color.getRed() + " G" + color.getGreen() + " B" + color.getBlue() : "random") : ""); } - public void send(LivingEntity entity, List players) { + public void send(Entity entity, List players) { send(entity.getLocation(), NMS.getNMS().entityNameplateHeight(entity), players); } @@ -77,6 +97,8 @@ public void sendParticle(Location lc, List players, double offX, double offX = random.nextInt(24) / 24D; // this offset contains the note number offY = 0; offZ = 0; + amount = 0; // amount must be 0 for note color to be enabled + extra = 1; // speed must be to 1 for the same reason }else if (dustColor != null) { data = dustColor; }else if (colors != null) { @@ -93,11 +115,22 @@ public void sendParticle(Location lc, List players, double offX, double } } + public void serialize(ConfigurationSection section) { + section.set("particleEffect", type.particle.name()); + section.set("particleShape", shape.name()); + if (color != null) section.set("particleColor", color.serialize()); + } + public static ParticleEffect deserialize(ConfigurationSection data) { return new ParticleEffect( Particle.valueOf(data.getString("particleEffect").toUpperCase()), ParticleShape.valueOf(data.getString("particleShape").toUpperCase()), - Color.deserialize(data.getConfigurationSection("particleColor").getValues(false))); + data.contains("particleColor") ? Color.deserialize(data.getConfigurationSection("particleColor").getValues(false)) : null); + } + + public static boolean canHaveColor(Particle particle) { + if (NMS.getMCVersion() >= 13) return particle.getDataType() == Post1_13.getDustOptionClass(); + return particle == Particle.REDSTONE || particle == Particle.SPELL_MOB || particle == Particle.SPELL_MOB_AMBIENT; } public enum ParticleShape { diff --git a/core/src/main/java/fr/skytasul/quests/utils/ThrowingConsumer.java b/core/src/main/java/fr/skytasul/quests/utils/ThrowingConsumer.java new file mode 100644 index 00000000..4e52d93d --- /dev/null +++ b/core/src/main/java/fr/skytasul/quests/utils/ThrowingConsumer.java @@ -0,0 +1,8 @@ +package fr.skytasul.quests.utils; + +@FunctionalInterface +public interface ThrowingConsumer { + + public void accept(T object) throws E; + +} diff --git a/core/src/main/java/fr/skytasul/quests/utils/Utils.java b/core/src/main/java/fr/skytasul/quests/utils/Utils.java index c4e09e48..e82cbd08 100644 --- a/core/src/main/java/fr/skytasul/quests/utils/Utils.java +++ b/core/src/main/java/fr/skytasul/quests/utils/Utils.java @@ -1,5 +1,13 @@ package fr.skytasul.quests.utils; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; @@ -11,11 +19,12 @@ import java.util.Map.Entry; import java.util.Objects; import java.util.Optional; +import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; - +import java.util.stream.Stream; import org.apache.commons.lang.StringUtils; import org.bukkit.Bukkit; import org.bukkit.Location; @@ -23,6 +32,8 @@ import org.bukkit.World; import org.bukkit.command.CommandSender; import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.MemoryConfiguration; +import org.bukkit.entity.EntityType; import org.bukkit.entity.Firework; import org.bukkit.entity.LivingEntity; import org.bukkit.entity.Player; @@ -31,8 +42,6 @@ import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.metadata.FixedMetadataValue; import org.bukkit.scoreboard.DisplaySlot; -import org.bukkit.util.Consumer; - import fr.skytasul.quests.BeautyQuests; import fr.skytasul.quests.QuestsConfiguration; import fr.skytasul.quests.api.rewards.AbstractReward; @@ -41,7 +50,7 @@ import fr.skytasul.quests.utils.compatibility.DependenciesManager; import fr.skytasul.quests.utils.compatibility.QuestsPlaceholders; import fr.skytasul.quests.utils.nms.NMS; - +import net.md_5.bungee.api.ChatColor; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; @@ -77,7 +86,7 @@ public static void spawnFirework(Location lc, FireworkMeta meta) { fw.setFireworkMeta(meta); }; if (NMS.getMCVersion() >= 12) { - lc.getWorld().spawn(lc, Firework.class, fwConsumer); + lc.getWorld().spawn(lc, Firework.class, fw -> fwConsumer.accept(fw)); // much better to use the built-in since 1.12 method to do operations on entity // before it is sent to the players, as it will not create flickering }else { @@ -92,7 +101,7 @@ public static List giveRewards(Player p, List rewards) { try { List messages = rew.give(p); if (messages != null) msg.addAll(messages); - }catch (Exception e) { + } catch (Throwable e) { BeautyQuests.logger.severe("Error when giving reward " + rew.getName() + " to " + p.getName(), e); } } @@ -107,6 +116,8 @@ public static String ticksToElapsedTime(int ticks) { } public static String millisToHumanString(long time) { + if (time == 0) return "x"; + StringBuilder sb = new StringBuilder(); long weeks = time / 604_800_000; @@ -154,6 +165,7 @@ public static void sendMessageWP(CommandSender sender, String msg, Object... rep public static String finalFormat(CommandSender sender, String text, boolean playerName, Object... replace) { if (DependenciesManager.papi.isEnabled() && sender instanceof Player) text = QuestsPlaceholders.setPlaceholders((Player) sender, text); if (playerName) text = text.replace("{PLAYER}", sender.getName()).replace("{PREFIX}", QuestsConfiguration.getPrefix()); + text = ChatColor.translateAlternateColorCodes('&', text); return format(text, replace); } @@ -329,6 +341,25 @@ public static double parseDouble(Object obj) { return 0; } + public static void walkResources(Class clazz, String path, int depth, Consumer consumer) throws URISyntaxException, IOException { + URI uri = clazz.getResource(path).toURI(); + FileSystem fileSystem = null; + Path myPath; + try { + if (uri.getScheme().equals("jar")) { + fileSystem = FileSystems.newFileSystem(uri, Collections.emptyMap()); + myPath = fileSystem.getPath(path); + }else { + myPath = Paths.get(uri); + } + + try (Stream walker = Files.walk(myPath, depth)) { + walker.forEach(consumer); + } + }finally { + if (fileSystem != null) fileSystem.close(); + } + } public static void runOrSync(Runnable run) { @@ -353,11 +384,11 @@ public static List> serializeList(Collection objects, return ls; } - public static List deserializeList(List> serialized, Function, T> deserialize){ + public static List deserializeList(List> serialized, Function, T> deserialize) { List ls = new ArrayList<>(); if (serialized != null) { - for (Map map : serialized) { - ls.add(deserialize.apply(map)); + for (Map map : serialized) { + ls.add(deserialize.apply((Map) map)); } } return ls; @@ -373,6 +404,22 @@ public static Map mapFromConfigurationSection(ConfigurationSecti return map; } + public static ConfigurationSection createConfigurationSection(Map content) { + MemoryConfiguration section = new MemoryConfiguration(); + setConfigurationSectionContent(section, content); + return section; + } + + public static void setConfigurationSectionContent(ConfigurationSection section, Map content) { + content.forEach((key, value) -> { + if (value instanceof Map) { + section.createSection(key, (Map) value); + }else { + section.set(key, value); + } + }); + } + public static List combineItems(List items) { ArrayList newItems = new ArrayList<>(items.size()); items: for (ItemStack original : items) { @@ -483,5 +530,22 @@ public static boolean isQuestItem(ItemStack item) { } return false; } + + public static XMaterial mobItem(EntityType type) { + if (type == null) return XMaterial.SPONGE; + Optional material = XMaterial.matchXMaterial(type.name() + "_SPAWN_EGG"); + if (material.isPresent()) return material.get(); + if (type == EntityType.WITHER) return XMaterial.WITHER_SKELETON_SKULL; + if (type == EntityType.IRON_GOLEM) return XMaterial.IRON_BLOCK; + if (type == EntityType.SNOWMAN) return XMaterial.SNOW_BLOCK; + if (type == EntityType.MUSHROOM_COW) return XMaterial.MOOSHROOM_SPAWN_EGG; + if (type == EntityType.GIANT) return XMaterial.ZOMBIE_SPAWN_EGG; + if (type == EntityType.ARMOR_STAND) return XMaterial.ARMOR_STAND; + if (type == EntityType.PLAYER) return XMaterial.PLAYER_HEAD; + if (type == EntityType.ENDER_DRAGON) return XMaterial.DRAGON_HEAD; + if (type.name().equals("PIG_ZOMBIE") || type.name().equals("ZOMBIFIED_PIGLIN")) return XMaterial.ZOMBIFIED_PIGLIN_SPAWN_EGG; + if (type.name().equals("ILLUSIONER")) return XMaterial.BLAZE_POWDER; + return XMaterial.SPONGE; + } } \ No newline at end of file diff --git a/core/src/main/java/fr/skytasul/quests/utils/XBlock.java b/core/src/main/java/fr/skytasul/quests/utils/XBlock.java index 91859efa..2261123d 100644 --- a/core/src/main/java/fr/skytasul/quests/utils/XBlock.java +++ b/core/src/main/java/fr/skytasul/quests/utils/XBlock.java @@ -1,7 +1,7 @@ /* * The MIT License (MIT) * - * Copyright (c) 2020 Crypto Morin + * Copyright (c) 2022 Crypto Morin * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,22 +21,20 @@ */ package fr.skytasul.quests.utils; -import java.util.Collection; -import java.util.Collections; -import java.util.EnumSet; -import java.util.Locale; -import java.util.Optional; -import java.util.Set; +import java.util.*; +import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.apache.commons.lang.Validate; import org.bukkit.DyeColor; import org.bukkit.Material; import org.bukkit.TreeSpecies; +import org.bukkit.block.Banner; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; import org.bukkit.block.BlockState; +import org.bukkit.block.data.BlockData; import org.bukkit.inventory.InventoryHolder; import org.bukkit.material.Cake; import org.bukkit.material.Colorable; @@ -56,7 +54,7 @@ * This class doesn't and shouldn't support materials that are {@link Material#isLegacy()}. * * @author Crypto Morin - * @version 2.0.0 + * @version 2.2.0 * @see Block * @see BlockState * @see MaterialData @@ -71,10 +69,27 @@ public final class XBlock { )); public static final Set DANGEROUS = Collections.unmodifiableSet(EnumSet.of(XMaterial.MAGMA_BLOCK, XMaterial.LAVA, XMaterial.CAMPFIRE, XMaterial.FIRE, XMaterial.SOUL_FIRE)); public static final byte CAKE_SLICES = 6; - private static final boolean ISFLAT = XMaterial.isNewVersion(); - - private XBlock() {} + private static final boolean ISFLAT = XMaterial.supports(13); + private static final Map ITEM_TO_BLOCK = new EnumMap<>(XMaterial.class); + + static { + ITEM_TO_BLOCK.put(XMaterial.MELON_SLICE, XMaterial.MELON_STEM); + ITEM_TO_BLOCK.put(XMaterial.MELON_SEEDS, XMaterial.MELON_STEM); + + ITEM_TO_BLOCK.put(XMaterial.CARROT_ON_A_STICK, XMaterial.CARROTS); + ITEM_TO_BLOCK.put(XMaterial.GOLDEN_CARROT, XMaterial.CARROTS); + ITEM_TO_BLOCK.put(XMaterial.CARROT, XMaterial.CARROTS); + + ITEM_TO_BLOCK.put(XMaterial.POTATO, XMaterial.POTATOES); + ITEM_TO_BLOCK.put(XMaterial.BAKED_POTATO, XMaterial.POTATOES); + ITEM_TO_BLOCK.put(XMaterial.POISONOUS_POTATO, XMaterial.POTATOES); + + ITEM_TO_BLOCK.put(XMaterial.PUMPKIN_SEEDS, XMaterial.PUMPKIN_STEM); + ITEM_TO_BLOCK.put(XMaterial.PUMPKIN_PIE, XMaterial.PUMPKIN); + } + private XBlock() {} + public static boolean isLit(Block block) { if (ISFLAT) { if (!(block.getBlockData() instanceof org.bukkit.block.data.Lightable)) return false; @@ -86,13 +101,14 @@ public static boolean isLit(Block block) { } /** - * Checks if the block is a container. - * Containers are chests, hoppers, enderchests and everything that - * has an inventory. - * - * @param block the block to check. - * @return true if the block is a container, otherwise false. - */ + * Checks if the block is a container. + * Containers are chests, hoppers, enderchests and everything that + * has an inventory. + * + * @param block the block to check. + * + * @return true if the block is a container, otherwise false. + */ public static boolean isContainer(@Nullable Block block) { return block != null && block.getState() instanceof InventoryHolder; } @@ -106,8 +122,10 @@ public static boolean isContainer(@Nullable Block block) { public static void setLit(Block block, boolean lit) { if (ISFLAT) { if (!(block.getBlockData() instanceof org.bukkit.block.data.Lightable)) return; - org.bukkit.block.data.Lightable lightable = (org.bukkit.block.data.Lightable) block.getBlockData(); + BlockData data = block.getBlockData(); + org.bukkit.block.data.Lightable lightable = (org.bukkit.block.data.Lightable) data; lightable.setLit(lit); + block.setBlockData(data, false); return; } @@ -123,6 +141,7 @@ else if (name.startsWith("REDSTONE_LAMP")) * Any material that can be planted which is from {@link #CROPS} * * @param material the material to check. + * * @return true if this material is a crop, otherwise false. */ public static boolean isCrop(XMaterial material) { @@ -133,6 +152,7 @@ public static boolean isCrop(XMaterial material) { * Any material that can damage players, usually by interacting with the block. * * @param material the material to check. + * * @return true if this material is dangerous, otherwise false. */ public static boolean isDangerous(XMaterial material) { @@ -192,24 +212,24 @@ public static boolean isPotato(@Nullable Material material) { public static BlockFace getDirection(Block block) { if (ISFLAT) { - if (!(block.getBlockData() instanceof Directional)) return BlockFace.SELF; - Directional direction = (Directional) block.getBlockData(); + if (!(block.getBlockData() instanceof org.bukkit.block.data.Directional)) return BlockFace.SELF; + org.bukkit.block.data.Directional direction = (org.bukkit.block.data.Directional) block.getBlockData(); return direction.getFacing(); } BlockState state = block.getState(); MaterialData data = state.getData(); - if (data instanceof org.bukkit.material.Directional) { - return ((org.bukkit.material.Directional) data).getFacing(); - } - return null; + if (data instanceof org.bukkit.material.Directional) return ((org.bukkit.material.Directional) data).getFacing(); + return BlockFace.SELF; } public static boolean setDirection(Block block, BlockFace facing) { if (ISFLAT) { if (!(block.getBlockData() instanceof org.bukkit.block.data.Directional)) return false; - org.bukkit.block.data.Directional direction = (org.bukkit.block.data.Directional) block.getBlockData(); + BlockData data = block.getBlockData(); + org.bukkit.block.data.Directional direction = (org.bukkit.block.data.Directional) data; direction.setFacing(facing); + block.setBlockData(data, false); return true; } @@ -223,6 +243,124 @@ public static boolean setDirection(Block block, BlockFace facing) { return false; } + public static boolean setType(@Nonnull Block block, @Nullable XMaterial material, boolean applyPhysics) { + Objects.requireNonNull(block, "Cannot set type of null block"); + if (material == null) material = XMaterial.AIR; + XMaterial smartConversion = ITEM_TO_BLOCK.get(material); + if (smartConversion != null) material = smartConversion; + if (material.parseMaterial() == null) return false; + + block.setType(material.parseMaterial(), applyPhysics); + if (XMaterial.supports(13)) return false; + + String parsedName = material.parseMaterial().name(); + if (parsedName.endsWith("_ITEM")) { + String blockName = parsedName.substring(0, parsedName.length() - "_ITEM".length()); + Material blockMaterial = Objects.requireNonNull(Material.getMaterial(blockName), () -> "Could not find block material for item '" + parsedName + "' as '" + blockName + '\''); + block.setType(blockMaterial, applyPhysics); + }else if (parsedName.contains("CAKE")) { + Material blockMaterial = Material.getMaterial("CAKE_BLOCK"); + block.setType(blockMaterial, applyPhysics); + } + + LegacyMaterial legacyMaterial = LegacyMaterial.getMaterial(parsedName); + if (legacyMaterial == LegacyMaterial.BANNER) block.setType(LegacyMaterial.STANDING_BANNER.material, applyPhysics); + LegacyMaterial.Handling handling = legacyMaterial == null ? null : legacyMaterial.handling; + + BlockState state = block.getState(); + boolean update = false; + + if (handling == LegacyMaterial.Handling.COLORABLE) { + if (state instanceof Banner) { + Banner banner = (Banner) state; + String xName = material.name(); + int colorIndex = xName.indexOf('_'); + String color = xName.substring(0, colorIndex); + if (color.equals("LIGHT")) color = xName.substring(0, "LIGHT_".length() + 4); + + banner.setBaseColor(DyeColor.valueOf(color)); + }else state.setRawData(material.getData()); + update = true; + }else if (handling == LegacyMaterial.Handling.WOOD_SPECIES) { + // Wood doesn't exist in 1.8 + // https://hub.spigotmc.org/stash/projects/SPIGOT/repos/bukkit/browse/src/main/java/org/bukkit/material/Wood.java?until=7d83cba0f2575112577ed7a091ed8a193bfc261a&untilPath=src%2Fmain%2Fjava%2Forg%2Fbukkit%2Fmaterial%2FWood.java + // https://hub.spigotmc.org/stash/projects/SPIGOT/repos/bukkit/browse/src/main/java/org/bukkit/TreeSpecies.java + + String name = material.name(); + int firstIndicator = name.indexOf('_'); + if (firstIndicator < 0) return false; + String woodType = name.substring(0, firstIndicator); + + TreeSpecies species; + switch (woodType) { + case "OAK": + species = TreeSpecies.GENERIC; + break; + case "DARK": + species = TreeSpecies.DARK_OAK; + break; + case "SPRUCE": + species = TreeSpecies.REDWOOD; + break; + default: { + try { + species = TreeSpecies.valueOf(woodType); + }catch (IllegalArgumentException ex) { + throw new AssertionError("Unknown material " + legacyMaterial + " for wood species"); + } + } + } + + // Doesn't handle stairs, slabs, fence and fence gates as they had their own separate materials. + boolean firstType = false; + switch (legacyMaterial) { + case WOOD: + case WOOD_DOUBLE_STEP: + state.setRawData(species.getData()); + update = true; + break; + case LOG: + case LEAVES: + firstType = true; + // fall through to next switch statement below + case LOG_2: + case LEAVES_2: + switch (species) { + case GENERIC: + case REDWOOD: + case BIRCH: + case JUNGLE: + if (!firstType) throw new AssertionError("Invalid tree species " + species + " for block type" + legacyMaterial + ", use block type 2 instead"); + break; + case ACACIA: + case DARK_OAK: + if (firstType) throw new AssertionError("Invalid tree species " + species + " for block type 2 " + legacyMaterial + ", use block type instead"); + break; + } + state.setRawData((byte) ((state.getRawData() & 0xC) | (species.getData() & 0x3))); + update = true; + break; + case SAPLING: + case WOOD_STEP: + state.setRawData((byte) ((state.getRawData() & 0x8) | species.getData())); + update = true; + break; + default: + throw new AssertionError("Unknown block type " + legacyMaterial + " for tree species: " + species); + } + }else if (material.getData() != 0) { + state.setRawData(material.getData()); + update = true; + } + + if (update) state.update(false, applyPhysics); + return update; + } + + public static boolean setType(@Nonnull Block block, @Nullable XMaterial material) { + return setType(block, material, true); + } + public static int getAge(Block block) { if (ISFLAT) { if (!(block.getBlockData() instanceof org.bukkit.block.data.Ageable)) return 0; @@ -238,8 +376,10 @@ public static int getAge(Block block) { public static void setAge(Block block, int age) { if (ISFLAT) { if (!(block.getBlockData() instanceof org.bukkit.block.data.Ageable)) return; - org.bukkit.block.data.Ageable ageable = (org.bukkit.block.data.Ageable) block.getBlockData(); + BlockData data = block.getBlockData(); + org.bukkit.block.data.Ageable ageable = (org.bukkit.block.data.Ageable) data; ageable.setAge(age); + block.setBlockData(data, false); } BlockState state = block.getState(); @@ -249,12 +389,13 @@ public static void setAge(Block block, int age) { } /** - * Sets the type of any block that can be colored. - * - * @param block the block to color. - * @param color the color to use. - * @return true if the block can be colored, otherwise false. - */ + * Sets the type of any block that can be colored. + * + * @param block the block to color. + * @param color the color to use. + * + * @return true if the block can be colored, otherwise false. + */ public static boolean setColor(Block block, DyeColor color) { if (ISFLAT) { String type = block.getType().name(); @@ -279,13 +420,16 @@ public static boolean setColor(Block block, DyeColor color) { * * @param block the block to set the fluid level of. * @param level the level of fluid. + * * @return true if this block can have a fluid level, otherwise false. */ public static boolean setFluidLevel(Block block, int level) { if (ISFLAT) { if (!(block.getBlockData() instanceof org.bukkit.block.data.Levelled)) return false; - org.bukkit.block.data.Levelled levelled = (org.bukkit.block.data.Levelled) block.getBlockData(); + BlockData data = block.getBlockData(); + org.bukkit.block.data.Levelled levelled = (org.bukkit.block.data.Levelled) data; levelled.setLevel(level); + block.setBlockData(data, false); return true; } @@ -350,12 +494,12 @@ public static boolean isOneOf(Block block, Collection blocks) { public static void setCakeSlices(Block block, int amount) { Validate.isTrue(isCake(block.getType()), "Block is not a cake: " + block.getType()); if (ISFLAT) { - org.bukkit.block.data.BlockData bd = block.getBlockData(); - org.bukkit.block.data.type.Cake cake = (org.bukkit.block.data.type.Cake) bd; + BlockData data = block.getBlockData(); + org.bukkit.block.data.type.Cake cake = (org.bukkit.block.data.type.Cake) data; int remaining = cake.getMaximumBites() - (cake.getBites() + amount); if (remaining > 0) { cake.setBites(remaining); - block.setBlockData(bd); + block.setBlockData(data); }else { block.breakNaturally(); } @@ -376,14 +520,14 @@ public static void setCakeSlices(Block block, int amount) { public static int addCakeSlices(Block block, int slices) { Validate.isTrue(isCake(block.getType()), "Block is not a cake: " + block.getType()); if (ISFLAT) { - org.bukkit.block.data.BlockData bd = block.getBlockData(); - org.bukkit.block.data.type.Cake cake = (org.bukkit.block.data.type.Cake) bd; + BlockData data = block.getBlockData(); + org.bukkit.block.data.type.Cake cake = (org.bukkit.block.data.type.Cake) data; int bites = cake.getBites() - slices; int remaining = cake.getMaximumBites() - bites; if (remaining > 0) { cake.setBites(bites); - block.setBlockData(bd); + block.setBlockData(data); return remaining; }else { block.breakNaturally(); @@ -403,19 +547,6 @@ public static int addCakeSlices(Block block, int slices) { block.breakNaturally(); return 0; } - } - - public static boolean setWooden(Block block, XMaterial species) { - block.setType(species.parseMaterial()); - if (ISFLAT) return true; - - TreeSpecies type = species == XMaterial.SPRUCE_LOG ? TreeSpecies.REDWOOD : TreeSpecies.valueOf(species.name().substring(0, species.name().indexOf('_'))); - - BlockState state = block.getState(); - MaterialData data = state.getData(); - ((Wood) data).setSpecies(type); - state.update(true); - return true; } public static void setEnderPearlOnFrame(Block endPortalFrame, boolean eye) { @@ -433,6 +564,7 @@ public static void setEnderPearlOnFrame(Block endPortalFrame, boolean eye) { /** * @param block the block to get its XMaterial type. + * * @return the XMaterial of the block. * @deprecated Not stable, use {@link #isType(Block, XMaterial)} or {@link #isSimilar(Block, XMaterial)} instead. * If you want to save a block material somewhere, you need to use {@link XMaterial#matchXMaterial(Material)} @@ -443,34 +575,37 @@ public static XMaterial getType(Block block) { String type = block.getType().name(); BlockState state = block.getState(); MaterialData data = state.getData(); + byte dataValue; if (data instanceof Wood) { TreeSpecies species = ((Wood) data).getSpecies(); - return XMaterial.matchXMaterial(species.name() + block.getType().name()) - .orElseThrow(() -> new IllegalArgumentException("Unsupported material from tree species " + species.name() + ": " + block.getType().name())); - } - if (data instanceof Colorable) { - Colorable color = (Colorable) data; - return XMaterial.matchXMaterial(color.getColor().name() + '_' + type).orElseThrow(() -> new IllegalArgumentException("Unsupported colored material")); + dataValue = species.getData(); + }else if (data instanceof Colorable) { + DyeColor color = ((Colorable) data).getColor(); + dataValue = color.getDyeData(); + }else { + dataValue = data.getData(); } - return XMaterial.matchXMaterial(block.getType()); - } - - /** + + return XMaterial.matchDefinedXMaterial(type, dataValue).orElseThrow(() -> new IllegalArgumentException("Unsupported material for block " + dataValue + ": " + block.getType().name())); + } + + /** * Same as {@link #isType(Block, XMaterial)} except it also does a simple {@link XMaterial#matchXMaterial(Material)} * comparison with the given block and material. * * @param block the block to compare. * @param material the material to compare with. + * * @return true if block type is similar to the given material. * @see #isType(Block, XMaterial) * @since 1.3.0 */ public static boolean isSimilar(Block block, XMaterial material) { return material == XMaterial.matchXMaterial(block.getType()) || isType(block, material); - } - - /** + } + + /** * Universal Method *

* Check if the block type matches the specified XMaterial. @@ -479,6 +614,7 @@ public static boolean isSimilar(Block block, XMaterial material) { * * @param block the block to check. * @param material the XMaterial similar to this block type. + * * @return true if the raw block type matches with the material. * @see #isSimilar(Block, XMaterial) */ @@ -515,14 +651,23 @@ public static boolean isType(Block block, XMaterial material) { case CAVE_AIR: case VOID_AIR: return isAir(mat); - default: - return mat.equals(material.parseMaterial()); // EDITED FOR BEAUTYQUESTS } + return false; } public static boolean isAir(@Nullable Material material) { - if (material == Material.AIR) return true; - return ISFLAT && (material == Material.CAVE_AIR || material == Material.VOID_AIR); + if (ISFLAT) { + // material.isAir() doesn't exist for 1.13 + switch (material) { + case AIR: + case CAVE_AIR: + case VOID_AIR: + return true; + default: + return false; + } + } + return material == Material.AIR; } public static boolean isPowered(Block block) { @@ -540,8 +685,10 @@ public static boolean isPowered(Block block) { public static void setPowered(Block block, boolean powered) { if (ISFLAT) { if (!(block.getBlockData() instanceof org.bukkit.block.data.Powerable)) return; - org.bukkit.block.data.Powerable powerable = (org.bukkit.block.data.Powerable) block.getBlockData(); + BlockData data = block.getBlockData(); + org.bukkit.block.data.Powerable powerable = (org.bukkit.block.data.Powerable) data; powerable.setPowered(powered); + block.setBlockData(data, false); return; } @@ -565,8 +712,12 @@ public static boolean isOpen(Block block) { public static void setOpened(Block block, boolean opened) { if (ISFLAT) { if (!(block.getBlockData() instanceof org.bukkit.block.data.Openable)) return; - org.bukkit.block.data.Openable openable = (org.bukkit.block.data.Openable) block.getBlockData(); + // These useless "data" variables are used because JVM doesn't like upcasts/downcasts for + // non-existing classes even if unused. + BlockData data = block.getBlockData(); + org.bukkit.block.data.Openable openable = (org.bukkit.block.data.Openable) data; openable.setOpen(opened); + block.setBlockData(data, false); return; } @@ -591,8 +742,10 @@ public static BlockFace getRotation(Block block) { public static void setRotation(Block block, BlockFace facing) { if (ISFLAT) { if (!(block.getBlockData() instanceof org.bukkit.block.data.Rotatable)) return; - org.bukkit.block.data.Rotatable rotatable = (org.bukkit.block.data.Rotatable) block.getBlockData(); + BlockData data = block.getBlockData(); + org.bukkit.block.data.Rotatable rotatable = (org.bukkit.block.data.Rotatable) data; rotatable.setRotation(facing); + block.setBlockData(data, false); } } @@ -604,6 +757,69 @@ private static boolean isMaterial(Block block, BlockMaterial... materials) { return false; } + private enum LegacyMaterial { + // Colorable + STANDING_BANNER( + Handling.COLORABLE), + WALL_BANNER( + Handling.COLORABLE), + BANNER( + Handling.COLORABLE), + CARPET( + Handling.COLORABLE), + WOOL( + Handling.COLORABLE), + STAINED_CLAY( + Handling.COLORABLE), + STAINED_GLASS( + Handling.COLORABLE), + STAINED_GLASS_PANE( + Handling.COLORABLE), + THIN_GLASS( + Handling.COLORABLE), + + // Wood Species + WOOD( + Handling.WOOD_SPECIES), + WOOD_STEP( + Handling.WOOD_SPECIES), + WOOD_DOUBLE_STEP( + Handling.WOOD_SPECIES), + LEAVES( + Handling.WOOD_SPECIES), + LEAVES_2( + Handling.WOOD_SPECIES), + LOG( + Handling.WOOD_SPECIES), + LOG_2( + Handling.WOOD_SPECIES), + SAPLING( + Handling.WOOD_SPECIES); + + private static final Map LOOKUP = new HashMap<>(); + + static { + for (LegacyMaterial legacyMaterial : values()) { + LOOKUP.put(legacyMaterial.name(), legacyMaterial); + } + } + + private final Material material = Material.getMaterial(name()); + private final Handling handling; + + LegacyMaterial(Handling handling) { + this.handling = handling; + } + + private static LegacyMaterial getMaterial(String name) { + return LOOKUP.get(name); + } + + private enum Handling { + COLORABLE, WOOD_SPECIES; + } + } + /** * An enum with cached legacy materials which can be used when comparing blocks with blocks and blocks with items. * diff --git a/core/src/main/java/fr/skytasul/quests/utils/XMaterial.java b/core/src/main/java/fr/skytasul/quests/utils/XMaterial.java index 193363d0..6bde6370 100644 --- a/core/src/main/java/fr/skytasul/quests/utils/XMaterial.java +++ b/core/src/main/java/fr/skytasul/quests/utils/XMaterial.java @@ -22,37 +22,24 @@ */ package fr.skytasul.quests.utils; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Locale; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.TimeUnit; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.regex.PatternSyntaxException; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.apache.commons.lang.StringUtils; -import org.apache.commons.lang.Validate; -import org.apache.commons.lang.WordUtils; +import com.google.common.base.Enums; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; import org.bukkit.Bukkit; import org.bukkit.Material; -import org.bukkit.SkullType; -import org.bukkit.entity.EntityType; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.inventory.meta.SpawnEggMeta; import org.bukkit.potion.Potion; -import com.google.common.base.Enums; -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; +import java.util.stream.Collectors; /** * XMaterial - Data Values/Pre-flattening
@@ -74,3160 +61,2178 @@ * /give @p minecraft:dirt 1 10 where 1 is the item amount, and 10 is the data value. The material {@link #DIRT} with a data value of {@code 10} doesn't exist. * * @author Crypto Morin - * @version 10.1.1.1 + * @version 11.0.0 * @see Material * @see ItemStack */ public enum XMaterial { - ACACIA_BOAT( - "BOAT_ACACIA"), - ACACIA_BUTTON( - "WOOD_BUTTON"), - ACACIA_DOOR( - "ACACIA_DOOR", - "ACACIA_DOOR_ITEM"), - ACACIA_FENCE, - ACACIA_FENCE_GATE, - ACACIA_LEAVES( - "LEAVES_2"), - ACACIA_LOG( - "LOG_2"), - ACACIA_PLANKS( - 4, - "WOOD"), - ACACIA_PRESSURE_PLATE( - "WOOD_PLATE"), - ACACIA_SAPLING( - 4, - "SAPLING"), - ACACIA_SIGN( - "SIGN_POST", - "SIGN"), - ACACIA_SLAB( - 4, - "WOOD_DOUBLE_STEP", - "WOOD_STEP", - "WOODEN_SLAB"), - ACACIA_STAIRS, - ACACIA_TRAPDOOR( - "TRAP_DOOR"), - ACACIA_WALL_SIGN( - "WALL_SIGN"), - ACACIA_WOOD( - "LOG_2"), - ACTIVATOR_RAIL, - /** - * https://minecraft.gamepedia.com/Air - * {@link Material#isAir()} - * - * @see #VOID_AIR - * @see #CAVE_AIR - */ - AIR, - ALLIUM( - 2, - "RED_ROSE"), - AMETHYST_BLOCK, - AMETHYST_CLUSTER, - AMETHYST_SHARD, - ANCIENT_DEBRIS, - ANDESITE( - 5, - "STONE"), - ANDESITE_SLAB, - ANDESITE_STAIRS, - ANDESITE_WALL, - ANVIL, - APPLE, - ARMOR_STAND, - ARROW, - ATTACHED_MELON_STEM( - 7, - "MELON_STEM"), - ATTACHED_PUMPKIN_STEM( - 7, - "PUMPKIN_STEM"), - AXOLOTL_BUCKET, - AXOLOTL_SPAWN_EGG, - AZALEA, - AZALEA_LEAVES, - AZURE_BLUET( - 3, - "RED_ROSE"), - BAKED_POTATO, - BAMBOO, - BAMBOO_SAPLING, - BARREL, - BARRIER, - BASALT, - BAT_SPAWN_EGG( - 65, - "MONSTER_EGG"), - BEACON, - BEDROCK, - BEEF( - "RAW_BEEF"), - BEEHIVE, - /** - * Beetroot is a known material in pre-1.13 - */ - BEETROOT( - "BEETROOT_BLOCK"), - BEETROOTS( - "BEETROOT"), - BEETROOT_SEEDS, - BEETROOT_SOUP, - BEE_NEST, - BEE_SPAWN_EGG, - BELL, - BIG_DRIPLEAF, - BIG_DRIPLEAF_STEM, - BIRCH_BOAT( - "BOAT_BIRCH"), - BIRCH_BUTTON( - "WOOD_BUTTON"), - BIRCH_DOOR( - "BIRCH_DOOR", - "BIRCH_DOOR_ITEM"), - BIRCH_FENCE, - BIRCH_FENCE_GATE, - BIRCH_LEAVES( - 2, - "LEAVES"), - BIRCH_LOG( - 2, - "LOG"), - BIRCH_PLANKS( - 2, - "WOOD"), - BIRCH_PRESSURE_PLATE( - "WOOD_PLATE"), - BIRCH_SAPLING( - 2, - "SAPLING"), - BIRCH_SIGN( - "SIGN_POST", - "SIGN"), - BIRCH_SLAB( - 2, - "WOOD_DOUBLE_STEP", - "WOOD_STEP", - "WOODEN_SLAB"), - BIRCH_STAIRS( - "BIRCH_WOOD_STAIRS"), - BIRCH_TRAPDOOR( - "TRAP_DOOR"), - BIRCH_WALL_SIGN( - "WALL_SIGN"), - BIRCH_WOOD( - 2, - "LOG"), - BLACKSTONE, - BLACKSTONE_SLAB, - BLACKSTONE_STAIRS, - BLACKSTONE_WALL, - BLACK_BANNER( - "STANDING_BANNER", - "BANNER"), - /** - * Version 1.12+ interprets "BED" as BLACK_BED due to enum alphabetic ordering. - */ - BLACK_BED( - supports(12) ? 15 : 0, - "BED_BLOCK", - "BED"), - BLACK_CANDLE, - BLACK_CANDLE_CAKE, - BLACK_CARPET( - 15, - "CARPET"), - BLACK_CONCRETE( - 15, - "CONCRETE"), - BLACK_CONCRETE_POWDER( - 15, - "CONCRETE_POWDER"), - BLACK_DYE, - BLACK_GLAZED_TERRACOTTA, - BLACK_SHULKER_BOX, - BLACK_STAINED_GLASS( - 15, - "STAINED_GLASS"), - BLACK_STAINED_GLASS_PANE( - 15, - "STAINED_GLASS_PANE"), - BLACK_TERRACOTTA( - 15, - "STAINED_CLAY"), - BLACK_WALL_BANNER( - "WALL_BANNER"), - BLACK_WOOL( - 15, - "WOOL"), - BLAST_FURNACE, - BLAZE_POWDER, - BLAZE_ROD, - BLAZE_SPAWN_EGG( - 61, - "MONSTER_EGG"), - BLUE_BANNER( - 4, - "STANDING_BANNER", - "BANNER"), - BLUE_BED( - supports(12) ? 11 : 0, - "BED_BLOCK", - "BED"), - BLUE_CANDLE, - BLUE_CANDLE_CAKE, - BLUE_CARPET( - 11, - "CARPET"), - BLUE_CONCRETE( - 11, - "CONCRETE"), - BLUE_CONCRETE_POWDER( - 11, - "CONCRETE_POWDER"), - BLUE_DYE( - 4, - "INK_SACK", - "LAPIS_LAZULI"), - BLUE_GLAZED_TERRACOTTA, - BLUE_ICE, - BLUE_ORCHID( - 1, - "RED_ROSE"), - BLUE_SHULKER_BOX, - BLUE_STAINED_GLASS( - 11, - "STAINED_GLASS"), - BLUE_STAINED_GLASS_PANE( - 11, - "THIN_GLASS", - "STAINED_GLASS_PANE"), - BLUE_TERRACOTTA( - 11, - "STAINED_CLAY"), - BLUE_WALL_BANNER( - 4, - "WALL_BANNER"), - BLUE_WOOL( - 11, - "WOOL"), - BONE, - BONE_BLOCK, - BONE_MEAL( - 15, - "INK_SACK"), - BOOK, - BOOKSHELF, - BOW, - BOWL, - BRAIN_CORAL, - BRAIN_CORAL_BLOCK, - BRAIN_CORAL_FAN, - BRAIN_CORAL_WALL_FAN, - BREAD, - BREWING_STAND( - "BREWING_STAND", - "BREWING_STAND_ITEM"), - BRICK( - "CLAY_BRICK"), - BRICKS( - "BRICKS", - "BRICK"), - BRICK_SLAB( - 4, - "STEP"), - BRICK_STAIRS, - BRICK_WALL, - BROWN_BANNER( - 3, - "STANDING_BANNER", - "BANNER"), - BROWN_BED( - supports(12) ? 12 : 0, - "BED_BLOCK", - "BED"), - BROWN_CANDLE, - BROWN_CANDLE_CAKE, - BROWN_CARPET( - 12, - "CARPET"), - BROWN_CONCRETE( - 12, - "CONCRETE"), - BROWN_CONCRETE_POWDER( - 12, - "CONCRETE_POWDER"), - BROWN_DYE( - 3, - "INK_SACK", - "DYE", - "COCOA_BEANS"), - BROWN_GLAZED_TERRACOTTA, - BROWN_MUSHROOM, - BROWN_MUSHROOM_BLOCK( - "BROWN_MUSHROOM", - "HUGE_MUSHROOM_1"), - BROWN_SHULKER_BOX, - BROWN_STAINED_GLASS( - 12, - "STAINED_GLASS"), - BROWN_STAINED_GLASS_PANE( - 12, - "THIN_GLASS", - "STAINED_GLASS_PANE"), - BROWN_TERRACOTTA( - 12, - "STAINED_CLAY"), - BROWN_WALL_BANNER( - 3, - "WALL_BANNER"), - BROWN_WOOL( - 12, - "WOOL"), - BUBBLE_COLUMN, - BUBBLE_CORAL, - BUBBLE_CORAL_BLOCK, - BUBBLE_CORAL_FAN, - BUBBLE_CORAL_WALL_FAN, - BUCKET, - BUDDING_AMETHYST, - BUNDLE, - CACTUS, - CAKE( - "CAKE_BLOCK"), - CALCITE, - CAMPFIRE, - CANDLE, - CANDLE_CAKE, - CARROT( - "CARROT_ITEM"), - CARROTS( - "CARROT"), - CARROT_ON_A_STICK( - "CARROT_STICK"), - CARTOGRAPHY_TABLE, - CARVED_PUMPKIN, - CAT_SPAWN_EGG, - CAULDRON( - "CAULDRON", - "CAULDRON_ITEM"), - /** - * 1.13 tag is not added because it's the same thing as {@link #AIR} - * - * @see #VOID_AIR - */ - CAVE_AIR( - "AIR"), - CAVE_SPIDER_SPAWN_EGG( - 59, - "MONSTER_EGG"), - CAVE_VINES, - CAVE_VINES_PLANT, - CHAIN, - CHAINMAIL_BOOTS, - CHAINMAIL_CHESTPLATE, - CHAINMAIL_HELMET, - CHAINMAIL_LEGGINGS, - CHAIN_COMMAND_BLOCK( - "COMMAND", - "COMMAND_CHAIN"), - CHARCOAL( - 1, - "COAL"), - CHEST( - "LOCKED_CHEST"), - CHEST_MINECART( - "STORAGE_MINECART"), - CHICKEN( - "RAW_CHICKEN"), - CHICKEN_SPAWN_EGG( - 93, - "MONSTER_EGG"), - CHIPPED_ANVIL( - 1, - "ANVIL"), - CHISELED_DEEPSLATE, - CHISELED_NETHER_BRICKS( - 1, - "NETHER_BRICKS"), - CHISELED_POLISHED_BLACKSTONE( - "POLISHED_BLACKSTONE"), - CHISELED_QUARTZ_BLOCK( - 1, - "QUARTZ_BLOCK"), - CHISELED_RED_SANDSTONE( - 1, - "RED_SANDSTONE"), - CHISELED_SANDSTONE( - 1, - "SANDSTONE"), - CHISELED_STONE_BRICKS( - 3, - "SMOOTH_BRICK"), - CHORUS_FLOWER, - CHORUS_FRUIT, - CHORUS_PLANT, - CLAY, - CLAY_BALL, - CLOCK( - "WATCH"), - COAL, - COAL_BLOCK, - COAL_ORE, - COARSE_DIRT( - 1, - "DIRT"), - COBBLED_DEEPSLATE, - COBBLED_DEEPSLATE_SLAB, - COBBLED_DEEPSLATE_STAIRS, - COBBLED_DEEPSLATE_WALL, - COBBLESTONE, - COBBLESTONE_SLAB( - 3, - "STEP"), - COBBLESTONE_STAIRS, - COBBLESTONE_WALL( - "COBBLE_WALL"), - COBWEB( - "WEB"), - COCOA, - COCOA_BEANS( - 3, - "INK_SACK"), - COD( - "RAW_FISH"), - COD_BUCKET, - COD_SPAWN_EGG, - COMMAND_BLOCK( - "COMMAND"), - COMMAND_BLOCK_MINECART( - "COMMAND_MINECART"), - /** - * Unlike redstone torch and redstone lamp... neither REDTONE_COMPARATOR_OFF nor REDSTONE_COMPARATOR_ON - * are items. REDSTONE_COMPARATOR is. - * - * @see #REDSTONE_TORCH - * @see #REDSTONE_LAMP - */ - COMPARATOR( - "REDSTONE_COMPARATOR_OFF", - "REDSTONE_COMPARATOR_ON", - "REDSTONE_COMPARATOR"), - COMPASS, - COMPOSTER, - CONDUIT, - COOKED_BEEF, - COOKED_CHICKEN, - COOKED_COD( - "COOKED_FISH"), - COOKED_MUTTON, - COOKED_PORKCHOP( - "GRILLED_PORK"), - COOKED_RABBIT, - COOKED_SALMON( - 1, - "COOKED_FISH"), - COOKIE, - COPPER_BLOCK, - COPPER_INGOT, - COPPER_ORE, - CORNFLOWER, - COW_SPAWN_EGG( - 92, - "MONSTER_EGG"), - CRACKED_DEEPSLATE_BRICKS, - CRACKED_DEEPSLATE_TILES, - CRACKED_NETHER_BRICKS( - 2, - "NETHER_BRICKS"), - CRACKED_POLISHED_BLACKSTONE_BRICKS( - "POLISHED_BLACKSTONE_BRICKS"), - CRACKED_STONE_BRICKS( - 2, - "SMOOTH_BRICK"), - CRAFTING_TABLE( - "WORKBENCH"), - CREEPER_BANNER_PATTERN, - CREEPER_HEAD( - 4, - "SKULL", - "SKULL_ITEM"), - CREEPER_SPAWN_EGG( - 50, - "MONSTER_EGG"), - CREEPER_WALL_HEAD( - 4, - "SKULL", - "SKULL_ITEM"), - CRIMSON_BUTTON, - CRIMSON_DOOR, - CRIMSON_FENCE, - CRIMSON_FENCE_GATE, - CRIMSON_FUNGUS, - CRIMSON_HYPHAE, - CRIMSON_NYLIUM, - CRIMSON_PLANKS, - CRIMSON_PRESSURE_PLATE, - CRIMSON_ROOTS, - CRIMSON_SIGN( - "SIGN_POST"), - CRIMSON_SLAB, - CRIMSON_STAIRS, - CRIMSON_STEM, - CRIMSON_TRAPDOOR, - CRIMSON_WALL_SIGN( - "WALL_SIGN"), - CROSSBOW, - CRYING_OBSIDIAN, - CUT_COPPER, - CUT_COPPER_SLAB, - CUT_COPPER_STAIRS, - CUT_RED_SANDSTONE, - CUT_RED_SANDSTONE_SLAB( - "STONE_SLAB2"), - CUT_SANDSTONE, - CUT_SANDSTONE_SLAB( - 1, - "STEP"), - CYAN_BANNER( - 6, - "STANDING_BANNER", - "BANNER"), - CYAN_BED( - supports(12) ? 9 : 0, - "BED_BLOCK", - "BED"), - CYAN_CANDLE, - CYAN_CANDLE_CAKE, - CYAN_CARPET( - 9, - "CARPET"), - CYAN_CONCRETE( - 9, - "CONCRETE"), - CYAN_CONCRETE_POWDER( - 9, - "CONCRETE_POWDER"), - CYAN_DYE( - 6, - "INK_SACK"), - CYAN_GLAZED_TERRACOTTA, - CYAN_SHULKER_BOX, - CYAN_STAINED_GLASS( - 9, - "STAINED_GLASS"), - CYAN_STAINED_GLASS_PANE( - 9, - "STAINED_GLASS_PANE"), - CYAN_TERRACOTTA( - 9, - "STAINED_CLAY"), - CYAN_WALL_BANNER( - 6, - "WALL_BANNER"), - CYAN_WOOL( - 9, - "WOOL"), - DAMAGED_ANVIL( - 2, - "ANVIL"), - DANDELION( - "YELLOW_FLOWER"), - DARK_OAK_BOAT( - "BOAT_DARK_OAK"), - DARK_OAK_BUTTON( - "WOOD_BUTTON"), - DARK_OAK_DOOR( - "DARK_OAK_DOOR", - "DARK_OAK_DOOR_ITEM"), - DARK_OAK_FENCE, - DARK_OAK_FENCE_GATE, - DARK_OAK_LEAVES( - 1, - "LEAVES_2"), - DARK_OAK_LOG( - 1, - "LOG_2"), - DARK_OAK_PLANKS( - 5, - "WOOD"), - DARK_OAK_PRESSURE_PLATE( - "WOOD_PLATE"), - DARK_OAK_SAPLING( - 5, - "SAPLING"), - DARK_OAK_SIGN( - "SIGN_POST", - "SIGN"), - DARK_OAK_SLAB( - 5, - "WOOD_DOUBLE_STEP", - "WOOD_STEP", - "WOODEN_SLAB"), - DARK_OAK_STAIRS, - DARK_OAK_TRAPDOOR( - "TRAP_DOOR"), - DARK_OAK_WALL_SIGN( - "WALL_SIGN"), - DARK_OAK_WOOD( - 1, - "LOG_2"), - DARK_PRISMARINE( - 2, - "PRISMARINE"), - DARK_PRISMARINE_SLAB, - DARK_PRISMARINE_STAIRS, - DAYLIGHT_DETECTOR( - "DAYLIGHT_DETECTOR_INVERTED"), - DEAD_BRAIN_CORAL, - DEAD_BRAIN_CORAL_BLOCK, - DEAD_BRAIN_CORAL_FAN, - DEAD_BRAIN_CORAL_WALL_FAN, - DEAD_BUBBLE_CORAL, - DEAD_BUBBLE_CORAL_BLOCK, - DEAD_BUBBLE_CORAL_FAN, - DEAD_BUBBLE_CORAL_WALL_FAN, - DEAD_BUSH( - "LONG_GRASS"), - DEAD_FIRE_CORAL, - DEAD_FIRE_CORAL_BLOCK, - DEAD_FIRE_CORAL_FAN, - DEAD_FIRE_CORAL_WALL_FAN, - DEAD_HORN_CORAL, - DEAD_HORN_CORAL_BLOCK, - DEAD_HORN_CORAL_FAN, - DEAD_HORN_CORAL_WALL_FAN, - DEAD_TUBE_CORAL, - DEAD_TUBE_CORAL_BLOCK, - DEAD_TUBE_CORAL_FAN, - DEAD_TUBE_CORAL_WALL_FAN, - DEBUG_STICK, - DEEPSLATE, - DEEPSLATE_BRICKS, - DEEPSLATE_BRICK_SLAB, - DEEPSLATE_BRICK_STAIRS, - DEEPSLATE_BRICK_WALL, - DEEPSLATE_COAL_ORE, - DEEPSLATE_COPPER_ORE, - DEEPSLATE_DIAMOND_ORE, - DEEPSLATE_EMERALD_ORE, - DEEPSLATE_GOLD_ORE, - DEEPSLATE_IRON_ORE, - DEEPSLATE_LAPIS_ORE, - DEEPSLATE_REDSTONE_ORE, - DEEPSLATE_TILES, - DEEPSLATE_TILE_SLAB, - DEEPSLATE_TILE_STAIRS, - DEEPSLATE_TILE_WALL, - DETECTOR_RAIL, - DIAMOND, - DIAMOND_AXE, - DIAMOND_BLOCK, - DIAMOND_BOOTS, - DIAMOND_CHESTPLATE, - DIAMOND_HELMET, - DIAMOND_HOE, - DIAMOND_HORSE_ARMOR( - "DIAMOND_BARDING"), - DIAMOND_LEGGINGS, - DIAMOND_ORE, - DIAMOND_PICKAXE, - DIAMOND_SHOVEL( - "DIAMOND_SPADE"), - DIAMOND_SWORD, - DIORITE( - 3, - "STONE"), - DIORITE_SLAB, - DIORITE_STAIRS, - DIORITE_WALL, - DIRT, - /** - * Changed in 1.17 - */ - DIRT_PATH( - "GRASS_PATH"), - DISPENSER, - DOLPHIN_SPAWN_EGG, - DONKEY_SPAWN_EGG( - 32, - "MONSTER_EGG"), - DRAGON_BREATH( - "DRAGONS_BREATH"), - DRAGON_EGG, - DRAGON_HEAD( - 5, - "SKULL", - "SKULL_ITEM"), - DRAGON_WALL_HEAD( - 5, - "SKULL", - "SKULL_ITEM"), - DRIED_KELP, - DRIED_KELP_BLOCK, - DRIPSTONE_BLOCK, - DROPPER, - DROWNED_SPAWN_EGG, - EGG, - ELDER_GUARDIAN_SPAWN_EGG( - 4, - "MONSTER_EGG"), - ELYTRA, - EMERALD, - EMERALD_BLOCK, - EMERALD_ORE, - ENCHANTED_BOOK, - ENCHANTED_GOLDEN_APPLE( - 1, - "GOLDEN_APPLE"), - ENCHANTING_TABLE( - "ENCHANTMENT_TABLE"), - ENDERMAN_SPAWN_EGG( - 58, - "MONSTER_EGG"), - ENDERMITE_SPAWN_EGG( - 67, - "MONSTER_EGG"), - ENDER_CHEST, - ENDER_EYE( - "EYE_OF_ENDER"), - ENDER_PEARL, - END_CRYSTAL, - END_GATEWAY, - END_PORTAL( - "ENDER_PORTAL"), - END_PORTAL_FRAME( - "ENDER_PORTAL_FRAME"), - END_ROD, - END_STONE( - "ENDER_STONE"), - END_STONE_BRICKS( - "END_BRICKS"), - END_STONE_BRICK_SLAB, - END_STONE_BRICK_STAIRS, - END_STONE_BRICK_WALL, - EVOKER_SPAWN_EGG( - 34, - "MONSTER_EGG"), - EXPERIENCE_BOTTLE( - "EXP_BOTTLE"), - EXPOSED_COPPER, - EXPOSED_CUT_COPPER, - EXPOSED_CUT_COPPER_SLAB, - EXPOSED_CUT_COPPER_STAIRS, - FARMLAND( - "SOIL"), - FEATHER, - FERMENTED_SPIDER_EYE, - FERN( - 2, - "LONG_GRASS"), - /** - * For some reasons filled map items are really special. - * Their data value starts from 0 and every time a player - * creates a new map that maps data value increases. - * https://github.com/CryptoMorin/XSeries/issues/91 - */ - FILLED_MAP( - "MAP"), - FIRE, - FIREWORK_ROCKET( - "FIREWORK"), - FIREWORK_STAR( - "FIREWORK_CHARGE"), - FIRE_CHARGE( - "FIREBALL"), - FIRE_CORAL, - FIRE_CORAL_BLOCK, - FIRE_CORAL_FAN, - FIRE_CORAL_WALL_FAN, - FISHING_ROD, - FLETCHING_TABLE, - FLINT, - FLINT_AND_STEEL, - FLOWERING_AZALEA, - FLOWERING_AZALEA_LEAVES, - FLOWER_BANNER_PATTERN, - FLOWER_POT( - "FLOWER_POT", - "FLOWER_POT_ITEM"), - FOX_SPAWN_EGG, - /** - * This special material cannot be obtained as an item. - */ - FROSTED_ICE, - FURNACE( - "BURNING_FURNACE"), - FURNACE_MINECART( - "POWERED_MINECART"), - GHAST_SPAWN_EGG( - 56, - "MONSTER_EGG"), - GHAST_TEAR, - GILDED_BLACKSTONE, - GLASS, - GLASS_BOTTLE, - GLASS_PANE( - "THIN_GLASS"), - GLISTERING_MELON_SLICE( - "SPECKLED_MELON"), - GLOBE_BANNER_PATTERN, - GLOWSTONE, - GLOWSTONE_DUST, - GLOW_BERRIES, - GLOW_INK_SAC, - GLOW_ITEM_FRAME, - GLOW_LICHEN, - GLOW_SQUID_SPAWN_EGG, - GOAT_SPAWN_EGG, - GOLDEN_APPLE, - GOLDEN_AXE( - "GOLD_AXE"), - GOLDEN_BOOTS( - "GOLD_BOOTS"), - GOLDEN_CARROT, - GOLDEN_CHESTPLATE( - "GOLD_CHESTPLATE"), - GOLDEN_HELMET( - "GOLD_HELMET"), - GOLDEN_HOE( - "GOLD_HOE"), - GOLDEN_HORSE_ARMOR( - "GOLD_BARDING"), - GOLDEN_LEGGINGS( - "GOLD_LEGGINGS"), - GOLDEN_PICKAXE( - "GOLD_PICKAXE"), - GOLDEN_SHOVEL( - "GOLD_SPADE"), - GOLDEN_SWORD( - "GOLD_SWORD"), - GOLD_BLOCK, - GOLD_INGOT, - GOLD_NUGGET, - GOLD_ORE, - GRANITE( - 1, - "STONE"), - GRANITE_SLAB, - GRANITE_STAIRS, - GRANITE_WALL, - GRASS( - 1, - "LONG_GRASS"), - GRASS_BLOCK( - "GRASS"), - GRAVEL, - GRAY_BANNER( - 8, - "STANDING_BANNER", - "BANNER"), - GRAY_BED( - supports(12) ? 7 : 0, - "BED_BLOCK", - "BED"), - GRAY_CANDLE, - GRAY_CANDLE_CAKE, - GRAY_CARPET( - 7, - "CARPET"), - GRAY_CONCRETE( - 7, - "CONCRETE"), - GRAY_CONCRETE_POWDER( - 7, - "CONCRETE_POWDER"), - GRAY_DYE( - 8, - "INK_SACK"), - GRAY_GLAZED_TERRACOTTA, - GRAY_SHULKER_BOX, - GRAY_STAINED_GLASS( - 7, - "STAINED_GLASS"), - GRAY_STAINED_GLASS_PANE( - 7, - "THIN_GLASS", - "STAINED_GLASS_PANE"), - GRAY_TERRACOTTA( - 7, - "STAINED_CLAY"), - GRAY_WALL_BANNER( - 8, - "WALL_BANNER"), - GRAY_WOOL( - 7, - "WOOL"), - GREEN_BANNER( - 2, - "STANDING_BANNER", - "BANNER"), - GREEN_BED( - supports(12) ? 13 : 0, - "BED_BLOCK", - "BED"), - GREEN_CANDLE, - GREEN_CANDLE_CAKE, - GREEN_CARPET( - 13, - "CARPET"), - GREEN_CONCRETE( - 13, - "CONCRETE"), - GREEN_CONCRETE_POWDER( - 13, - "CONCRETE_POWDER"), - /** - * 1.13 renamed to CACTUS_GREEN - * 1.14 renamed to GREEN_DYE - */ - GREEN_DYE( - 2, - "INK_SACK", - "CACTUS_GREEN"), - GREEN_GLAZED_TERRACOTTA, - GREEN_SHULKER_BOX, - GREEN_STAINED_GLASS( - 13, - "STAINED_GLASS"), - GREEN_STAINED_GLASS_PANE( - 13, - "THIN_GLASS", - "STAINED_GLASS_PANE"), - GREEN_TERRACOTTA( - 13, - "STAINED_CLAY"), - GREEN_WALL_BANNER( - 2, - "WALL_BANNER"), - GREEN_WOOL( - 13, - "WOOL"), - GRINDSTONE, - GUARDIAN_SPAWN_EGG( - 68, - "MONSTER_EGG"), - GUNPOWDER( - "SULPHUR"), - HANGING_ROOTS, - HAY_BLOCK, - HEART_OF_THE_SEA, - HEAVY_WEIGHTED_PRESSURE_PLATE( - "IRON_PLATE"), - HOGLIN_SPAWN_EGG( - "MONSTER_EGG"), - HONEYCOMB, - HONEYCOMB_BLOCK, - HONEY_BLOCK, - HONEY_BOTTLE, - HOPPER, - HOPPER_MINECART, - HORN_CORAL, - HORN_CORAL_BLOCK, - HORN_CORAL_FAN, - HORN_CORAL_WALL_FAN, - HORSE_SPAWN_EGG( - 100, - "MONSTER_EGG"), - HUSK_SPAWN_EGG( - 23, - "MONSTER_EGG"), - ICE, - INFESTED_CHISELED_STONE_BRICKS( - 5, - "MONSTER_EGGS"), - INFESTED_COBBLESTONE( - 1, - "MONSTER_EGGS"), - INFESTED_CRACKED_STONE_BRICKS( - 4, - "MONSTER_EGGS"), - INFESTED_DEEPSLATE, - INFESTED_MOSSY_STONE_BRICKS( - 3, - "MONSTER_EGGS"), - INFESTED_STONE( - "MONSTER_EGGS"), - INFESTED_STONE_BRICKS( - 2, - "MONSTER_EGGS"), - /** - * We will only add "INK_SAC" for {@link #BLACK_DYE} since it's - * the only material (linked with this material) that is added - * after 1.13, which means it can use both INK_SACK and INK_SAC. - */ - INK_SAC( - "INK_SACK"), - IRON_AXE, - IRON_BARS( - "IRON_FENCE"), - IRON_BLOCK, - IRON_BOOTS, - IRON_CHESTPLATE, - IRON_DOOR( - "IRON_DOOR_BLOCK"), - IRON_HELMET, - IRON_HOE, - IRON_HORSE_ARMOR( - "IRON_BARDING"), - IRON_INGOT, - IRON_LEGGINGS, - IRON_NUGGET, - IRON_ORE, - IRON_PICKAXE, - IRON_SHOVEL( - "IRON_SPADE"), - IRON_SWORD, - IRON_TRAPDOOR, - ITEM_FRAME, - JACK_O_LANTERN, - JIGSAW, - JUKEBOX, - JUNGLE_BOAT( - "BOAT_JUNGLE"), - JUNGLE_BUTTON( - "WOOD_BUTTON"), - JUNGLE_DOOR( - "JUNGLE_DOOR", - "JUNGLE_DOOR_ITEM"), - JUNGLE_FENCE, - JUNGLE_FENCE_GATE, - JUNGLE_LEAVES( - 3, - "LEAVES"), - JUNGLE_LOG( - 3, - "LOG"), - JUNGLE_PLANKS( - 3, - "WOOD"), - JUNGLE_PRESSURE_PLATE( - "WOOD_PLATE"), - JUNGLE_SAPLING( - 3, - "SAPLING"), - JUNGLE_SIGN( - "SIGN_POST", - "SIGN"), - JUNGLE_SLAB( - 3, - "WOOD_DOUBLE_STEP", - "WOOD_STEP", - "WOODEN_SLAB"), - JUNGLE_STAIRS( - "JUNGLE_WOOD_STAIRS"), - JUNGLE_TRAPDOOR( - "TRAP_DOOR"), - JUNGLE_WALL_SIGN( - "WALL_SIGN"), - JUNGLE_WOOD( - 3, - "LOG"), - KELP, - KELP_PLANT, - KNOWLEDGE_BOOK( - "BOOK"), - LADDER, - LANTERN, - LAPIS_BLOCK, - LAPIS_LAZULI( - 4, - "INK_SACK"), - LAPIS_ORE, - LARGE_AMETHYST_BUD, - LARGE_FERN( - 3, - "DOUBLE_PLANT"), - LAVA( - "STATIONARY_LAVA"), - LAVA_BUCKET, - LAVA_CAULDRON, - LEAD( - "LEASH"), - LEATHER, - LEATHER_BOOTS, - LEATHER_CHESTPLATE, - LEATHER_HELMET, - LEATHER_HORSE_ARMOR( - "IRON_HORSE_ARMOR"), - LEATHER_LEGGINGS, - LECTERN, - LEVER, - LIGHT, - LIGHTNING_ROD, - LIGHT_BLUE_BANNER( - 12, - "STANDING_BANNER", - "BANNER"), - LIGHT_BLUE_BED( - supports(12) ? 3 : 0, - "BED_BLOCK", - "BED"), - LIGHT_BLUE_CANDLE, - LIGHT_BLUE_CANDLE_CAKE, - LIGHT_BLUE_CARPET( - 3, - "CARPET"), - LIGHT_BLUE_CONCRETE( - 3, - "CONCRETE"), - LIGHT_BLUE_CONCRETE_POWDER( - 3, - "CONCRETE_POWDER"), - LIGHT_BLUE_DYE( - 12, - "INK_SACK"), - LIGHT_BLUE_GLAZED_TERRACOTTA, - LIGHT_BLUE_SHULKER_BOX, - LIGHT_BLUE_STAINED_GLASS( - 3, - "STAINED_GLASS"), - LIGHT_BLUE_STAINED_GLASS_PANE( - 3, - "THIN_GLASS", - "STAINED_GLASS_PANE"), - LIGHT_BLUE_TERRACOTTA( - 3, - "STAINED_CLAY"), - LIGHT_BLUE_WALL_BANNER( - 12, - "WALL_BANNER", - "STANDING_BANNER", - "BANNER"), - LIGHT_BLUE_WOOL( - 3, - "WOOL"), - LIGHT_GRAY_BANNER( - 7, - "STANDING_BANNER", - "BANNER"), - LIGHT_GRAY_BED( - supports(12) ? 8 : 0, - "BED_BLOCK", - "BED"), - LIGHT_GRAY_CANDLE, - LIGHT_GRAY_CANDLE_CAKE, - LIGHT_GRAY_CARPET( - 8, - "CARPET"), - LIGHT_GRAY_CONCRETE( - 8, - "CONCRETE"), - LIGHT_GRAY_CONCRETE_POWDER( - 8, - "CONCRETE_POWDER"), - LIGHT_GRAY_DYE( - 7, - "INK_SACK"), - /** - * Renamed to SILVER_GLAZED_TERRACOTTA in 1.12 - * Renamed to LIGHT_GRAY_GLAZED_TERRACOTTA in 1.14 - */ - LIGHT_GRAY_GLAZED_TERRACOTTA( - "SILVER_GLAZED_TERRACOTTA"), - LIGHT_GRAY_SHULKER_BOX( - "SILVER_SHULKER_BOX"), - LIGHT_GRAY_STAINED_GLASS( - 8, - "STAINED_GLASS"), - LIGHT_GRAY_STAINED_GLASS_PANE( - 8, - "THIN_GLASS", - "STAINED_GLASS_PANE"), - LIGHT_GRAY_TERRACOTTA( - 8, - "STAINED_CLAY"), - LIGHT_GRAY_WALL_BANNER( - 7, - "WALL_BANNER"), - LIGHT_GRAY_WOOL( - 8, - "WOOL"), - LIGHT_WEIGHTED_PRESSURE_PLATE( - "GOLD_PLATE"), - LILAC( - 1, - "DOUBLE_PLANT"), - LILY_OF_THE_VALLEY, - LILY_PAD( - "WATER_LILY"), - LIME_BANNER( - 10, - "STANDING_BANNER", - "BANNER"), - LIME_BED( - supports(12) ? 5 : 0, - "BED_BLOCK", - "BED"), - LIME_CANDLE, - LIME_CANDLE_CAKE, - LIME_CARPET( - 5, - "CARPET"), - LIME_CONCRETE( - 5, - "CONCRETE"), - LIME_CONCRETE_POWDER( - 5, - "CONCRETE_POWDER"), - LIME_DYE( - 10, - "INK_SACK"), - LIME_GLAZED_TERRACOTTA, - LIME_SHULKER_BOX, - LIME_STAINED_GLASS( - 5, - "STAINED_GLASS"), - LIME_STAINED_GLASS_PANE( - 5, - "STAINED_GLASS_PANE"), - LIME_TERRACOTTA( - 5, - "STAINED_CLAY"), - LIME_WALL_BANNER( - 10, - "WALL_BANNER"), - LIME_WOOL( - 5, - "WOOL"), - LINGERING_POTION, - LLAMA_SPAWN_EGG( - 103, - "MONSTER_EGG"), - LODESTONE, - LOOM, - MAGENTA_BANNER( - 13, - "STANDING_BANNER", - "BANNER"), - MAGENTA_BED( - supports(12) ? 2 : 0, - "BED_BLOCK", - "BED"), - MAGENTA_CANDLE, - MAGENTA_CANDLE_CAKE, - MAGENTA_CARPET( - 2, - "CARPET"), - MAGENTA_CONCRETE( - 2, - "CONCRETE"), - MAGENTA_CONCRETE_POWDER( - 2, - "CONCRETE_POWDER"), - MAGENTA_DYE( - 13, - "INK_SACK"), - MAGENTA_GLAZED_TERRACOTTA, - MAGENTA_SHULKER_BOX, - MAGENTA_STAINED_GLASS( - 2, - "STAINED_GLASS"), - MAGENTA_STAINED_GLASS_PANE( - 2, - "THIN_GLASS", - "STAINED_GLASS_PANE"), - MAGENTA_TERRACOTTA( - 2, - "STAINED_CLAY"), - MAGENTA_WALL_BANNER( - 13, - "WALL_BANNER"), - MAGENTA_WOOL( - 2, - "WOOL"), - MAGMA_BLOCK( - "MAGMA"), - MAGMA_CREAM, - MAGMA_CUBE_SPAWN_EGG( - 62, - "MONSTER_EGG"), - /** - * Adding this to the duplicated list will give you a filled map - * for 1.13+ versions and removing it from duplicated list will - * still give you a filled map in -1.12 versions. - * Since higher versions are our priority I'll keep 1.13+ support - * until I can come up with something to fix it. - */ - MAP( - "EMPTY_MAP"), - MEDIUM_AMETHYST_BUD, - MELON( - "MELON_BLOCK"), - MELON_SEEDS, - MELON_SLICE( - "MELON"), - MELON_STEM, - MILK_BUCKET, - MINECART, - MOJANG_BANNER_PATTERN, - MOOSHROOM_SPAWN_EGG( - 96, - "MONSTER_EGG"), - MOSSY_COBBLESTONE, - MOSSY_COBBLESTONE_SLAB(), - MOSSY_COBBLESTONE_STAIRS, - MOSSY_COBBLESTONE_WALL( - 1, - "COBBLE_WALL", - "COBBLESTONE_WALL"), - MOSSY_STONE_BRICKS( - 1, - "SMOOTH_BRICK"), - MOSSY_STONE_BRICK_SLAB, - MOSSY_STONE_BRICK_STAIRS, - MOSSY_STONE_BRICK_WALL, - MOSS_BLOCK, - MOSS_CARPET, - MOVING_PISTON( - "PISTON_MOVING_PIECE"), - MULE_SPAWN_EGG( - 32, - "MONSTER_EGG"), - MUSHROOM_STEM( - "BROWN_MUSHROOM"), - MUSHROOM_STEW( - "MUSHROOM_SOUP"), - MUSIC_DISC_11( - "GOLD_RECORD"), - MUSIC_DISC_13( - "GREEN_RECORD"), - MUSIC_DISC_BLOCKS( - "RECORD_3"), - MUSIC_DISC_CAT( - "RECORD_4"), - MUSIC_DISC_CHIRP( - "RECORD_5"), - MUSIC_DISC_FAR( - "RECORD_6"), - MUSIC_DISC_MALL( - "RECORD_7"), - MUSIC_DISC_MELLOHI( - "RECORD_8"), - MUSIC_DISC_OTHERSIDE, - MUSIC_DISC_PIGSTEP, - MUSIC_DISC_STAL( - "RECORD_9"), - MUSIC_DISC_STRAD( - "RECORD_10"), - MUSIC_DISC_WAIT( - "RECORD_11"), - MUSIC_DISC_WARD( - "RECORD_12"), - MUTTON, - MYCELIUM( - "MYCEL"), - NAME_TAG, - NAUTILUS_SHELL, - NETHERITE_AXE, - NETHERITE_BLOCK, - NETHERITE_BOOTS, - NETHERITE_CHESTPLATE, - NETHERITE_HELMET, - NETHERITE_HOE, - NETHERITE_INGOT, - NETHERITE_LEGGINGS, - NETHERITE_PICKAXE, - NETHERITE_SCRAP, - NETHERITE_SHOVEL, - NETHERITE_SWORD, - NETHERRACK, - NETHER_BRICK( - "NETHER_BRICK_ITEM"), - NETHER_BRICKS( - "NETHER_BRICK"), - NETHER_BRICK_FENCE( - "NETHER_FENCE"), - NETHER_BRICK_SLAB( - 6, - "STEP"), - NETHER_BRICK_STAIRS, - NETHER_BRICK_WALL, - NETHER_GOLD_ORE, - NETHER_PORTAL( - "PORTAL"), - NETHER_QUARTZ_ORE( - "QUARTZ_ORE"), - NETHER_SPROUTS, - NETHER_STAR, - /** - * Just like mentioned in https://minecraft.gamepedia.com/Nether_Wart - * Nether wart is also known as nether stalk in the code. - * NETHER_STALK is the planted state of nether warts. - */ - NETHER_WART( - "NETHER_WARTS", - "NETHER_STALK"), - NETHER_WART_BLOCK, - NOTE_BLOCK, - OAK_BOAT( - "BOAT"), - OAK_BUTTON( - "WOOD_BUTTON"), - OAK_DOOR( - "WOODEN_DOOR", - "WOOD_DOOR"), - OAK_FENCE( - "FENCE"), - OAK_FENCE_GATE( - "FENCE_GATE"), - OAK_LEAVES( - "LEAVES"), - OAK_LOG( - "LOG"), - OAK_PLANKS( - "WOOD"), - OAK_PRESSURE_PLATE( - "WOOD_PLATE"), - OAK_SAPLING( - "SAPLING"), - OAK_SIGN( - "SIGN_POST", - "SIGN"), - OAK_SLAB( - "WOOD_DOUBLE_STEP", - "WOOD_STEP", - "WOODEN_SLAB"), - OAK_STAIRS( - "WOOD_STAIRS"), - OAK_TRAPDOOR( - "TRAP_DOOR"), - OAK_WALL_SIGN( - "WALL_SIGN"), - OAK_WOOD( - "LOG"), - OBSERVER, - OBSIDIAN, - OCELOT_SPAWN_EGG( - 98, - "MONSTER_EGG"), - ORANGE_BANNER( - 14, - "STANDING_BANNER", - "BANNER"), - ORANGE_BED( - supports(12) ? 1 : 0, - "BED_BLOCK", - "BED"), - ORANGE_CANDLE, - ORANGE_CANDLE_CAKE, - ORANGE_CARPET( - 1, - "CARPET"), - ORANGE_CONCRETE( - 1, - "CONCRETE"), - ORANGE_CONCRETE_POWDER( - 1, - "CONCRETE_POWDER"), - ORANGE_DYE( - 14, - "INK_SACK"), - ORANGE_GLAZED_TERRACOTTA, - ORANGE_SHULKER_BOX, - ORANGE_STAINED_GLASS( - 1, - "STAINED_GLASS"), - ORANGE_STAINED_GLASS_PANE( - 1, - "STAINED_GLASS_PANE"), - ORANGE_TERRACOTTA( - 1, - "STAINED_CLAY"), - ORANGE_TULIP( - 5, - "RED_ROSE"), - ORANGE_WALL_BANNER( - 14, - "WALL_BANNER"), - ORANGE_WOOL( - 1, - "WOOL"), - OXEYE_DAISY( - 8, - "RED_ROSE"), - OXIDIZED_COPPER, - OXIDIZED_CUT_COPPER, - OXIDIZED_CUT_COPPER_SLAB, - OXIDIZED_CUT_COPPER_STAIRS, - PACKED_ICE, - PAINTING, - PANDA_SPAWN_EGG, - PAPER, - PARROT_SPAWN_EGG( - 105, - "MONSTER_EGG"), - PEONY( - 5, - "DOUBLE_PLANT"), - PETRIFIED_OAK_SLAB( - "WOOD_STEP"), - PHANTOM_MEMBRANE, - PHANTOM_SPAWN_EGG, - PIGLIN_BANNER_PATTERN, - PIGLIN_BRUTE_SPAWN_EGG, - PIGLIN_SPAWN_EGG( - 57, - "MONSTER_EGG"), - PIG_SPAWN_EGG( - 90, - "MONSTER_EGG"), - PILLAGER_SPAWN_EGG, - PINK_BANNER( - 9, - "STANDING_BANNER", - "BANNER"), - PINK_BED( - supports(12) ? 6 : 0, - "BED_BLOCK", - "BED"), - PINK_CANDLE, - PINK_CANDLE_CAKE, - PINK_CARPET( - 6, - "CARPET"), - PINK_CONCRETE( - 6, - "CONCRETE"), - PINK_CONCRETE_POWDER( - 6, - "CONCRETE_POWDER"), - PINK_DYE( - 9, - "INK_SACK"), - PINK_GLAZED_TERRACOTTA, - PINK_SHULKER_BOX, - PINK_STAINED_GLASS( - 6, - "STAINED_GLASS"), - PINK_STAINED_GLASS_PANE( - 6, - "THIN_GLASS", - "STAINED_GLASS_PANE"), - PINK_TERRACOTTA( - 6, - "STAINED_CLAY"), - PINK_TULIP( - 7, - "RED_ROSE"), - PINK_WALL_BANNER( - 9, - "WALL_BANNER"), - PINK_WOOL( - 6, - "WOOL"), - PISTON( - "PISTON_BASE"), - PISTON_HEAD( - "PISTON_EXTENSION"), - PLAYER_HEAD( - 3, - "SKULL", - "SKULL_ITEM"), - PLAYER_WALL_HEAD( - 3, - "SKULL", - "SKULL_ITEM"), - PODZOL( - 2, - "DIRT"), - POINTED_DRIPSTONE, - POISONOUS_POTATO, - POLAR_BEAR_SPAWN_EGG( - 102, - "MONSTER_EGG"), - POLISHED_ANDESITE( - 6, - "STONE"), - POLISHED_ANDESITE_SLAB, - POLISHED_ANDESITE_STAIRS, - POLISHED_BASALT, - POLISHED_BLACKSTONE, - POLISHED_BLACKSTONE_BRICKS, - POLISHED_BLACKSTONE_BRICK_SLAB, - POLISHED_BLACKSTONE_BRICK_STAIRS, - POLISHED_BLACKSTONE_BRICK_WALL, - POLISHED_BLACKSTONE_BUTTON, - POLISHED_BLACKSTONE_PRESSURE_PLATE, - POLISHED_BLACKSTONE_SLAB, - POLISHED_BLACKSTONE_STAIRS, - POLISHED_BLACKSTONE_WALL, - POLISHED_DEEPSLATE, - POLISHED_DEEPSLATE_SLAB, - POLISHED_DEEPSLATE_STAIRS, - POLISHED_DEEPSLATE_WALL, - POLISHED_DIORITE( - 4, - "STONE"), - POLISHED_DIORITE_SLAB, - POLISHED_DIORITE_STAIRS, - POLISHED_GRANITE( - 2, - "STONE"), - POLISHED_GRANITE_SLAB, - POLISHED_GRANITE_STAIRS, - POPPED_CHORUS_FRUIT( - "CHORUS_FRUIT_POPPED"), - POPPY( - "RED_ROSE"), - PORKCHOP( - "PORK"), - POTATO( - "POTATO_ITEM"), - POTATOES( - "POTATO"), - POTION, - POTTED_ACACIA_SAPLING( - 4, - "FLOWER_POT"), - POTTED_ALLIUM( - 2, - "RED_ROSE", - "FLOWER_POT"), - POTTED_AZALEA_BUSH, - POTTED_AZURE_BLUET( - 3, - "RED_ROSE", - "FLOWER_POT"), - POTTED_BAMBOO, - POTTED_BIRCH_SAPLING( - 2, - "FLOWER_POT"), - POTTED_BLUE_ORCHID( - 1, - "RED_ROSE", - "FLOWER_POT"), - POTTED_BROWN_MUSHROOM( - "FLOWER_POT"), - POTTED_CACTUS( - "FLOWER_POT"), - POTTED_CORNFLOWER, - POTTED_CRIMSON_FUNGUS, - POTTED_CRIMSON_ROOTS, - POTTED_DANDELION( - "YELLOW_FLOWER", - "FLOWER_POT"), - POTTED_DARK_OAK_SAPLING( - 5, - "FLOWER_POT"), - POTTED_DEAD_BUSH( - "FLOWER_POT"), - POTTED_FERN( - 2, - "LONG_GRASS", - "FLOWER_POT"), - POTTED_FLOWERING_AZALEA_BUSH, - POTTED_JUNGLE_SAPLING( - 3, - "FLOWER_POT"), - POTTED_LILY_OF_THE_VALLEY, - POTTED_OAK_SAPLING( - "FLOWER_POT"), - POTTED_ORANGE_TULIP( - 5, - "RED_ROSE", - "FLOWER_POT"), - POTTED_OXEYE_DAISY( - 8, - "RED_ROSE", - "FLOWER_POT"), - POTTED_PINK_TULIP( - 7, - "RED_ROSE", - "FLOWER_POT"), - POTTED_POPPY( - "RED_ROSE", - "FLOWER_POT"), - POTTED_RED_MUSHROOM( - "FLOWER_POT"), - POTTED_RED_TULIP( - 4, - "RED_ROSE", - "FLOWER_POT"), - POTTED_SPRUCE_SAPLING( - 1, - "FLOWER_POT"), - POTTED_WARPED_FUNGUS, - POTTED_WARPED_ROOTS, - POTTED_WHITE_TULIP( - 6, - "RED_ROSE", - "FLOWER_POT"), - POTTED_WITHER_ROSE, - POWDER_SNOW, - POWDER_SNOW_BUCKET, - POWDER_SNOW_CAULDRON, - POWERED_RAIL, - PRISMARINE, - PRISMARINE_BRICKS( - 1, - "PRISMARINE"), - PRISMARINE_BRICK_SLAB, - PRISMARINE_BRICK_STAIRS, - PRISMARINE_CRYSTALS, - PRISMARINE_SHARD, - PRISMARINE_SLAB, - PRISMARINE_STAIRS, - PRISMARINE_WALL, - PUFFERFISH( - 3, - "RAW_FISH"), - PUFFERFISH_BUCKET, - PUFFERFISH_SPAWN_EGG, - PUMPKIN, - PUMPKIN_PIE, - PUMPKIN_SEEDS, - PUMPKIN_STEM, - PURPLE_BANNER( - 5, - "STANDING_BANNER", - "BANNER"), - PURPLE_BED( - supports(12) ? 10 : 0, - "BED_BLOCK", - "BED"), - PURPLE_CANDLE, - PURPLE_CANDLE_CAKE, - PURPLE_CARPET( - 10, - "CARPET"), - PURPLE_CONCRETE( - 10, - "CONCRETE"), - PURPLE_CONCRETE_POWDER( - 10, - "CONCRETE_POWDER"), - PURPLE_DYE( - 5, - "INK_SACK"), - PURPLE_GLAZED_TERRACOTTA, - PURPLE_SHULKER_BOX, - PURPLE_STAINED_GLASS( - 10, - "STAINED_GLASS"), - PURPLE_STAINED_GLASS_PANE( - 10, - "THIN_GLASS", - "STAINED_GLASS_PANE"), - PURPLE_TERRACOTTA( - 10, - "STAINED_CLAY"), - PURPLE_WALL_BANNER( - 5, - "WALL_BANNER"), - PURPLE_WOOL( - 10, - "WOOL"), - PURPUR_BLOCK, - PURPUR_PILLAR, - PURPUR_SLAB( - "PURPUR_DOUBLE_SLAB"), - PURPUR_STAIRS, - QUARTZ, - QUARTZ_BLOCK, - QUARTZ_BRICKS, - QUARTZ_PILLAR( - 2, - "QUARTZ_BLOCK"), - QUARTZ_SLAB( - 7, - "STEP"), - QUARTZ_STAIRS, - RABBIT, - RABBIT_FOOT, - RABBIT_HIDE, - RABBIT_SPAWN_EGG( - 101, - "MONSTER_EGG"), - RABBIT_STEW, - RAIL( - "RAILS"), - RAVAGER_SPAWN_EGG, - RAW_COPPER, - RAW_COPPER_BLOCK, - RAW_GOLD, - RAW_GOLD_BLOCK, - RAW_IRON, - RAW_IRON_BLOCK, - REDSTONE, - REDSTONE_BLOCK, - /** - * Unlike redstone torch, REDSTONE_LAMP_ON isn't an item. - * The name is just here on the list for matching. - * - * @see #REDSTONE_TORCH - */ - REDSTONE_LAMP( - "REDSTONE_LAMP_ON", - "REDSTONE_LAMP_OFF"), - REDSTONE_ORE( - "GLOWING_REDSTONE_ORE"), - /** - * REDSTONE_TORCH_OFF isn't an item, but a block. - * But REDSTONE_TORCH_ON is the item. - * The name is just here on the list for matching. - */ - REDSTONE_TORCH( - "REDSTONE_TORCH_OFF", - "REDSTONE_TORCH_ON"), - REDSTONE_WALL_TORCH, - REDSTONE_WIRE, - RED_BANNER( - 1, - "STANDING_BANNER", - "BANNER"), - /** - * Data value 14 or 0 - */ - RED_BED( - supports(12) ? 14 : 0, - "BED_BLOCK", - "BED"), - RED_CANDLE, - RED_CANDLE_CAKE, - RED_CARPET( - 14, - "CARPET"), - RED_CONCRETE( - 14, - "CONCRETE"), - RED_CONCRETE_POWDER( - 14, - "CONCRETE_POWDER"), - RED_DYE( - 1, - "INK_SACK", - "ROSE_RED"), - RED_GLAZED_TERRACOTTA, - RED_MUSHROOM, - RED_MUSHROOM_BLOCK( - "RED_MUSHROOM", - "HUGE_MUSHROOM_2"), - RED_NETHER_BRICKS( - "RED_NETHER_BRICK"), - RED_NETHER_BRICK_SLAB, - RED_NETHER_BRICK_STAIRS, - RED_NETHER_BRICK_WALL, - RED_SAND( - 1, - "SAND"), - RED_SANDSTONE, - RED_SANDSTONE_SLAB( - "DOUBLE_STONE_SLAB2", - "STONE_SLAB2"), - RED_SANDSTONE_STAIRS, - RED_SANDSTONE_WALL, - RED_SHULKER_BOX, - RED_STAINED_GLASS( - 14, - "STAINED_GLASS"), - RED_STAINED_GLASS_PANE( - 14, - "THIN_GLASS", - "STAINED_GLASS_PANE"), - RED_TERRACOTTA( - 14, - "STAINED_CLAY"), - RED_TULIP( - 4, - "RED_ROSE"), - RED_WALL_BANNER( - 1, - "WALL_BANNER"), - RED_WOOL( - 14, - "WOOL"), - REPEATER( - "DIODE_BLOCK_ON", - "DIODE_BLOCK_OFF", - "DIODE"), - REPEATING_COMMAND_BLOCK( - "COMMAND", - "COMMAND_REPEATING"), - RESPAWN_ANCHOR, - ROOTED_DIRT, - ROSE_BUSH( - 4, - "DOUBLE_PLANT"), - ROTTEN_FLESH, - SADDLE, - SALMON( - 1, - "RAW_FISH"), - SALMON_BUCKET, - SALMON_SPAWN_EGG, - SAND, - SANDSTONE, - SANDSTONE_SLAB( - 1, - "DOUBLE_STEP", - "STEP", - "STONE_SLAB"), - SANDSTONE_STAIRS, - SANDSTONE_WALL, - SCAFFOLDING, - SCULK_SENSOR, - SCUTE, - SEAGRASS, - SEA_LANTERN, - SEA_PICKLE, - SHEARS, - SHEEP_SPAWN_EGG( - 91, - "MONSTER_EGG"), - SHIELD, - SHROOMLIGHT, - SHULKER_BOX( - "PURPLE_SHULKER_BOX"), - SHULKER_SHELL, - SHULKER_SPAWN_EGG( - 69, - "MONSTER_EGG"), - SILVERFISH_SPAWN_EGG( - 60, - "MONSTER_EGG"), - SKELETON_HORSE_SPAWN_EGG( - 28, - "MONSTER_EGG"), - SKELETON_SKULL( - "SKULL", - "SKULL_ITEM"), - SKELETON_SPAWN_EGG( - 51, - "MONSTER_EGG"), - SKELETON_WALL_SKULL( - "SKULL", - "SKULL_ITEM"), - SKULL_BANNER_PATTERN, - SLIME_BALL, - SLIME_BLOCK, - SLIME_SPAWN_EGG( - 55, - "MONSTER_EGG"), - SMALL_AMETHYST_BUD, - SMALL_DRIPLEAF, - SMITHING_TABLE, - SMOKER, - SMOOTH_BASALT, - SMOOTH_QUARTZ, - SMOOTH_QUARTZ_SLAB, - SMOOTH_QUARTZ_STAIRS, - SMOOTH_RED_SANDSTONE( - 2, - "RED_SANDSTONE"), - SMOOTH_RED_SANDSTONE_SLAB( - "STONE_SLAB2"), - SMOOTH_RED_SANDSTONE_STAIRS, - SMOOTH_SANDSTONE( - 2, - "SANDSTONE"), - SMOOTH_SANDSTONE_SLAB, - SMOOTH_SANDSTONE_STAIRS, - SMOOTH_STONE, - SMOOTH_STONE_SLAB, - SNOW, - SNOWBALL( - "SNOW_BALL"), - SNOW_BLOCK, - SOUL_CAMPFIRE, - SOUL_FIRE, - SOUL_LANTERN, - SOUL_SAND, - SOUL_SOIL, - SOUL_TORCH, - SOUL_WALL_TORCH, - SPAWNER( - "MOB_SPAWNER"), - SPECTRAL_ARROW, - SPIDER_EYE, - SPIDER_SPAWN_EGG( - 52, - "MONSTER_EGG"), - SPLASH_POTION, - SPONGE, - SPORE_BLOSSOM, - SPRUCE_BOAT( - "BOAT_SPRUCE"), - SPRUCE_BUTTON( - "WOOD_BUTTON"), - SPRUCE_DOOR( - "SPRUCE_DOOR", - "SPRUCE_DOOR_ITEM"), - SPRUCE_FENCE, - SPRUCE_FENCE_GATE, - SPRUCE_LEAVES( - 1, - "LEAVES"), - SPRUCE_LOG( - 1, - "LOG"), - SPRUCE_PLANKS( - 1, - "WOOD"), - SPRUCE_PRESSURE_PLATE( - "WOOD_PLATE"), - SPRUCE_SAPLING( - 1, - "SAPLING"), - SPRUCE_SIGN( - "SIGN_POST", - "SIGN"), - SPRUCE_SLAB( - 1, - "WOOD_DOUBLE_STEP", - "WOOD_STEP", - "WOODEN_SLAB"), - SPRUCE_STAIRS( - "SPRUCE_WOOD_STAIRS"), - SPRUCE_TRAPDOOR( - "TRAP_DOOR"), - SPRUCE_WALL_SIGN( - "WALL_SIGN"), - SPRUCE_WOOD( - 1, - "LOG"), - SPYGLASS, - SQUID_SPAWN_EGG( - 94, - "MONSTER_EGG"), - STICK, - STICKY_PISTON( - "PISTON_BASE", - "PISTON_STICKY_BASE"), - STONE, - STONECUTTER, - STONE_AXE, - STONE_BRICKS( - "SMOOTH_BRICK"), - STONE_BRICK_SLAB( - 5, - "DOUBLE_STEP", - "STEP", - "STONE_SLAB"), - STONE_BRICK_STAIRS( - "SMOOTH_STAIRS"), - STONE_BRICK_WALL, - STONE_BUTTON, - STONE_HOE, - STONE_PICKAXE, - STONE_PRESSURE_PLATE( - "STONE_PLATE"), - STONE_SHOVEL( - "STONE_SPADE"), - STONE_SLAB( - "DOUBLE_STEP", - "STEP"), - STONE_STAIRS, - STONE_SWORD, - STRAY_SPAWN_EGG( - 6, - "MONSTER_EGG"), - STRIDER_SPAWN_EGG, - STRING, - STRIPPED_ACACIA_LOG( - "LOG_2"), - STRIPPED_ACACIA_WOOD( - "LOG_2"), - STRIPPED_BIRCH_LOG( - 2, - "LOG"), - STRIPPED_BIRCH_WOOD( - 2, - "LOG"), - STRIPPED_CRIMSON_HYPHAE, - STRIPPED_CRIMSON_STEM, - STRIPPED_DARK_OAK_LOG( - "LOG"), - STRIPPED_DARK_OAK_WOOD( - "LOG"), - STRIPPED_JUNGLE_LOG( - 3, - "LOG"), - STRIPPED_JUNGLE_WOOD( - 3, - "LOG"), - STRIPPED_OAK_LOG( - "LOG"), - STRIPPED_OAK_WOOD( - "LOG"), - STRIPPED_SPRUCE_LOG( - 1, - "LOG"), - STRIPPED_SPRUCE_WOOD( - 1, - "LOG"), - STRIPPED_WARPED_HYPHAE, - STRIPPED_WARPED_STEM, - STRUCTURE_BLOCK, - /** - * Originally developers used barrier blocks for its purpose. - * So technically this isn't really considered as a suggested material. - */ - STRUCTURE_VOID( - 10, - "BARRIER"), - SUGAR, - /** - * Sugar Cane is a known material in pre-1.13 - */ - SUGAR_CANE( - "SUGAR_CANE_BLOCK"), - SUNFLOWER( - "DOUBLE_PLANT"), - SUSPICIOUS_STEW, - SWEET_BERRIES, - SWEET_BERRY_BUSH, - TALL_GRASS( - 2, - "DOUBLE_PLANT"), - TALL_SEAGRASS, - TARGET, - TERRACOTTA( - "HARD_CLAY"), - TINTED_GLASS, - TIPPED_ARROW, - TNT, - TNT_MINECART( - "EXPLOSIVE_MINECART"), - TORCH, - TOTEM_OF_UNDYING( - "TOTEM"), - TRADER_LLAMA_SPAWN_EGG, - TRAPPED_CHEST, - TRIDENT, - TRIPWIRE, - TRIPWIRE_HOOK, - TROPICAL_FISH( - 2, - "RAW_FISH"), - TROPICAL_FISH_BUCKET( - "BUCKET", - "WATER_BUCKET"), - TROPICAL_FISH_SPAWN_EGG( - "MONSTER_EGG"), - TUBE_CORAL, - TUBE_CORAL_BLOCK, - TUBE_CORAL_FAN, - TUBE_CORAL_WALL_FAN, - TUFF, - TURTLE_EGG, - TURTLE_HELMET, - TURTLE_SPAWN_EGG, - TWISTING_VINES, - TWISTING_VINES_PLANT, - VEX_SPAWN_EGG( - 35, - "MONSTER_EGG"), - VILLAGER_SPAWN_EGG( - 120, - "MONSTER_EGG"), - VINDICATOR_SPAWN_EGG( - 36, - "MONSTER_EGG"), - VINE, - /** - * 1.13 tag is not added because it's the same thing as {@link #AIR} - * - * @see #CAVE_AIR - */ - VOID_AIR( - "AIR"), - WALL_TORCH( - "TORCH"), - WANDERING_TRADER_SPAWN_EGG, - WARPED_BUTTON, - WARPED_DOOR, - WARPED_FENCE, - WARPED_FENCE_GATE, - WARPED_FUNGUS, - WARPED_FUNGUS_ON_A_STICK, - WARPED_HYPHAE, - WARPED_NYLIUM, - WARPED_PLANKS, - WARPED_PRESSURE_PLATE, - WARPED_ROOTS, - WARPED_SIGN( - "SIGN_POST"), - WARPED_SLAB, - WARPED_STAIRS, - WARPED_STEM, - WARPED_TRAPDOOR, - WARPED_WALL_SIGN( - "WALL_SIGN"), - WARPED_WART_BLOCK, - /** - * This is used for blocks only. - * In 1.13- WATER will turn into STATIONARY_WATER after it finished spreading. - * After 1.13+ this uses - * https://hub.spigotmc.org/javadocs/spigot/org/bukkit/block/data/Levelled.html water flowing system. - */ - WATER( - "STATIONARY_WATER"), - WATER_BUCKET, - WATER_CAULDRON, - WAXED_COPPER_BLOCK, - WAXED_CUT_COPPER, - WAXED_CUT_COPPER_SLAB, - WAXED_CUT_COPPER_STAIRS, - WAXED_EXPOSED_COPPER, - WAXED_EXPOSED_CUT_COPPER, - WAXED_EXPOSED_CUT_COPPER_SLAB, - WAXED_EXPOSED_CUT_COPPER_STAIRS, - WAXED_OXIDIZED_COPPER, - WAXED_OXIDIZED_CUT_COPPER, - WAXED_OXIDIZED_CUT_COPPER_SLAB, - WAXED_OXIDIZED_CUT_COPPER_STAIRS, - WAXED_WEATHERED_COPPER, - WAXED_WEATHERED_CUT_COPPER, - WAXED_WEATHERED_CUT_COPPER_SLAB, - WAXED_WEATHERED_CUT_COPPER_STAIRS, - WEATHERED_COPPER, - WEATHERED_CUT_COPPER, - WEATHERED_CUT_COPPER_SLAB, - WEATHERED_CUT_COPPER_STAIRS, - WEEPING_VINES, - WEEPING_VINES_PLANT, - WET_SPONGE( - 1, - "SPONGE"), - /** - * Wheat is a known material in pre-1.13 - */ - WHEAT( - "CROPS"), - WHEAT_SEEDS( - "SEEDS"), - WHITE_BANNER( - 15, - "STANDING_BANNER", - "BANNER"), - WHITE_BED( - "BED_BLOCK", - "BED"), - WHITE_CANDLE, - WHITE_CANDLE_CAKE, - WHITE_CARPET( - "CARPET"), - WHITE_CONCRETE( - "CONCRETE"), - WHITE_CONCRETE_POWDER( - "CONCRETE_POWDER"), - WHITE_DYE( - 15, - "INK_SACK", - "BONE_MEAL"), - WHITE_GLAZED_TERRACOTTA, - WHITE_SHULKER_BOX, - WHITE_STAINED_GLASS( - "STAINED_GLASS"), - WHITE_STAINED_GLASS_PANE( - "THIN_GLASS", - "STAINED_GLASS_PANE"), - WHITE_TERRACOTTA( - "STAINED_CLAY"), - WHITE_TULIP( - 6, - "RED_ROSE"), - WHITE_WALL_BANNER( - 15, - "WALL_BANNER"), - WHITE_WOOL( - "WOOL"), - WITCH_SPAWN_EGG( - 66, - "MONSTER_EGG"), - WITHER_ROSE, - WITHER_SKELETON_SKULL( - 1, - "SKULL", - "SKULL_ITEM"), - WITHER_SKELETON_SPAWN_EGG( - 5, - "MONSTER_EGG"), - WITHER_SKELETON_WALL_SKULL( - 1, - "SKULL", - "SKULL_ITEM"), - WOLF_SPAWN_EGG( - 95, - "MONSTER_EGG"), - WOODEN_AXE( - "WOOD_AXE"), - WOODEN_HOE( - "WOOD_HOE"), - WOODEN_PICKAXE( - "WOOD_PICKAXE"), - WOODEN_SHOVEL( - "WOOD_SPADE"), - WOODEN_SWORD( - "WOOD_SWORD"), - WRITABLE_BOOK( - "BOOK_AND_QUILL"), - WRITTEN_BOOK, - YELLOW_BANNER( - 11, - "STANDING_BANNER", - "BANNER"), - YELLOW_BED( - supports(12) ? 4 : 0, - "BED_BLOCK", - "BED"), - YELLOW_CANDLE, - YELLOW_CANDLE_CAKE, - YELLOW_CARPET( - 4, - "CARPET"), - YELLOW_CONCRETE( - 4, - "CONCRETE"), - YELLOW_CONCRETE_POWDER( - 4, - "CONCRETE_POWDER"), - YELLOW_DYE( - 11, - "INK_SACK", - "DANDELION_YELLOW"), - YELLOW_GLAZED_TERRACOTTA, - YELLOW_SHULKER_BOX, - YELLOW_STAINED_GLASS( - 4, - "STAINED_GLASS"), - YELLOW_STAINED_GLASS_PANE( - 4, - "THIN_GLASS", - "STAINED_GLASS_PANE"), - YELLOW_TERRACOTTA( - 4, - "STAINED_CLAY"), - YELLOW_WALL_BANNER( - 11, - "WALL_BANNER"), - YELLOW_WOOL( - 4, - "WOOL"), - ZOGLIN_SPAWN_EGG, - ZOMBIE_HEAD( - 2, - "SKULL", - "SKULL_ITEM"), - ZOMBIE_HORSE_SPAWN_EGG( - 29, - "MONSTER_EGG"), - ZOMBIE_SPAWN_EGG( - 54, - "MONSTER_EGG"), - ZOMBIE_VILLAGER_SPAWN_EGG( - 27, - "MONSTER_EGG"), - ZOMBIE_WALL_HEAD( - 2, - "SKULL", - "SKULL_ITEM"), - ZOMBIFIED_PIGLIN_SPAWN_EGG( - 57, - "MONSTER_EGG", - "ZOMBIE_PIGMAN_SPAWN_EGG"); - - /** - * Cached array of {@link XMaterial#values()} to avoid allocating memory for - * calling the method every time. - * - * @since 2.0.0 - */ - public static final XMaterial[] VALUES = values(); - - /** - * We don't want to use {@link Enums#getIfPresent(Class, String)} to avoid a few checks. - * - * @since 5.1.0 - */ - private static final Map NAMES = new HashMap<>(); - - /** - * Guava (Google Core Libraries for Java)'s cache for performance and timed caches. - * For strings that match a certain XMaterial. Mostly cached for configs. - * - * @since 1.0.0 - */ - private static final Cache NAME_CACHE = CacheBuilder.newBuilder().expireAfterAccess(1, TimeUnit.HOURS).build(); - /** - * This is used for {@link #isOneOf(Collection)} - * - * @since 3.4.0 - */ - private static final Cache CACHED_REGEX = CacheBuilder.newBuilder().expireAfterAccess(3, TimeUnit.HOURS).build(); - /** - * The maximum data value in the pre-flattening update which belongs to {@link #VILLAGER_SPAWN_EGG}
- * https://minecraftitemids.com/types/spawn-egg - * - * @see #matchXMaterialWithData(String) - * @since 8.0.0 - */ - private static final byte MAX_DATA_VALUE = 120; - /** - * Used to tell the system that the passed object's (name or material) data value - * is not provided or is invalid. - * - * @since 8.0.0 - */ - private static final byte UNKNOWN_DATA_VALUE = -1; - /** - * The maximum material ID before the pre-flattening update which belongs to {@link #MUSIC_DISC_WAIT} - * - * @since 8.1.0 - */ - private static final short MAX_ID = 2267; - /** - * XMaterial Paradox (Duplication Check) - *

- * A set of duplicated material names in 1.13 and 1.12 that will conflict with the legacy names. - * Values are the new material names. This map also contains illegal elements. Check the static initializer for more info. - *

- * Duplications are not useful at all in versions above the flattening update {@link Data#ISFLAT} - * This set is only used for matching materials, for parsing refer to {@link #isDuplicated()} - * - * @since 3.0.0 - */ - private static final Set DUPLICATED; - - static { - for (XMaterial material : VALUES) NAMES.put(material.name(), material); - } - - static { - if (Data.ISFLAT) { - // It's not needed at all if it's the newer version. We can save some memory. - DUPLICATED = null; - }else { - // MELON_SLICE, CARROTS, POTATOES, BEETROOTS, GRASS_BLOCK, BRICKS, NETHER_BRICKS, BROWN_MUSHROOM - // Using the constructor to add elements will decide to allocate more size which we don't need. - DUPLICATED = new HashSet<>(4); - DUPLICATED.add(GRASS.name()); - DUPLICATED.add(MELON.name()); - DUPLICATED.add(BRICK.name()); - DUPLICATED.add(NETHER_BRICK.name()); - } - } - - /** - * The data value of this material https://minecraft.gamepedia.com/Java_Edition_data_values/Pre-flattening - * It's never a negative number. - * - * @see #getData() - */ - private final byte data; - /** - * A list of material names that was being used for older verions. - * - * @see #getLegacy() - */ - @Nonnull - private final String[] legacy; - /** - * The cached Bukkit parsed material. - * - * @see #parseMaterial() - * @since 9.0.0 - */ - @Nullable - private final Material material; - - XMaterial(int data, @Nonnull String... legacy) { - this.data = (byte) data; - this.legacy = legacy; - - Material mat = null; - if ((!Data.ISFLAT && this.isDuplicated()) || (mat = Material.getMaterial(this.name())) == null) { - for (int i = legacy.length - 1; i >= 0; i--) { - mat = Material.getMaterial(legacy[i]); - if (mat != null) break; - } - } - this.material = mat; - } - - XMaterial(String... legacy) { - this(0, legacy); - } - - /** - * Checks if the version is 1.13 Aquatic Update or higher. - * An invocation of this method yields the cached result from the expression: - *

- *

- * {@link #supports(int) 13}} - *
- * - * @return true if 1.13 or higher. - * @see #getVersion() - * @see #supports(int) - * @since 1.0.0 - * @deprecated Use {@code XMaterial.supports(13)} instead. This method name can be confusing. - */ - @Deprecated - public static boolean isNewVersion() { - return Data.ISFLAT; - } - - /** - * This is just an extra method that can be used for many cases. - * It can be used in {@link org.bukkit.event.player.PlayerInteractEvent} - * or when accessing {@link org.bukkit.entity.Player#getMainHand()}, - * or other compatibility related methods. - *

- * An invocation of this method yields exactly the same result as the expression: - *

- *

- * !{@link #supports(int)} 9 - *
- * - * @since 2.0.0 - * @deprecated Use {@code !XMaterial.supports(9)} instead. - */ - @Deprecated - public static boolean isOneEight() { - return !supports(9); - } - - /** - * Gets the XMaterial with this name similar to {@link #valueOf(String)} - * without throwing an exception. - * - * @param name the name of the material. - * - * @return an optional that can be empty. - * @since 5.1.0 - */ - @Nonnull - private static Optional getIfPresent(@Nonnull String name) { - return Optional.ofNullable(NAMES.get(name)); - } - - /** - * The current version of the server. - * - * @return the current server version minor number. - * @see #supports(int) - * @since 2.0.0 - */ - public static int getVersion() { - return Data.VERSION; - } - - /** - * When using 1.13+, this helps to find the old material name - * with its data value using a cached search for optimization. - * - * @see #matchDefinedXMaterial(String, byte) - * @since 1.0.0 - */ - @Nullable - private static XMaterial requestOldXMaterial(@Nonnull String name, byte data) { - String holder = name + data; - XMaterial cache = NAME_CACHE.getIfPresent(holder); - if (cache != null) return cache; - - for (XMaterial material : VALUES) { - // Not using material.name().equals(name) check is intended. - if ((data == UNKNOWN_DATA_VALUE || data == material.data) && material.anyMatchLegacy(name)) { - NAME_CACHE.put(holder, material); - return material; - } - } - - return null; - } - - /** - * Parses the given material name as an XMaterial with a given data - * value in the string if attached. Check {@link #matchXMaterialWithData(String)} for more info. - * - * @see #matchXMaterialWithData(String) - * @see #matchDefinedXMaterial(String, byte) - * @since 2.0.0 - */ - @Nonnull - public static Optional matchXMaterial(@Nonnull String name) { - Validate.notEmpty(name, "Cannot match a material with null or empty material name"); - Optional oldMatch = matchXMaterialWithData(name); - return oldMatch.isPresent() ? oldMatch : matchDefinedXMaterial(format(name), UNKNOWN_DATA_VALUE); - } - - /** - * Parses material name and data value from the specified string. - * The separator for the material name and its data value is {@code :} - * Spaces are allowed. Mostly used when getting materials from config for old school minecrafters. - *

- * Examples - *

-	 *     {@code INK_SACK:1 -> RED_DYE}
-	 *     {@code WOOL: 14  -> RED_WOOL}
-	 * 
- * - * @param name the material string that consists of the material name, data and separator character. - * - * @return the parsed XMaterial. - * @see #matchXMaterial(String) - * @since 3.0.0 - */ - @Nonnull - private static Optional matchXMaterialWithData(@Nonnull String name) { - int index = name.indexOf(':'); - if (index != -1) { - String mat = format(name.substring(0, index)); - try { - // We don't use Byte.parseByte because we have our own range check. - byte data = (byte) Integer.parseInt(StringUtils.deleteWhitespace(name.substring(index + 1))); - return data >= 0 && data < MAX_DATA_VALUE ? matchDefinedXMaterial(mat, data) : matchDefinedXMaterial(mat, UNKNOWN_DATA_VALUE); - }catch (NumberFormatException ignored) { - return matchDefinedXMaterial(mat, UNKNOWN_DATA_VALUE); - } - } - - return Optional.empty(); - } - - /** - * Parses the given material as an XMaterial. - * - * @throws IllegalArgumentException may be thrown as an unexpected exception. - * @see #matchDefinedXMaterial(String, byte) - * @see #matchXMaterial(ItemStack) - * @since 2.0.0 - */ - @Nonnull - public static XMaterial matchXMaterial(@Nonnull Material material) { - Objects.requireNonNull(material, "Cannot match null material"); - return matchDefinedXMaterial(material.name(), UNKNOWN_DATA_VALUE).orElseThrow(() -> new IllegalArgumentException("Unsupported material with no data value: " + material.name())); - } - - /** - * Parses the given item as an XMaterial using its material and data value (durability) - * if not a damageable item {@link ItemStack#getDurability()}. - * - * @param item the ItemStack to match. - * - * @return an XMaterial if matched any. - * @throws IllegalArgumentException may be thrown as an unexpected exception. - * @see #matchXMaterial(Material) - * @since 2.0.0 - */ - @Nonnull - @SuppressWarnings ("deprecation") - public static XMaterial matchXMaterial(@Nonnull ItemStack item) { - Objects.requireNonNull(item, "Cannot match null ItemStack"); - String material = item.getType().name(); - byte data = (byte) (Data.ISFLAT || item.getType().getMaxDurability() > 0 ? 0 : item.getDurability()); - - // They didn't really use the items data value in older versions. - if (!Data.ISFLAT && item.hasItemMeta() && material.equals("MONSTER_EGG")) { - ItemMeta meta = item.getItemMeta(); - if (meta instanceof SpawnEggMeta) { - SpawnEggMeta egg = (SpawnEggMeta) meta; - material = egg.getSpawnedType().name() + "_SPAWN_EGG"; - } - } - - // Potions used the items data value to store - // information about the type of potion in 1.8 - if (!supports(9) && material.endsWith("ION")) { - // There's also 16000+ data value technique, but this is more reliable. - return Potion.fromItemStack(item).isSplash() ? SPLASH_POTION : POTION; - } - - // Refer to the enum for info. - // Currently this is the only material with a non-zero data value - // that has been renamed after the flattening update. - // If this happens to more materials in the future, - // I might have to change then system. - if (Data.ISFLAT && !supports(14) && material.equals("CACTUS_GREEN")) return GREEN_DYE; - - // Check FILLED_MAP enum for more info. - // if (!Data.ISFLAT && item.hasItemMeta() && item.getItemMeta() instanceof org.bukkit.inventory.meta.MapMeta) return FILLED_MAP; - - // No orElseThrow, I don't want to deal with Java's final variable bullshit. - Optional result = matchDefinedXMaterial(material, data); - if (result.isPresent()) return result.get(); - throw new IllegalArgumentException("Unsupported material from item: " + material + " (" + data + ')'); - } - - /** - * The main method that parses the given material name and data value as an XMaterial. - * All the values passed to this method will not be null or empty and are formatted correctly. - * - * @param name the formatted name of the material. - * @param data the data value of the material. Is always 0 or {@link #UNKNOWN_DATA_VALUE} when {@link Data#ISFLAT} - * - * @return an XMaterial (with the same data value if specified) - * @see #matchXMaterial(Material) - * @see #matchXMaterial(int, byte) - * @see #matchXMaterial(ItemStack) - * @since 3.0.0 - */ - @Nonnull - protected static Optional matchDefinedXMaterial(@Nonnull String name, byte data) { - // if (!Boolean.valueOf(Boolean.getBoolean(Boolean.TRUE.toString())).equals(Boolean.FALSE.booleanValue())) return null; - Boolean duplicated = null; - boolean isAMap = name.equalsIgnoreCase("MAP"); - - // Do basic number and boolean checks before accessing more complex enum stuff. - if (Data.ISFLAT || (!isAMap && data <= 0 && !(duplicated = isDuplicated(name)))) { - Optional xMaterial = getIfPresent(name); - if (xMaterial.isPresent()) return xMaterial; - } - // Usually flat versions wouldn't pass this point, but some special materials do. - - XMaterial oldXMaterial = requestOldXMaterial(name, data); - if (oldXMaterial == null) { - // Special case. Refer to FILLED_MAP for more info. - return (data >= 0 && isAMap) ? Optional.of(FILLED_MAP) : Optional.empty(); - } - - if (!Data.ISFLAT && oldXMaterial.isPlural() && (duplicated == null ? isDuplicated(name) : duplicated)) return getIfPresent(name); - return Optional.of(oldXMaterial); - } - - /** - * XMaterial Paradox (Duplication Check) - * Checks if the material has any duplicates. - *

- * Example: - *

{@code MELON, CARROT, POTATO, BEETROOT -> true} - * - * @param name the name of the material to check. - * - * @return true if there's a duplicated material for this material, otherwise false. - * @since 2.0.0 - */ - private static boolean isDuplicated(@Nonnull String name) { - // Don't use matchXMaterial() since this method is being called from matchXMaterial() itself and will cause a StackOverflowError. - return DUPLICATED.contains(name); - } - - /** - * Gets the XMaterial based on the material's ID (Magic Value) and data value.
- * You should avoid using this for performance issues. - * - * @param id the ID (Magic value) of the material. - * @param data the data value of the material. - * - * @return a parsed XMaterial with the same ID and data value. - * @see #matchXMaterial(ItemStack) - * @since 2.0.0 - * @deprecated this method loops through all the available materials and matches their ID using {@link #getId()} - * which takes a really long time. Plugins should no longer support IDs. If you want, you can make a {@link Map} cache yourself. - * This method obviously doesn't work for 1.13+ and will not be supported. This is only here for debugging purposes. - */ - @Nonnull - @Deprecated - public static Optional matchXMaterial(int id, byte data) { - if (id < 0 || id > MAX_ID || data < 0) return Optional.empty(); - for (XMaterial materials : VALUES) { - if (materials.data == data && materials.getId() == id) return Optional.of(materials); - } - return Optional.empty(); - } - - /** - * Attempts to build the string like an enum name. - * Removes all the spaces, and extra non-English characters. Also removes some config/in-game based strings. - * While this method is hard to maintain, it's extremely efficient. It's approximately more than x5 times faster than - * the normal RegEx + String Methods approach for both formatted and unformatted material names. - * - * @param name the material name to modify. - * - * @return an enum name. - * @since 2.0.0 - */ - @Nonnull - protected static String format(@Nonnull String name) { - int len = name.length(); - char[] chs = new char[len]; - int count = 0; - boolean appendUnderline = false; - - for (int i = 0; i < len; i++) { - char ch = name.charAt(i); - - if (!appendUnderline && count != 0 && (ch == '-' || ch == ' ' || ch == '_') && chs[count] != '_') - appendUnderline = true; - else { - boolean number = false; - // Old materials have numbers in them. - if ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (number = (ch >= '0' && ch <= '9'))) { - if (appendUnderline) { - chs[count++] = '_'; - appendUnderline = false; - } - - if (number) - chs[count++] = ch; - else chs[count++] = (char) (ch & 0x5f); - } - } - } - - return new String(chs, 0, count); - } - - /** - * Checks if the specified version is the same version or higher than the current server version. - * - * @param version the major version to be checked. "1." is ignored. E.g. 1.12 = 12 | 1.9 = 9 - * - * @return true of the version is equal or higher than the current version. - * @since 2.0.0 - */ - public static boolean supports(int version) { - return Data.VERSION >= version; - } - - public String[] getLegacy() { - return this.legacy; - } - - /** - * XMaterial Paradox (Duplication Check) - * I've concluded that this is just an infinite loop that keeps - * going around the Singular Form and the Plural Form materials. A waste of brain cells and a waste of time. - * This solution works just fine anyway. - *

- * A solution for XMaterial Paradox. - * Manually parses the duplicated materials to find the exact material based on the server version. - * If the name ends with "S" -> Plural Form Material. - * Plural methods are only plural if they're also {@link #DUPLICATED} - *

- * The only special exceptions are {@link #BRICKS} and {@link #NETHER_BRICKS} - * - * @return true if this material is a plural form material, otherwise false. - * @since 8.0.0 - */ - private boolean isPlural() { - // this.name().charAt(this.name().length() - 1) == 'S' - return this == CARROTS || this == POTATOES; - } - - /** - * Checks if the list of given material names matches the given base material. - * Mostly used for configs. - *

- * Supports {@link String#contains} {@code CONTAINS:NAME} and Regular Expression {@code REGEX:PATTERN} formats. - *

- * Example: - *

-	 *     XMaterial material = {@link #matchXMaterial(ItemStack)};
-	 *     if (material.isOneOf(plugin.getConfig().getStringList("disabled-items")) return;
-	 * 
- *
- * {@code CONTAINS} Examples: - *
-	 *     {@code "CONTAINS:CHEST" -> CHEST, ENDERCHEST, TRAPPED_CHEST -> true}
-	 *     {@code "cOnTaINS:dYe" -> GREEN_DYE, YELLOW_DYE, BLUE_DYE, INK_SACK -> true}
-	 * 
- *

- * {@code REGEX} Examples - *

-	 *     {@code "REGEX:^.+_.+_.+$" -> Every Material with 3 underlines or more: SHULKER_SPAWN_EGG, SILVERFISH_SPAWN_EGG, SKELETON_HORSE_SPAWN_EGG}
-	 *     {@code "REGEX:^.{1,3}$" -> Material names that have 3 letters only: BED, MAP, AIR}
-	 * 
- *

- * The reason that there are tags for {@code CONTAINS} and {@code REGEX} is for the performance. - * Although RegEx patterns are cached in this method, - * please avoid using the {@code REGEX} tag if you can use the {@code CONTAINS} tag instead. - * It'll have a huge impact on performance. - * Please avoid using {@code (capturing groups)} there's no use for them in this case. - * If you want to use groups, use {@code (?: non-capturing groups)}. It's faster. - *

- * Want to learn RegEx? You can mess around in RegExr website. - * - * @param materials the material names to check base material on. - * - * @return true if one of the given material names is similar to the base material. - * @since 3.1.1 - */ - public boolean isOneOf(@Nullable Collection materials) { - if (materials == null || materials.isEmpty()) return false; - String name = this.name(); - - for (String comp : materials) { - String checker = comp.toUpperCase(Locale.ENGLISH); - if (checker.startsWith("CONTAINS:")) { - comp = format(checker.substring(9)); - if (name.contains(comp)) return true; - continue; - } - if (checker.startsWith("REGEX:")) { - comp = comp.substring(6); - Pattern pattern = CACHED_REGEX.getIfPresent(comp); - if (pattern == null) { - try { - pattern = Pattern.compile(comp); - CACHED_REGEX.put(comp, pattern); - }catch (PatternSyntaxException ex) { - ex.printStackTrace(); - } - } - if (pattern != null && pattern.matcher(name).matches()) return true; - continue; - } - - // Direct Object Equals - Optional xMat = matchXMaterial(comp); - if (xMat.isPresent() && xMat.get() == this) return true; - } - return false; - } - - /** - * Sets the {@link Material} (and data value on older versions) of an item. - * Damageable materials will not have their durability changed. - *

- * Use {@link #parseItem()} instead when creating new ItemStacks. - * - * @param item the item to change its type. - * - * @see #parseItem() - * @since 3.0.0 - */ - @Nonnull - @SuppressWarnings ("deprecation") - public ItemStack setType(@Nonnull ItemStack item) { - Objects.requireNonNull(item, "Cannot set material for null ItemStack"); - Material material = this.parseMaterial(); - Objects.requireNonNull(material, () -> "Unsupported material: " + this.name()); - - item.setType(material); - if (!Data.ISFLAT && material.getMaxDurability() <= 0) item.setDurability(this.data); - return item; - } - - /** - * Checks if the given material name matches any of this xmaterial's legacy material names. - * All the values passed to this method will not be null or empty and are formatted correctly. - * - * @param name the material name to check. - * - * @return true if it's one of the legacy names, otherwise false. - * @since 2.0.0 - */ - private boolean anyMatchLegacy(@Nonnull String name) { - for (int i = this.legacy.length - 1; i >= 0; i--) { - if (name.equals(this.legacy[i])) return true; - } - return false; - } - - /** - * Parses an enum name to a user-friendly name. - * These names will have underlines removed and with each word capitalized. - *

- * Examples: - *

-	 *     {@literal EMERALD                 -> Emerald}
-	 *     {@literal EMERALD_BLOCK           -> Emerald Block}
-	 *     {@literal ENCHANTED_GOLDEN_APPLE  -> Enchanted Golden Apple}
-	 * 
- * - * @return a more user-friendly enum name. - * @since 3.0.0 - */ - @Override - @Nonnull - public String toString() { - return WordUtils.capitalize(this.name().replace('_', ' ').toLowerCase(Locale.ENGLISH)); - } - - /** - * Gets the ID (Magic value) of the material. - * https://www.minecraftinfo.com/idlist.htm - *

- * Spigot added material ID support back in 1.16+ - * - * @return the ID of the material or -1 if it's not a legacy material or the server doesn't support the material. - * @see #matchXMaterial(int, byte) - * @since 2.2.0 - */ - @SuppressWarnings ("deprecation") - public int getId() { - // https://hub.spigotmc.org/stash/projects/SPIGOT/repos/bukkit/diff/src/main/java/org/bukkit/Material.java?until=1cb03826ebde4ef887519ce37b0a2a341494a183 - // Should start working again in 1.16+ - Material material = this.parseMaterial(); - if (material == null) return -1; - try { - return material.getId(); - }catch (IllegalArgumentException ignored) { - return -1; - } - } - - /** - * The data value of this material pre-flattening. - *

- * Can be accessed with {@link ItemStack#getData()} then {@code MaterialData#getData()} - * or {@link ItemStack#getDurability()} if not damageable. - * - * @return data of this material, or 0 if none. - * @since 1.0.0 - */ - @SuppressWarnings ("deprecation") - public byte getData() { - return data; - } - - /** - * Parses an item from this XMaterial. - * Uses data values on older versions. - * - * @return an ItemStack with the same material (and data value if in older versions.) - * @see #setType(ItemStack) - * @since 2.0.0 - */ - @Nullable - @SuppressWarnings ("deprecation") - public ItemStack parseItem() { - Material material = this.parseMaterial(); - if (material == null) return null; - return Data.ISFLAT ? new ItemStack(material) : new ItemStack(material, 1, this.data); - } - - /** - * Parses the material of this XMaterial. - * - * @return the material related to this XMaterial based on the server version. - * @since 1.0.0 - */ - @Nullable - public Material parseMaterial() { - return this.material; - } - - /** - * Checks if an item has the same material (and data value on older versions). - * - * @param item item to check. - * - * @return true if the material is the same as the item's material (and data value if on older versions), otherwise false. - * @since 1.0.0 - */ - @SuppressWarnings ("deprecation") - public boolean isSimilar(@Nonnull ItemStack item) { - Objects.requireNonNull(item, "Cannot compare with null ItemStack"); - if (item.getType() != this.parseMaterial()) return false; - return Data.ISFLAT || item.getDurability() == this.data || item.getType().getMaxDurability() > 0; - } - - /** - * Checks if this material is supported in the current version. - * Suggested materials will be ignored. - *

- * Note that you should use {@link #parseMaterial()} or {@link #parseItem()} and check if it's null - * if you're going to parse and use the material/item later. - * - * @return true if the material exists in {@link Material} list. - * @since 2.0.0 - */ - public boolean isSupported() { - return this.material != null; - } - - /** - * This method is needed due to Java enum initialization limitations. - * It's really inefficient yes, but it's only used for initialization. - *

- * Yes there are many other ways like comparing the hardcoded ordinal or using a boolean in the enum constructor, - * but it's not really a big deal. - *

- * This method should not be called if the version is after the flattening update {@link Data#ISFLAT} - * and is only used for parsing materials, not matching, for matching check {@link #DUPLICATED} - */ - private boolean isDuplicated() { - switch (this.name()) { - case "MELON": - case "CARROT": - case "POTATO": - case "GRASS": - case "BRICK": - case "NETHER_BRICK": - - // Illegal Elements - // Since both 1.12 and 1.13 have _DOOR XMaterial will use it - // for 1.12 to parse the material, but it needs _DOOR_ITEM. - // We'll trick XMaterial into thinking this needs to be parsed - // using the old methods. - // Some of these materials have their enum name added to the legacy list as well. - case "DARK_OAK_DOOR": - case "ACACIA_DOOR": - case "BIRCH_DOOR": - case "JUNGLE_DOOR": - case "SPRUCE_DOOR": - case "MAP": - case "CAULDRON": - case "BREWING_STAND": - case "FLOWER_POT": - return true; - default: - return false; - } - } - - /** - * Used for data that need to be accessed during enum initialization. - * - * @since 9.0.0 - */ - private static final class Data { - /** - * The current version of the server in the form of a major version. - * If the static initialization for this fails, you know something's wrong with the server software. - * - * @since 1.0.0 - */ - private static final int VERSION; - - static { // This needs to be right below VERSION because of initialization order. - String version = Bukkit.getVersion(); - Matcher matcher = Pattern.compile("MC: \\d\\.(\\d+)").matcher(version); - - if (matcher.find()) - VERSION = Integer.parseInt(matcher.group(1)); - else throw new IllegalArgumentException("Failed to parse server version from: " + version); - } - - /** - * Cached result if the server version is after the v1.13 flattening update. - * - * @since 3.0.0 - */ - private static final boolean ISFLAT = supports(13); - } - - /* CUSTOM */ - - public static ItemStack playerSkullItem() { - Material mat = PLAYER_HEAD.parseMaterial(); - if (isNewVersion()) return new ItemStack(mat); - return new ItemStack(Material.getMaterial("SKULL_ITEM"), 1, (short) SkullType.PLAYER.ordinal()); - } - - public static XMaterial mobItem(EntityType type) { - if (type == null) return SPONGE; - Optional material = XMaterial.matchXMaterial(type.name() + "_SPAWN_EGG"); - if (material.isPresent()) return material.get(); - if (type == EntityType.WITHER) return WITHER_SKELETON_SKULL; - if (type == EntityType.IRON_GOLEM) return IRON_BLOCK; - if (type == EntityType.SNOWMAN) return SNOW_BLOCK; - if (type == EntityType.MUSHROOM_COW) return MOOSHROOM_SPAWN_EGG; - if (type == EntityType.GIANT) return ZOMBIE_SPAWN_EGG; - if (type == EntityType.ARMOR_STAND) return ARMOR_STAND; - if (type == EntityType.PLAYER) return PLAYER_HEAD; - if (type == EntityType.ENDER_DRAGON) return DRAGON_HEAD; - if (type.name().equals("PIG_ZOMBIE") || type.name().equals("ZOMBIFIED_PIGLIN")) return ZOMBIFIED_PIGLIN_SPAWN_EGG; - if (type.name().equals("ILLUSIONER")) return BLAZE_POWDER; - return SPONGE; - } - + ACACIA_BOAT("BOAT_ACACIA"), + ACACIA_BUTTON("WOOD_BUTTON"), + ACACIA_CHEST_BOAT, + ACACIA_DOOR("ACACIA_DOOR", "ACACIA_DOOR_ITEM"), + ACACIA_FENCE, + ACACIA_FENCE_GATE, + ACACIA_LEAVES(4, "LEAVES_2"), + ACACIA_LOG(4, "LOG_2"), + ACACIA_PLANKS(4, "WOOD"), + ACACIA_PRESSURE_PLATE("WOOD_PLATE"), + ACACIA_SAPLING(4, "SAPLING"), + ACACIA_SIGN("SIGN_POST", "SIGN"), + ACACIA_SLAB(4, "WOOD_DOUBLE_STEP", "WOOD_STEP", "WOODEN_SLAB"), + ACACIA_STAIRS, + ACACIA_TRAPDOOR("TRAP_DOOR"), + ACACIA_WALL_SIGN("WALL_SIGN"), + ACACIA_WOOD(4, "LOG_2"), + ACTIVATOR_RAIL, + /** + * https://minecraft.gamepedia.com/Air + * {@link Material#isAir()} + * + * @see #VOID_AIR + * @see #CAVE_AIR + */ + AIR, + ALLAY_SPAWN_EGG, + ALLIUM(2, "RED_ROSE"), + AMETHYST_BLOCK, + AMETHYST_CLUSTER, + AMETHYST_SHARD, + ANCIENT_DEBRIS, + ANDESITE(5, "STONE"), + ANDESITE_SLAB, + ANDESITE_STAIRS, + ANDESITE_WALL, + ANVIL, + APPLE, + ARMOR_STAND, + ARROW, + ATTACHED_MELON_STEM(7, "MELON_STEM"), + ATTACHED_PUMPKIN_STEM(7, "PUMPKIN_STEM"), + AXOLOTL_BUCKET, + AXOLOTL_SPAWN_EGG, + AZALEA, + AZALEA_LEAVES, + AZURE_BLUET(3, "RED_ROSE"), + BAKED_POTATO, + BAMBOO, + BAMBOO_SAPLING, + BARREL, + BARRIER, + BASALT, + BAT_SPAWN_EGG(65, "MONSTER_EGG"), + BEACON, + BEDROCK, + BEEF("RAW_BEEF"), + BEEHIVE, + /** + * Beetroot is a known material in pre-1.13 + */ + BEETROOT("BEETROOT_BLOCK"), + BEETROOTS("BEETROOT"), + BEETROOT_SEEDS, + BEETROOT_SOUP, + BEE_NEST, + BEE_SPAWN_EGG, + BELL, + BIG_DRIPLEAF, + BIG_DRIPLEAF_STEM, + BIRCH_BOAT("BOAT_BIRCH"), + BIRCH_BUTTON("WOOD_BUTTON"), + BIRCH_CHEST_BOAT, + BIRCH_DOOR("BIRCH_DOOR", "BIRCH_DOOR_ITEM"), + BIRCH_FENCE, + BIRCH_FENCE_GATE, + BIRCH_LEAVES(2, "LEAVES"), + BIRCH_LOG(2, "LOG"), + BIRCH_PLANKS(2, "WOOD"), + BIRCH_PRESSURE_PLATE("WOOD_PLATE"), + BIRCH_SAPLING(2, "SAPLING"), + BIRCH_SIGN("SIGN_POST", "SIGN"), + BIRCH_SLAB(2, "WOOD_DOUBLE_STEP", "WOOD_STEP", "WOODEN_SLAB"), + BIRCH_STAIRS("BIRCH_WOOD_STAIRS"), + BIRCH_TRAPDOOR("TRAP_DOOR"), + BIRCH_WALL_SIGN("WALL_SIGN"), + BIRCH_WOOD(2, "LOG"), + BLACKSTONE, + BLACKSTONE_SLAB, + BLACKSTONE_STAIRS, + BLACKSTONE_WALL, + BLACK_BANNER("STANDING_BANNER", "BANNER"), + /** + * Version 1.12+ interprets "BED" as BLACK_BED due to enum alphabetic ordering. + */ + BLACK_BED(supports(12) ? 15 : 0, "BED_BLOCK", "BED"), + BLACK_CANDLE, + BLACK_CANDLE_CAKE, + BLACK_CARPET(15, "CARPET"), + BLACK_CONCRETE(15, "CONCRETE"), + BLACK_CONCRETE_POWDER(15, "CONCRETE_POWDER"), + BLACK_DYE, + BLACK_GLAZED_TERRACOTTA, + BLACK_SHULKER_BOX, + BLACK_STAINED_GLASS(15, "STAINED_GLASS"), + BLACK_STAINED_GLASS_PANE(15, "STAINED_GLASS_PANE"), + BLACK_TERRACOTTA(15, "STAINED_CLAY"), + BLACK_WALL_BANNER("WALL_BANNER"), + BLACK_WOOL(15, "WOOL"), + BLAST_FURNACE, + BLAZE_POWDER, + BLAZE_ROD, + BLAZE_SPAWN_EGG(61, "MONSTER_EGG"), + BLUE_BANNER(4, "STANDING_BANNER", "BANNER"), + BLUE_BED(supports(12) ? 11 : 0, "BED_BLOCK", "BED"), + BLUE_CANDLE, + BLUE_CANDLE_CAKE, + BLUE_CARPET(11, "CARPET"), + BLUE_CONCRETE(11, "CONCRETE"), + BLUE_CONCRETE_POWDER(11, "CONCRETE_POWDER"), + BLUE_DYE(4, "INK_SACK", "LAPIS_LAZULI"), + BLUE_GLAZED_TERRACOTTA, + BLUE_ICE, + BLUE_ORCHID(1, "RED_ROSE"), + BLUE_SHULKER_BOX, + BLUE_STAINED_GLASS(11, "STAINED_GLASS"), + BLUE_STAINED_GLASS_PANE(11, "THIN_GLASS", "STAINED_GLASS_PANE"), + BLUE_TERRACOTTA(11, "STAINED_CLAY"), + BLUE_WALL_BANNER(4, "WALL_BANNER"), + BLUE_WOOL(11, "WOOL"), + BONE, + BONE_BLOCK, + BONE_MEAL(15, "INK_SACK"), + BOOK, + BOOKSHELF, + BOW, + BOWL, + BRAIN_CORAL, + BRAIN_CORAL_BLOCK, + BRAIN_CORAL_FAN, + BRAIN_CORAL_WALL_FAN, + BREAD, + BREWING_STAND("BREWING_STAND", "BREWING_STAND_ITEM"), + BRICK("CLAY_BRICK"), + BRICKS("BRICKS", "BRICK"), + BRICK_SLAB(4, "STEP"), + BRICK_STAIRS, + BRICK_WALL, + BROWN_BANNER(3, "STANDING_BANNER", "BANNER"), + BROWN_BED(supports(12) ? 12 : 0, "BED_BLOCK", "BED"), + BROWN_CANDLE, + BROWN_CANDLE_CAKE, + BROWN_CARPET(12, "CARPET"), + BROWN_CONCRETE(12, "CONCRETE"), + BROWN_CONCRETE_POWDER(12, "CONCRETE_POWDER"), + BROWN_DYE(3, "INK_SACK", "DYE", "COCOA_BEANS"), + BROWN_GLAZED_TERRACOTTA, + BROWN_MUSHROOM, + BROWN_MUSHROOM_BLOCK("BROWN_MUSHROOM", "HUGE_MUSHROOM_1"), + BROWN_SHULKER_BOX, + BROWN_STAINED_GLASS(12, "STAINED_GLASS"), + BROWN_STAINED_GLASS_PANE(12, "THIN_GLASS", "STAINED_GLASS_PANE"), + BROWN_TERRACOTTA(12, "STAINED_CLAY"), + BROWN_WALL_BANNER(3, "WALL_BANNER"), + BROWN_WOOL(12, "WOOL"), + BUBBLE_COLUMN, + BUBBLE_CORAL, + BUBBLE_CORAL_BLOCK, + BUBBLE_CORAL_FAN, + BUBBLE_CORAL_WALL_FAN, + BUCKET, + BUDDING_AMETHYST, + BUNDLE, + CACTUS, + CAKE("CAKE_BLOCK"), + CALCITE, + CAMPFIRE, + CANDLE, + CANDLE_CAKE, + CARROT("CARROT_ITEM"), + CARROTS("CARROT"), + CARROT_ON_A_STICK("CARROT_STICK"), + CARTOGRAPHY_TABLE, + CARVED_PUMPKIN, + CAT_SPAWN_EGG, + CAULDRON("CAULDRON", "CAULDRON_ITEM"), + /** + * 1.13 tag is not added because it's the same thing as {@link #AIR} + * + * @see #VOID_AIR + */ + CAVE_AIR("AIR"), + CAVE_SPIDER_SPAWN_EGG(59, "MONSTER_EGG"), + CAVE_VINES, + CAVE_VINES_PLANT, + CHAIN, + CHAINMAIL_BOOTS, + CHAINMAIL_CHESTPLATE, + CHAINMAIL_HELMET, + CHAINMAIL_LEGGINGS, + CHAIN_COMMAND_BLOCK("COMMAND", "COMMAND_CHAIN"), + CHARCOAL(1, "COAL"), + CHEST("LOCKED_CHEST"), + CHEST_MINECART("STORAGE_MINECART"), + CHICKEN("RAW_CHICKEN"), + CHICKEN_SPAWN_EGG(93, "MONSTER_EGG"), + CHIPPED_ANVIL(1, "ANVIL"), + CHISELED_DEEPSLATE, + CHISELED_NETHER_BRICKS(1, "NETHER_BRICKS"), + CHISELED_POLISHED_BLACKSTONE("POLISHED_BLACKSTONE"), + CHISELED_QUARTZ_BLOCK(1, "QUARTZ_BLOCK"), + CHISELED_RED_SANDSTONE(1, "RED_SANDSTONE"), + CHISELED_SANDSTONE(1, "SANDSTONE"), + CHISELED_STONE_BRICKS(3, "SMOOTH_BRICK"), + CHORUS_FLOWER, + CHORUS_FRUIT, + CHORUS_PLANT, + CLAY, + CLAY_BALL, + CLOCK("WATCH"), + COAL, + COAL_BLOCK, + COAL_ORE, + COARSE_DIRT(1, "DIRT"), + COBBLED_DEEPSLATE, + COBBLED_DEEPSLATE_SLAB, + COBBLED_DEEPSLATE_STAIRS, + COBBLED_DEEPSLATE_WALL, + COBBLESTONE, + COBBLESTONE_SLAB(3, "STEP"), + COBBLESTONE_STAIRS, + COBBLESTONE_WALL("COBBLE_WALL"), + COBWEB("WEB"), + COCOA, + COCOA_BEANS(3, "INK_SACK"), + COD("RAW_FISH"), + COD_BUCKET, + COD_SPAWN_EGG, + COMMAND_BLOCK("COMMAND"), + COMMAND_BLOCK_MINECART("COMMAND_MINECART"), + /** + * Unlike redstone torch and redstone lamp... neither REDTONE_COMPARATOR_OFF nor REDSTONE_COMPARATOR_ON + * are items. REDSTONE_COMPARATOR is. + * + * @see #REDSTONE_TORCH + * @see #REDSTONE_LAMP + */ + COMPARATOR("REDSTONE_COMPARATOR_OFF", "REDSTONE_COMPARATOR_ON", "REDSTONE_COMPARATOR"), + COMPASS, + COMPOSTER, + CONDUIT, + COOKED_BEEF, + COOKED_CHICKEN, + COOKED_COD("COOKED_FISH"), + COOKED_MUTTON, + COOKED_PORKCHOP("GRILLED_PORK"), + COOKED_RABBIT, + COOKED_SALMON(1, "COOKED_FISH"), + COOKIE, + COPPER_BLOCK, + COPPER_INGOT, + COPPER_ORE, + CORNFLOWER, + COW_SPAWN_EGG(92, "MONSTER_EGG"), + CRACKED_DEEPSLATE_BRICKS, + CRACKED_DEEPSLATE_TILES, + CRACKED_NETHER_BRICKS(2, "NETHER_BRICKS"), + CRACKED_POLISHED_BLACKSTONE_BRICKS("POLISHED_BLACKSTONE_BRICKS"), + CRACKED_STONE_BRICKS(2, "SMOOTH_BRICK"), + CRAFTING_TABLE("WORKBENCH"), + CREEPER_BANNER_PATTERN, + CREEPER_HEAD(4, "SKULL", "SKULL_ITEM"), + CREEPER_SPAWN_EGG(50, "MONSTER_EGG"), + CREEPER_WALL_HEAD(4, "SKULL", "SKULL_ITEM"), + CRIMSON_BUTTON, + CRIMSON_DOOR, + CRIMSON_FENCE, + CRIMSON_FENCE_GATE, + CRIMSON_FUNGUS, + CRIMSON_HYPHAE, + CRIMSON_NYLIUM, + CRIMSON_PLANKS, + CRIMSON_PRESSURE_PLATE, + CRIMSON_ROOTS, + CRIMSON_SIGN("SIGN_POST"), + CRIMSON_SLAB, + CRIMSON_STAIRS, + CRIMSON_STEM, + CRIMSON_TRAPDOOR, + CRIMSON_WALL_SIGN("WALL_SIGN"), + CROSSBOW, + CRYING_OBSIDIAN, + CUT_COPPER, + CUT_COPPER_SLAB, + CUT_COPPER_STAIRS, + CUT_RED_SANDSTONE, + CUT_RED_SANDSTONE_SLAB("STONE_SLAB2"), + CUT_SANDSTONE, + CUT_SANDSTONE_SLAB(1, "STEP"), + CYAN_BANNER(6, "STANDING_BANNER", "BANNER"), + CYAN_BED(supports(12) ? 9 : 0, "BED_BLOCK", "BED"), + CYAN_CANDLE, + CYAN_CANDLE_CAKE, + CYAN_CARPET(9, "CARPET"), + CYAN_CONCRETE(9, "CONCRETE"), + CYAN_CONCRETE_POWDER(9, "CONCRETE_POWDER"), + CYAN_DYE(6, "INK_SACK"), + CYAN_GLAZED_TERRACOTTA, + CYAN_SHULKER_BOX, + CYAN_STAINED_GLASS(9, "STAINED_GLASS"), + CYAN_STAINED_GLASS_PANE(9, "STAINED_GLASS_PANE"), + CYAN_TERRACOTTA(9, "STAINED_CLAY"), + CYAN_WALL_BANNER(6, "WALL_BANNER"), + CYAN_WOOL(9, "WOOL"), + DAMAGED_ANVIL(2, "ANVIL"), + DANDELION("YELLOW_FLOWER"), + DARK_OAK_BOAT("BOAT_DARK_OAK"), + DARK_OAK_BUTTON("WOOD_BUTTON"), + DARK_OAK_CHEST_BOAT, + DARK_OAK_DOOR("DARK_OAK_DOOR", "DARK_OAK_DOOR_ITEM"), + DARK_OAK_FENCE, + DARK_OAK_FENCE_GATE, + DARK_OAK_LEAVES(5, "LEAVES_2"), + DARK_OAK_LOG(5, "LOG_2"), + DARK_OAK_PLANKS(5, "WOOD"), + DARK_OAK_PRESSURE_PLATE("WOOD_PLATE"), + DARK_OAK_SAPLING(5, "SAPLING"), + DARK_OAK_SIGN("SIGN_POST", "SIGN"), + DARK_OAK_SLAB(5, "WOOD_DOUBLE_STEP", "WOOD_STEP", "WOODEN_SLAB"), + DARK_OAK_STAIRS, + DARK_OAK_TRAPDOOR("TRAP_DOOR"), + DARK_OAK_WALL_SIGN("WALL_SIGN"), + DARK_OAK_WOOD(5, "LOG_2"), + DARK_PRISMARINE(2, "PRISMARINE"), + DARK_PRISMARINE_SLAB, + DARK_PRISMARINE_STAIRS, + DAYLIGHT_DETECTOR("DAYLIGHT_DETECTOR_INVERTED"), + DEAD_BRAIN_CORAL, + DEAD_BRAIN_CORAL_BLOCK, + DEAD_BRAIN_CORAL_FAN, + DEAD_BRAIN_CORAL_WALL_FAN, + DEAD_BUBBLE_CORAL, + DEAD_BUBBLE_CORAL_BLOCK, + DEAD_BUBBLE_CORAL_FAN, + DEAD_BUBBLE_CORAL_WALL_FAN, + DEAD_BUSH("LONG_GRASS"), + DEAD_FIRE_CORAL, + DEAD_FIRE_CORAL_BLOCK, + DEAD_FIRE_CORAL_FAN, + DEAD_FIRE_CORAL_WALL_FAN, + DEAD_HORN_CORAL, + DEAD_HORN_CORAL_BLOCK, + DEAD_HORN_CORAL_FAN, + DEAD_HORN_CORAL_WALL_FAN, + DEAD_TUBE_CORAL, + DEAD_TUBE_CORAL_BLOCK, + DEAD_TUBE_CORAL_FAN, + DEAD_TUBE_CORAL_WALL_FAN, + DEBUG_STICK, + DEEPSLATE, + DEEPSLATE_BRICKS, + DEEPSLATE_BRICK_SLAB, + DEEPSLATE_BRICK_STAIRS, + DEEPSLATE_BRICK_WALL, + DEEPSLATE_COAL_ORE, + DEEPSLATE_COPPER_ORE, + DEEPSLATE_DIAMOND_ORE, + DEEPSLATE_EMERALD_ORE, + DEEPSLATE_GOLD_ORE, + DEEPSLATE_IRON_ORE, + DEEPSLATE_LAPIS_ORE, + DEEPSLATE_REDSTONE_ORE, + DEEPSLATE_TILES, + DEEPSLATE_TILE_SLAB, + DEEPSLATE_TILE_STAIRS, + DEEPSLATE_TILE_WALL, + DETECTOR_RAIL, + DIAMOND, + DIAMOND_AXE, + DIAMOND_BLOCK, + DIAMOND_BOOTS, + DIAMOND_CHESTPLATE, + DIAMOND_HELMET, + DIAMOND_HOE, + DIAMOND_HORSE_ARMOR("DIAMOND_BARDING"), + DIAMOND_LEGGINGS, + DIAMOND_ORE, + DIAMOND_PICKAXE, + DIAMOND_SHOVEL("DIAMOND_SPADE"), + DIAMOND_SWORD, + DIORITE(3, "STONE"), + DIORITE_SLAB, + DIORITE_STAIRS, + DIORITE_WALL, + DIRT, + /** + * Changed in 1.17 + */ + DIRT_PATH("GRASS_PATH"), + DISC_FRAGMENT_5, + DISPENSER, + DOLPHIN_SPAWN_EGG, + DONKEY_SPAWN_EGG(32, "MONSTER_EGG"), + DRAGON_BREATH("DRAGONS_BREATH"), + DRAGON_EGG, + DRAGON_HEAD(5, "SKULL", "SKULL_ITEM"), + DRAGON_WALL_HEAD(5, "SKULL", "SKULL_ITEM"), + DRIED_KELP, + DRIED_KELP_BLOCK, + DRIPSTONE_BLOCK, + DROPPER, + DROWNED_SPAWN_EGG, + ECHO_SHARD, + EGG, + ELDER_GUARDIAN_SPAWN_EGG(4, "MONSTER_EGG"), + ELYTRA, + EMERALD, + EMERALD_BLOCK, + EMERALD_ORE, + ENCHANTED_BOOK, + ENCHANTED_GOLDEN_APPLE(1, "GOLDEN_APPLE"), + ENCHANTING_TABLE("ENCHANTMENT_TABLE"), + ENDERMAN_SPAWN_EGG(58, "MONSTER_EGG"), + ENDERMITE_SPAWN_EGG(67, "MONSTER_EGG"), + ENDER_CHEST, + ENDER_EYE("EYE_OF_ENDER"), + ENDER_PEARL, + END_CRYSTAL, + END_GATEWAY, + END_PORTAL("ENDER_PORTAL"), + END_PORTAL_FRAME("ENDER_PORTAL_FRAME"), + END_ROD, + END_STONE("ENDER_STONE"), + END_STONE_BRICKS("END_BRICKS"), + END_STONE_BRICK_SLAB, + END_STONE_BRICK_STAIRS, + END_STONE_BRICK_WALL, + EVOKER_SPAWN_EGG(34, "MONSTER_EGG"), + EXPERIENCE_BOTTLE("EXP_BOTTLE"), + EXPOSED_COPPER, + EXPOSED_CUT_COPPER, + EXPOSED_CUT_COPPER_SLAB, + EXPOSED_CUT_COPPER_STAIRS, + FARMLAND("SOIL"), + FEATHER, + FERMENTED_SPIDER_EYE, + FERN(2, "LONG_GRASS"), + /** + * For some reasons filled map items are really special. + * Their data value starts from 0 and every time a player + * creates a new map that maps data value increases. + * https://github.com/CryptoMorin/XSeries/issues/91 + */ + FILLED_MAP("MAP"), + FIRE, + FIREWORK_ROCKET("FIREWORK"), + FIREWORK_STAR("FIREWORK_CHARGE"), + FIRE_CHARGE("FIREBALL"), + FIRE_CORAL, + FIRE_CORAL_BLOCK, + FIRE_CORAL_FAN, + FIRE_CORAL_WALL_FAN, + FISHING_ROD, + FLETCHING_TABLE, + FLINT, + FLINT_AND_STEEL, + FLOWERING_AZALEA, + FLOWERING_AZALEA_LEAVES, + FLOWER_BANNER_PATTERN, + FLOWER_POT("FLOWER_POT", "FLOWER_POT_ITEM"), + FOX_SPAWN_EGG, + FROGSPAWN, + FROG_SPAWN_EGG, + /** + * This special material cannot be obtained as an item. + */ + FROSTED_ICE, + FURNACE("BURNING_FURNACE"), + FURNACE_MINECART("POWERED_MINECART"), + GHAST_SPAWN_EGG(56, "MONSTER_EGG"), + GHAST_TEAR, + GILDED_BLACKSTONE, + GLASS, + GLASS_BOTTLE, + GLASS_PANE("THIN_GLASS"), + GLISTERING_MELON_SLICE("SPECKLED_MELON"), + GLOBE_BANNER_PATTERN, + GLOWSTONE, + GLOWSTONE_DUST, + GLOW_BERRIES, + GLOW_INK_SAC, + GLOW_ITEM_FRAME, + GLOW_LICHEN, + GLOW_SQUID_SPAWN_EGG, + GOAT_HORN, + GOAT_SPAWN_EGG, + GOLDEN_APPLE, + GOLDEN_AXE("GOLD_AXE"), + GOLDEN_BOOTS("GOLD_BOOTS"), + GOLDEN_CARROT, + GOLDEN_CHESTPLATE("GOLD_CHESTPLATE"), + GOLDEN_HELMET("GOLD_HELMET"), + GOLDEN_HOE("GOLD_HOE"), + GOLDEN_HORSE_ARMOR("GOLD_BARDING"), + GOLDEN_LEGGINGS("GOLD_LEGGINGS"), + GOLDEN_PICKAXE("GOLD_PICKAXE"), + GOLDEN_SHOVEL("GOLD_SPADE"), + GOLDEN_SWORD("GOLD_SWORD"), + GOLD_BLOCK, + GOLD_INGOT, + GOLD_NUGGET, + GOLD_ORE, + GRANITE(1, "STONE"), + GRANITE_SLAB, + GRANITE_STAIRS, + GRANITE_WALL, + GRASS(1, "LONG_GRASS"), + GRASS_BLOCK("GRASS"), + GRAVEL, + GRAY_BANNER(8, "STANDING_BANNER", "BANNER"), + GRAY_BED(supports(12) ? 7 : 0, "BED_BLOCK", "BED"), + GRAY_CANDLE, + GRAY_CANDLE_CAKE, + GRAY_CARPET(7, "CARPET"), + GRAY_CONCRETE(7, "CONCRETE"), + GRAY_CONCRETE_POWDER(7, "CONCRETE_POWDER"), + GRAY_DYE(8, "INK_SACK"), + GRAY_GLAZED_TERRACOTTA, + GRAY_SHULKER_BOX, + GRAY_STAINED_GLASS(7, "STAINED_GLASS"), + GRAY_STAINED_GLASS_PANE(7, "THIN_GLASS", "STAINED_GLASS_PANE"), + GRAY_TERRACOTTA(7, "STAINED_CLAY"), + GRAY_WALL_BANNER(8, "WALL_BANNER"), + GRAY_WOOL(7, "WOOL"), + GREEN_BANNER(2, "STANDING_BANNER", "BANNER"), + GREEN_BED(supports(12) ? 13 : 0, "BED_BLOCK", "BED"), + GREEN_CANDLE, + GREEN_CANDLE_CAKE, + GREEN_CARPET(13, "CARPET"), + GREEN_CONCRETE(13, "CONCRETE"), + GREEN_CONCRETE_POWDER(13, "CONCRETE_POWDER"), + /** + * 1.13 renamed to CACTUS_GREEN + * 1.14 renamed to GREEN_DYE + */ + GREEN_DYE(2, "INK_SACK", "CACTUS_GREEN"), + GREEN_GLAZED_TERRACOTTA, + GREEN_SHULKER_BOX, + GREEN_STAINED_GLASS(13, "STAINED_GLASS"), + GREEN_STAINED_GLASS_PANE(13, "THIN_GLASS", "STAINED_GLASS_PANE"), + GREEN_TERRACOTTA(13, "STAINED_CLAY"), + GREEN_WALL_BANNER(2, "WALL_BANNER"), + GREEN_WOOL(13, "WOOL"), + GRINDSTONE, + GUARDIAN_SPAWN_EGG(68, "MONSTER_EGG"), + GUNPOWDER("SULPHUR"), + HANGING_ROOTS, + HAY_BLOCK, + HEART_OF_THE_SEA, + HEAVY_WEIGHTED_PRESSURE_PLATE("IRON_PLATE"), + HOGLIN_SPAWN_EGG("MONSTER_EGG"), + HONEYCOMB, + HONEYCOMB_BLOCK, + HONEY_BLOCK, + HONEY_BOTTLE, + HOPPER, + HOPPER_MINECART, + HORN_CORAL, + HORN_CORAL_BLOCK, + HORN_CORAL_FAN, + HORN_CORAL_WALL_FAN, + HORSE_SPAWN_EGG(100, "MONSTER_EGG"), + HUSK_SPAWN_EGG(23, "MONSTER_EGG"), + ICE, + INFESTED_CHISELED_STONE_BRICKS(5, "MONSTER_EGGS"), + INFESTED_COBBLESTONE(1, "MONSTER_EGGS"), + INFESTED_CRACKED_STONE_BRICKS(4, "MONSTER_EGGS"), + INFESTED_DEEPSLATE, + INFESTED_MOSSY_STONE_BRICKS(3, "MONSTER_EGGS"), + INFESTED_STONE("MONSTER_EGGS"), + INFESTED_STONE_BRICKS(2, "MONSTER_EGGS"), + /** + * We will only add "INK_SAC" for {@link #BLACK_DYE} since it's + * the only material (linked with this material) that is added + * after 1.13, which means it can use both INK_SACK and INK_SAC. + */ + INK_SAC("INK_SACK"), + IRON_AXE, + IRON_BARS("IRON_FENCE"), + IRON_BLOCK, + IRON_BOOTS, + IRON_CHESTPLATE, + IRON_DOOR("IRON_DOOR_BLOCK"), + IRON_HELMET, + IRON_HOE, + IRON_HORSE_ARMOR("IRON_BARDING"), + IRON_INGOT, + IRON_LEGGINGS, + IRON_NUGGET, + IRON_ORE, + IRON_PICKAXE, + IRON_SHOVEL("IRON_SPADE"), + IRON_SWORD, + IRON_TRAPDOOR, + ITEM_FRAME, + JACK_O_LANTERN, + JIGSAW, + JUKEBOX, + JUNGLE_BOAT("BOAT_JUNGLE"), + JUNGLE_BUTTON("WOOD_BUTTON"), + JUNGLE_CHEST_BOAT, + JUNGLE_DOOR("JUNGLE_DOOR", "JUNGLE_DOOR_ITEM"), + JUNGLE_FENCE, + JUNGLE_FENCE_GATE, + JUNGLE_LEAVES(3, "LEAVES"), + JUNGLE_LOG(3, "LOG"), + JUNGLE_PLANKS(3, "WOOD"), + JUNGLE_PRESSURE_PLATE("WOOD_PLATE"), + JUNGLE_SAPLING(3, "SAPLING"), + JUNGLE_SIGN("SIGN_POST", "SIGN"), + JUNGLE_SLAB(3, "WOOD_DOUBLE_STEP", "WOOD_STEP", "WOODEN_SLAB"), + JUNGLE_STAIRS("JUNGLE_WOOD_STAIRS"), + JUNGLE_TRAPDOOR("TRAP_DOOR"), + JUNGLE_WALL_SIGN("WALL_SIGN"), + JUNGLE_WOOD(3, "LOG"), + KELP, + KELP_PLANT, + KNOWLEDGE_BOOK("BOOK"), + LADDER, + LANTERN, + LAPIS_BLOCK, + LAPIS_LAZULI(4, "INK_SACK"), + LAPIS_ORE, + LARGE_AMETHYST_BUD, + LARGE_FERN(3, "DOUBLE_PLANT"), + LAVA("STATIONARY_LAVA"), + LAVA_BUCKET, + LAVA_CAULDRON, + LEAD("LEASH"), + LEATHER, + LEATHER_BOOTS, + LEATHER_CHESTPLATE, + LEATHER_HELMET, + LEATHER_HORSE_ARMOR("IRON_HORSE_ARMOR"), + LEATHER_LEGGINGS, + LECTERN, + LEVER, + LIGHT, + LIGHTNING_ROD, + LIGHT_BLUE_BANNER(12, "STANDING_BANNER", "BANNER"), + LIGHT_BLUE_BED(supports(12) ? 3 : 0, "BED_BLOCK", "BED"), + LIGHT_BLUE_CANDLE, + LIGHT_BLUE_CANDLE_CAKE, + LIGHT_BLUE_CARPET(3, "CARPET"), + LIGHT_BLUE_CONCRETE(3, "CONCRETE"), + LIGHT_BLUE_CONCRETE_POWDER(3, "CONCRETE_POWDER"), + LIGHT_BLUE_DYE(12, "INK_SACK"), + LIGHT_BLUE_GLAZED_TERRACOTTA, + LIGHT_BLUE_SHULKER_BOX, + LIGHT_BLUE_STAINED_GLASS(3, "STAINED_GLASS"), + LIGHT_BLUE_STAINED_GLASS_PANE(3, "THIN_GLASS", "STAINED_GLASS_PANE"), + LIGHT_BLUE_TERRACOTTA(3, "STAINED_CLAY"), + LIGHT_BLUE_WALL_BANNER(12, "WALL_BANNER", "STANDING_BANNER", "BANNER"), + LIGHT_BLUE_WOOL(3, "WOOL"), + LIGHT_GRAY_BANNER(7, "STANDING_BANNER", "BANNER"), + LIGHT_GRAY_BED(supports(12) ? 8 : 0, "BED_BLOCK", "BED"), + LIGHT_GRAY_CANDLE, + LIGHT_GRAY_CANDLE_CAKE, + LIGHT_GRAY_CARPET(8, "CARPET"), + LIGHT_GRAY_CONCRETE(8, "CONCRETE"), + LIGHT_GRAY_CONCRETE_POWDER(8, "CONCRETE_POWDER"), + LIGHT_GRAY_DYE(7, "INK_SACK"), + /** + * Renamed to SILVER_GLAZED_TERRACOTTA in 1.12 + * Renamed to LIGHT_GRAY_GLAZED_TERRACOTTA in 1.14 + */ + LIGHT_GRAY_GLAZED_TERRACOTTA("SILVER_GLAZED_TERRACOTTA"), + LIGHT_GRAY_SHULKER_BOX("SILVER_SHULKER_BOX"), + LIGHT_GRAY_STAINED_GLASS(8, "STAINED_GLASS"), + LIGHT_GRAY_STAINED_GLASS_PANE(8, "THIN_GLASS", "STAINED_GLASS_PANE"), + LIGHT_GRAY_TERRACOTTA(8, "STAINED_CLAY"), + LIGHT_GRAY_WALL_BANNER(7, "WALL_BANNER"), + LIGHT_GRAY_WOOL(8, "WOOL"), + LIGHT_WEIGHTED_PRESSURE_PLATE("GOLD_PLATE"), + LILAC(1, "DOUBLE_PLANT"), + LILY_OF_THE_VALLEY, + LILY_PAD("WATER_LILY"), + LIME_BANNER(10, "STANDING_BANNER", "BANNER"), + LIME_BED(supports(12) ? 5 : 0, "BED_BLOCK", "BED"), + LIME_CANDLE, + LIME_CANDLE_CAKE, + LIME_CARPET(5, "CARPET"), + LIME_CONCRETE(5, "CONCRETE"), + LIME_CONCRETE_POWDER(5, "CONCRETE_POWDER"), + LIME_DYE(10, "INK_SACK"), + LIME_GLAZED_TERRACOTTA, + LIME_SHULKER_BOX, + LIME_STAINED_GLASS(5, "STAINED_GLASS"), + LIME_STAINED_GLASS_PANE(5, "STAINED_GLASS_PANE"), + LIME_TERRACOTTA(5, "STAINED_CLAY"), + LIME_WALL_BANNER(10, "WALL_BANNER"), + LIME_WOOL(5, "WOOL"), + LINGERING_POTION, + LLAMA_SPAWN_EGG(103, "MONSTER_EGG"), + LODESTONE, + LOOM, + MAGENTA_BANNER(13, "STANDING_BANNER", "BANNER"), + MAGENTA_BED(supports(12) ? 2 : 0, "BED_BLOCK", "BED"), + MAGENTA_CANDLE, + MAGENTA_CANDLE_CAKE, + MAGENTA_CARPET(2, "CARPET"), + MAGENTA_CONCRETE(2, "CONCRETE"), + MAGENTA_CONCRETE_POWDER(2, "CONCRETE_POWDER"), + MAGENTA_DYE(13, "INK_SACK"), + MAGENTA_GLAZED_TERRACOTTA, + MAGENTA_SHULKER_BOX, + MAGENTA_STAINED_GLASS(2, "STAINED_GLASS"), + MAGENTA_STAINED_GLASS_PANE(2, "THIN_GLASS", "STAINED_GLASS_PANE"), + MAGENTA_TERRACOTTA(2, "STAINED_CLAY"), + MAGENTA_WALL_BANNER(13, "WALL_BANNER"), + MAGENTA_WOOL(2, "WOOL"), + MAGMA_BLOCK("MAGMA"), + MAGMA_CREAM, + MAGMA_CUBE_SPAWN_EGG(62, "MONSTER_EGG"), + MANGROVE_BOAT, + MANGROVE_BUTTON, + MANGROVE_CHEST_BOAT, + MANGROVE_DOOR, + MANGROVE_FENCE, + MANGROVE_FENCE_GATE, + MANGROVE_LEAVES, + MANGROVE_LOG, + MANGROVE_PLANKS, + MANGROVE_PRESSURE_PLATE, + MANGROVE_PROPAGULE, + MANGROVE_ROOTS, + MANGROVE_SIGN, + MANGROVE_SLAB, + MANGROVE_STAIRS, + MANGROVE_TRAPDOOR, + MANGROVE_WALL_SIGN, + MANGROVE_WOOD, + /** + * Adding this to the duplicated list will give you a filled map + * for 1.13+ versions and removing it from duplicated list will + * still give you a filled map in -1.12 versions. + * Since higher versions are our priority I'll keep 1.13+ support + * until I can come up with something to fix it. + */ + MAP("EMPTY_MAP"), + MEDIUM_AMETHYST_BUD, + MELON("MELON_BLOCK"), + MELON_SEEDS, + MELON_SLICE("MELON"), + MELON_STEM, + MILK_BUCKET, + MINECART, + MOJANG_BANNER_PATTERN, + MOOSHROOM_SPAWN_EGG(96, "MONSTER_EGG"), + MOSSY_COBBLESTONE, + MOSSY_COBBLESTONE_SLAB(), + MOSSY_COBBLESTONE_STAIRS, + MOSSY_COBBLESTONE_WALL(1, "COBBLE_WALL", "COBBLESTONE_WALL"), + MOSSY_STONE_BRICKS(1, "SMOOTH_BRICK"), + MOSSY_STONE_BRICK_SLAB, + MOSSY_STONE_BRICK_STAIRS, + MOSSY_STONE_BRICK_WALL, + MOSS_BLOCK, + MOSS_CARPET, + MOVING_PISTON("PISTON_MOVING_PIECE"), + MUD, + MUDDY_MANGROVE_ROOTS, + MUD_BRICKS, + MUD_BRICK_SLAB, + MUD_BRICK_STAIRS, + MUD_BRICK_WALL, + MULE_SPAWN_EGG(32, "MONSTER_EGG"), + MUSHROOM_STEM("BROWN_MUSHROOM"), + MUSHROOM_STEW("MUSHROOM_SOUP"), + MUSIC_DISC_11("RECORD_11"), + MUSIC_DISC_13("GOLD_RECORD"), + MUSIC_DISC_5, + MUSIC_DISC_BLOCKS("RECORD_3"), + MUSIC_DISC_CAT("GREEN_RECORD"), + MUSIC_DISC_CHIRP("RECORD_4"), + MUSIC_DISC_FAR("RECORD_5"), + MUSIC_DISC_MALL("RECORD_6"), + MUSIC_DISC_MELLOHI("RECORD_7"), + MUSIC_DISC_OTHERSIDE, + MUSIC_DISC_PIGSTEP, + MUSIC_DISC_STAL("RECORD_8"), + MUSIC_DISC_STRAD("RECORD_9"), + MUSIC_DISC_WAIT("RECORD_12"), + MUSIC_DISC_WARD("RECORD_10"), + MUTTON, + MYCELIUM("MYCEL"), + NAME_TAG, + NAUTILUS_SHELL, + NETHERITE_AXE, + NETHERITE_BLOCK, + NETHERITE_BOOTS, + NETHERITE_CHESTPLATE, + NETHERITE_HELMET, + NETHERITE_HOE, + NETHERITE_INGOT, + NETHERITE_LEGGINGS, + NETHERITE_PICKAXE, + NETHERITE_SCRAP, + NETHERITE_SHOVEL, + NETHERITE_SWORD, + NETHERRACK, + NETHER_BRICK("NETHER_BRICK_ITEM"), + NETHER_BRICKS("NETHER_BRICK"), + NETHER_BRICK_FENCE("NETHER_FENCE"), + NETHER_BRICK_SLAB(6, "STEP"), + NETHER_BRICK_STAIRS, + NETHER_BRICK_WALL, + NETHER_GOLD_ORE, + NETHER_PORTAL("PORTAL"), + NETHER_QUARTZ_ORE("QUARTZ_ORE"), + NETHER_SPROUTS, + NETHER_STAR, + /** + * Just like mentioned in https://minecraft.gamepedia.com/Nether_Wart + * Nether wart is also known as nether stalk in the code. + * NETHER_STALK is the planted state of nether warts. + */ + NETHER_WART("NETHER_WARTS", "NETHER_STALK"), + NETHER_WART_BLOCK, + NOTE_BLOCK, + OAK_BOAT("BOAT"), + OAK_BUTTON("WOOD_BUTTON"), + OAK_CHEST_BOAT, + OAK_DOOR("WOODEN_DOOR", "WOOD_DOOR"), + OAK_FENCE("FENCE"), + OAK_FENCE_GATE("FENCE_GATE"), + OAK_LEAVES("LEAVES"), + OAK_LOG("LOG"), + OAK_PLANKS("WOOD"), + OAK_PRESSURE_PLATE("WOOD_PLATE"), + OAK_SAPLING("SAPLING"), + OAK_SIGN("SIGN_POST", "SIGN"), + OAK_SLAB("WOOD_DOUBLE_STEP", "WOOD_STEP", "WOODEN_SLAB"), + OAK_STAIRS("WOOD_STAIRS"), + OAK_TRAPDOOR("TRAP_DOOR"), + OAK_WALL_SIGN("WALL_SIGN"), + OAK_WOOD("LOG"), + OBSERVER, + OBSIDIAN, + OCELOT_SPAWN_EGG(98, "MONSTER_EGG"), + OCHRE_FROGLIGHT, + ORANGE_BANNER(14, "STANDING_BANNER", "BANNER"), + ORANGE_BED(supports(12) ? 1 : 0, "BED_BLOCK", "BED"), + ORANGE_CANDLE, + ORANGE_CANDLE_CAKE, + ORANGE_CARPET(1, "CARPET"), + ORANGE_CONCRETE(1, "CONCRETE"), + ORANGE_CONCRETE_POWDER(1, "CONCRETE_POWDER"), + ORANGE_DYE(14, "INK_SACK"), + ORANGE_GLAZED_TERRACOTTA, + ORANGE_SHULKER_BOX, + ORANGE_STAINED_GLASS(1, "STAINED_GLASS"), + ORANGE_STAINED_GLASS_PANE(1, "STAINED_GLASS_PANE"), + ORANGE_TERRACOTTA(1, "STAINED_CLAY"), + ORANGE_TULIP(5, "RED_ROSE"), + ORANGE_WALL_BANNER(14, "WALL_BANNER"), + ORANGE_WOOL(1, "WOOL"), + OXEYE_DAISY(8, "RED_ROSE"), + OXIDIZED_COPPER, + OXIDIZED_CUT_COPPER, + OXIDIZED_CUT_COPPER_SLAB, + OXIDIZED_CUT_COPPER_STAIRS, + PACKED_ICE, + PACKED_MUD, + PAINTING, + PANDA_SPAWN_EGG, + PAPER, + PARROT_SPAWN_EGG(105, "MONSTER_EGG"), + PEARLESCENT_FROGLIGHT, + PEONY(5, "DOUBLE_PLANT"), + PETRIFIED_OAK_SLAB("WOOD_STEP"), + PHANTOM_MEMBRANE, + PHANTOM_SPAWN_EGG, + PIGLIN_BANNER_PATTERN, + PIGLIN_BRUTE_SPAWN_EGG, + PIGLIN_SPAWN_EGG(57, "MONSTER_EGG"), + PIG_SPAWN_EGG(90, "MONSTER_EGG"), + PILLAGER_SPAWN_EGG, + PINK_BANNER(9, "STANDING_BANNER", "BANNER"), + PINK_BED(supports(12) ? 6 : 0, "BED_BLOCK", "BED"), + PINK_CANDLE, + PINK_CANDLE_CAKE, + PINK_CARPET(6, "CARPET"), + PINK_CONCRETE(6, "CONCRETE"), + PINK_CONCRETE_POWDER(6, "CONCRETE_POWDER"), + PINK_DYE(9, "INK_SACK"), + PINK_GLAZED_TERRACOTTA, + PINK_SHULKER_BOX, + PINK_STAINED_GLASS(6, "STAINED_GLASS"), + PINK_STAINED_GLASS_PANE(6, "THIN_GLASS", "STAINED_GLASS_PANE"), + PINK_TERRACOTTA(6, "STAINED_CLAY"), + PINK_TULIP(7, "RED_ROSE"), + PINK_WALL_BANNER(9, "WALL_BANNER"), + PINK_WOOL(6, "WOOL"), + PISTON("PISTON_BASE"), + PISTON_HEAD("PISTON_EXTENSION"), + PLAYER_HEAD(3, "SKULL", "SKULL_ITEM"), + PLAYER_WALL_HEAD(3, "SKULL", "SKULL_ITEM"), + PODZOL(2, "DIRT"), + POINTED_DRIPSTONE, + POISONOUS_POTATO, + POLAR_BEAR_SPAWN_EGG(102, "MONSTER_EGG"), + POLISHED_ANDESITE(6, "STONE"), + POLISHED_ANDESITE_SLAB, + POLISHED_ANDESITE_STAIRS, + POLISHED_BASALT, + POLISHED_BLACKSTONE, + POLISHED_BLACKSTONE_BRICKS, + POLISHED_BLACKSTONE_BRICK_SLAB, + POLISHED_BLACKSTONE_BRICK_STAIRS, + POLISHED_BLACKSTONE_BRICK_WALL, + POLISHED_BLACKSTONE_BUTTON, + POLISHED_BLACKSTONE_PRESSURE_PLATE, + POLISHED_BLACKSTONE_SLAB, + POLISHED_BLACKSTONE_STAIRS, + POLISHED_BLACKSTONE_WALL, + POLISHED_DEEPSLATE, + POLISHED_DEEPSLATE_SLAB, + POLISHED_DEEPSLATE_STAIRS, + POLISHED_DEEPSLATE_WALL, + POLISHED_DIORITE(4, "STONE"), + POLISHED_DIORITE_SLAB, + POLISHED_DIORITE_STAIRS, + POLISHED_GRANITE(2, "STONE"), + POLISHED_GRANITE_SLAB, + POLISHED_GRANITE_STAIRS, + POPPED_CHORUS_FRUIT("CHORUS_FRUIT_POPPED"), + POPPY("RED_ROSE"), + PORKCHOP("PORK"), + POTATO("POTATO_ITEM"), + POTATOES("POTATO"), + POTION, + POTTED_ACACIA_SAPLING(4, "FLOWER_POT"), + POTTED_ALLIUM(2, "RED_ROSE", "FLOWER_POT"), + POTTED_AZALEA_BUSH, + POTTED_AZURE_BLUET(3, "RED_ROSE", "FLOWER_POT"), + POTTED_BAMBOO, + POTTED_BIRCH_SAPLING(2, "FLOWER_POT"), + POTTED_BLUE_ORCHID(1, "RED_ROSE", "FLOWER_POT"), + POTTED_BROWN_MUSHROOM("FLOWER_POT"), + POTTED_CACTUS("FLOWER_POT"), + POTTED_CORNFLOWER, + POTTED_CRIMSON_FUNGUS, + POTTED_CRIMSON_ROOTS, + POTTED_DANDELION("YELLOW_FLOWER", "FLOWER_POT"), + POTTED_DARK_OAK_SAPLING(5, "FLOWER_POT"), + POTTED_DEAD_BUSH("FLOWER_POT"), + POTTED_FERN(2, "LONG_GRASS", "FLOWER_POT"), + POTTED_FLOWERING_AZALEA_BUSH, + POTTED_JUNGLE_SAPLING(3, "FLOWER_POT"), + POTTED_LILY_OF_THE_VALLEY, + POTTED_MANGROVE_PROPAGULE, + POTTED_OAK_SAPLING("FLOWER_POT"), + POTTED_ORANGE_TULIP(5, "RED_ROSE", "FLOWER_POT"), + POTTED_OXEYE_DAISY(8, "RED_ROSE", "FLOWER_POT"), + POTTED_PINK_TULIP(7, "RED_ROSE", "FLOWER_POT"), + POTTED_POPPY("RED_ROSE", "FLOWER_POT"), + POTTED_RED_MUSHROOM("FLOWER_POT"), + POTTED_RED_TULIP(4, "RED_ROSE", "FLOWER_POT"), + POTTED_SPRUCE_SAPLING(1, "FLOWER_POT"), + POTTED_WARPED_FUNGUS, + POTTED_WARPED_ROOTS, + POTTED_WHITE_TULIP(6, "RED_ROSE", "FLOWER_POT"), + POTTED_WITHER_ROSE, + POWDER_SNOW, + POWDER_SNOW_BUCKET, + POWDER_SNOW_CAULDRON, + POWERED_RAIL, + PRISMARINE, + PRISMARINE_BRICKS(1, "PRISMARINE"), + PRISMARINE_BRICK_SLAB, + PRISMARINE_BRICK_STAIRS, + PRISMARINE_CRYSTALS, + PRISMARINE_SHARD, + PRISMARINE_SLAB, + PRISMARINE_STAIRS, + PRISMARINE_WALL, + PUFFERFISH(3, "RAW_FISH"), + PUFFERFISH_BUCKET, + PUFFERFISH_SPAWN_EGG, + PUMPKIN, + PUMPKIN_PIE, + PUMPKIN_SEEDS, + PUMPKIN_STEM, + PURPLE_BANNER(5, "STANDING_BANNER", "BANNER"), + PURPLE_BED(supports(12) ? 10 : 0, "BED_BLOCK", "BED"), + PURPLE_CANDLE, + PURPLE_CANDLE_CAKE, + PURPLE_CARPET(10, "CARPET"), + PURPLE_CONCRETE(10, "CONCRETE"), + PURPLE_CONCRETE_POWDER(10, "CONCRETE_POWDER"), + PURPLE_DYE(5, "INK_SACK"), + PURPLE_GLAZED_TERRACOTTA, + PURPLE_SHULKER_BOX, + PURPLE_STAINED_GLASS(10, "STAINED_GLASS"), + PURPLE_STAINED_GLASS_PANE(10, "THIN_GLASS", "STAINED_GLASS_PANE"), + PURPLE_TERRACOTTA(10, "STAINED_CLAY"), + PURPLE_WALL_BANNER(5, "WALL_BANNER"), + PURPLE_WOOL(10, "WOOL"), + PURPUR_BLOCK, + PURPUR_PILLAR, + PURPUR_SLAB("PURPUR_DOUBLE_SLAB"), + PURPUR_STAIRS, + QUARTZ, + QUARTZ_BLOCK, + QUARTZ_BRICKS, + QUARTZ_PILLAR(2, "QUARTZ_BLOCK"), + QUARTZ_SLAB(7, "STEP"), + QUARTZ_STAIRS, + RABBIT, + RABBIT_FOOT, + RABBIT_HIDE, + RABBIT_SPAWN_EGG(101, "MONSTER_EGG"), + RABBIT_STEW, + RAIL("RAILS"), + RAVAGER_SPAWN_EGG, + RAW_COPPER, + RAW_COPPER_BLOCK, + RAW_GOLD, + RAW_GOLD_BLOCK, + RAW_IRON, + RAW_IRON_BLOCK, + RECOVERY_COMPASS, + REDSTONE, + REDSTONE_BLOCK, + /** + * Unlike redstone torch, REDSTONE_LAMP_ON isn't an item. + * The name is just here on the list for matching. + * + * @see #REDSTONE_TORCH + */ + REDSTONE_LAMP("REDSTONE_LAMP_ON", "REDSTONE_LAMP_OFF"), + REDSTONE_ORE("GLOWING_REDSTONE_ORE"), + /** + * REDSTONE_TORCH_OFF isn't an item, but a block. + * But REDSTONE_TORCH_ON is the item. + * The name is just here on the list for matching. + */ + REDSTONE_TORCH("REDSTONE_TORCH_OFF", "REDSTONE_TORCH_ON"), + REDSTONE_WALL_TORCH, + REDSTONE_WIRE, + RED_BANNER(1, "STANDING_BANNER", "BANNER"), + /** + * Data value 14 or 0 + */ + RED_BED(supports(12) ? 14 : 0, "BED_BLOCK", "BED"), + RED_CANDLE, + RED_CANDLE_CAKE, + RED_CARPET(14, "CARPET"), + RED_CONCRETE(14, "CONCRETE"), + RED_CONCRETE_POWDER(14, "CONCRETE_POWDER"), + RED_DYE(1, "INK_SACK", "ROSE_RED"), + RED_GLAZED_TERRACOTTA, + RED_MUSHROOM, + RED_MUSHROOM_BLOCK("RED_MUSHROOM", "HUGE_MUSHROOM_2"), + RED_NETHER_BRICKS("RED_NETHER_BRICK"), + RED_NETHER_BRICK_SLAB, + RED_NETHER_BRICK_STAIRS, + RED_NETHER_BRICK_WALL, + RED_SAND(1, "SAND"), + RED_SANDSTONE, + RED_SANDSTONE_SLAB("DOUBLE_STONE_SLAB2", "STONE_SLAB2"), + RED_SANDSTONE_STAIRS, + RED_SANDSTONE_WALL, + RED_SHULKER_BOX, + RED_STAINED_GLASS(14, "STAINED_GLASS"), + RED_STAINED_GLASS_PANE(14, "THIN_GLASS", "STAINED_GLASS_PANE"), + RED_TERRACOTTA(14, "STAINED_CLAY"), + RED_TULIP(4, "RED_ROSE"), + RED_WALL_BANNER(1, "WALL_BANNER"), + RED_WOOL(14, "WOOL"), + REINFORCED_DEEPSLATE, + REPEATER("DIODE_BLOCK_ON", "DIODE_BLOCK_OFF", "DIODE"), + REPEATING_COMMAND_BLOCK("COMMAND", "COMMAND_REPEATING"), + RESPAWN_ANCHOR, + ROOTED_DIRT, + ROSE_BUSH(4, "DOUBLE_PLANT"), + ROTTEN_FLESH, + SADDLE, + SALMON(1, "RAW_FISH"), + SALMON_BUCKET, + SALMON_SPAWN_EGG, + SAND, + SANDSTONE, + SANDSTONE_SLAB(1, "DOUBLE_STEP", "STEP", "STONE_SLAB"), + SANDSTONE_STAIRS, + SANDSTONE_WALL, + SCAFFOLDING, + SCULK, + SCULK_CATALYST, + SCULK_SENSOR, + SCULK_SHRIEKER, + SCULK_VEIN, + SCUTE, + SEAGRASS, + SEA_LANTERN, + SEA_PICKLE, + SHEARS, + SHEEP_SPAWN_EGG(91, "MONSTER_EGG"), + SHIELD, + SHROOMLIGHT, + SHULKER_BOX("PURPLE_SHULKER_BOX"), + SHULKER_SHELL, + SHULKER_SPAWN_EGG(69, "MONSTER_EGG"), + SILVERFISH_SPAWN_EGG(60, "MONSTER_EGG"), + SKELETON_HORSE_SPAWN_EGG(28, "MONSTER_EGG"), + SKELETON_SKULL("SKULL", "SKULL_ITEM"), + SKELETON_SPAWN_EGG(51, "MONSTER_EGG"), + SKELETON_WALL_SKULL("SKULL", "SKULL_ITEM"), + SKULL_BANNER_PATTERN, + SLIME_BALL, + SLIME_BLOCK, + SLIME_SPAWN_EGG(55, "MONSTER_EGG"), + SMALL_AMETHYST_BUD, + SMALL_DRIPLEAF, + SMITHING_TABLE, + SMOKER, + SMOOTH_BASALT, + SMOOTH_QUARTZ, + SMOOTH_QUARTZ_SLAB, + SMOOTH_QUARTZ_STAIRS, + SMOOTH_RED_SANDSTONE(2, "RED_SANDSTONE"), + SMOOTH_RED_SANDSTONE_SLAB("STONE_SLAB2"), + SMOOTH_RED_SANDSTONE_STAIRS, + SMOOTH_SANDSTONE(2, "SANDSTONE"), + SMOOTH_SANDSTONE_SLAB, + SMOOTH_SANDSTONE_STAIRS, + SMOOTH_STONE, + SMOOTH_STONE_SLAB, + SNOW, + SNOWBALL("SNOW_BALL"), + SNOW_BLOCK, + SOUL_CAMPFIRE, + SOUL_FIRE, + SOUL_LANTERN, + SOUL_SAND, + SOUL_SOIL, + SOUL_TORCH, + SOUL_WALL_TORCH, + SPAWNER("MOB_SPAWNER"), + SPECTRAL_ARROW, + SPIDER_EYE, + SPIDER_SPAWN_EGG(52, "MONSTER_EGG"), + SPLASH_POTION, + SPONGE, + SPORE_BLOSSOM, + SPRUCE_BOAT("BOAT_SPRUCE"), + SPRUCE_BUTTON("WOOD_BUTTON"), + SPRUCE_CHEST_BOAT, + SPRUCE_DOOR("SPRUCE_DOOR", "SPRUCE_DOOR_ITEM"), + SPRUCE_FENCE, + SPRUCE_FENCE_GATE, + SPRUCE_LEAVES(1, "LEAVES"), + SPRUCE_LOG(1, "LOG"), + SPRUCE_PLANKS(1, "WOOD"), + SPRUCE_PRESSURE_PLATE("WOOD_PLATE"), + SPRUCE_SAPLING(1, "SAPLING"), + SPRUCE_SIGN("SIGN_POST", "SIGN"), + SPRUCE_SLAB(1, "WOOD_DOUBLE_STEP", "WOOD_STEP", "WOODEN_SLAB"), + SPRUCE_STAIRS("SPRUCE_WOOD_STAIRS"), + SPRUCE_TRAPDOOR("TRAP_DOOR"), + SPRUCE_WALL_SIGN("WALL_SIGN"), + SPRUCE_WOOD(1, "LOG"), + SPYGLASS, + SQUID_SPAWN_EGG(94, "MONSTER_EGG"), + STICK, + STICKY_PISTON("PISTON_BASE", "PISTON_STICKY_BASE"), + STONE, + STONECUTTER, + STONE_AXE, + STONE_BRICKS("SMOOTH_BRICK"), + STONE_BRICK_SLAB(5, "DOUBLE_STEP", "STEP", "STONE_SLAB"), + STONE_BRICK_STAIRS("SMOOTH_STAIRS"), + STONE_BRICK_WALL, + STONE_BUTTON, + STONE_HOE, + STONE_PICKAXE, + STONE_PRESSURE_PLATE("STONE_PLATE"), + STONE_SHOVEL("STONE_SPADE"), + STONE_SLAB("DOUBLE_STEP", "STEP"), + STONE_STAIRS, + STONE_SWORD, + STRAY_SPAWN_EGG(6, "MONSTER_EGG"), + STRIDER_SPAWN_EGG, + STRING, + STRIPPED_ACACIA_LOG, + STRIPPED_ACACIA_WOOD, + STRIPPED_BIRCH_LOG, + STRIPPED_BIRCH_WOOD, + STRIPPED_CRIMSON_HYPHAE, + STRIPPED_CRIMSON_STEM, + STRIPPED_DARK_OAK_LOG, + STRIPPED_DARK_OAK_WOOD, + STRIPPED_JUNGLE_LOG, + STRIPPED_JUNGLE_WOOD, + STRIPPED_MANGROVE_LOG, + STRIPPED_MANGROVE_WOOD, + STRIPPED_OAK_LOG, + STRIPPED_OAK_WOOD, + STRIPPED_SPRUCE_LOG, + STRIPPED_SPRUCE_WOOD, + STRIPPED_WARPED_HYPHAE, + STRIPPED_WARPED_STEM, + STRUCTURE_BLOCK, + /** + * Originally developers used barrier blocks for its purpose. + * So technically this isn't really considered as a suggested material. + */ + STRUCTURE_VOID(10, "BARRIER"), + SUGAR, + /** + * Sugar Cane is a known material in pre-1.13 + */ + SUGAR_CANE("SUGAR_CANE_BLOCK"), + SUNFLOWER("DOUBLE_PLANT"), + SUSPICIOUS_STEW, + SWEET_BERRIES, + SWEET_BERRY_BUSH, + TADPOLE_BUCKET, + TADPOLE_SPAWN_EGG, + TALL_GRASS(2, "DOUBLE_PLANT"), + TALL_SEAGRASS, + TARGET, + TERRACOTTA("HARD_CLAY"), + TINTED_GLASS, + TIPPED_ARROW, + TNT, + TNT_MINECART("EXPLOSIVE_MINECART"), + TORCH, + TOTEM_OF_UNDYING("TOTEM"), + TRADER_LLAMA_SPAWN_EGG, + TRAPPED_CHEST, + TRIDENT, + TRIPWIRE, + TRIPWIRE_HOOK, + TROPICAL_FISH(2, "RAW_FISH"), + TROPICAL_FISH_BUCKET("BUCKET", "WATER_BUCKET"), + TROPICAL_FISH_SPAWN_EGG("MONSTER_EGG"), + TUBE_CORAL, + TUBE_CORAL_BLOCK, + TUBE_CORAL_FAN, + TUBE_CORAL_WALL_FAN, + TUFF, + TURTLE_EGG, + TURTLE_HELMET, + TURTLE_SPAWN_EGG, + TWISTING_VINES, + TWISTING_VINES_PLANT, + VERDANT_FROGLIGHT, + VEX_SPAWN_EGG(35, "MONSTER_EGG"), + VILLAGER_SPAWN_EGG(120, "MONSTER_EGG"), + VINDICATOR_SPAWN_EGG(36, "MONSTER_EGG"), + VINE, + /** + * 1.13 tag is not added because it's the same thing as {@link #AIR} + * + * @see #CAVE_AIR + */ + VOID_AIR("AIR"), + WALL_TORCH("TORCH"), + WANDERING_TRADER_SPAWN_EGG, + WARDEN_SPAWN_EGG, + WARPED_BUTTON, + WARPED_DOOR, + WARPED_FENCE, + WARPED_FENCE_GATE, + WARPED_FUNGUS, + WARPED_FUNGUS_ON_A_STICK, + WARPED_HYPHAE, + WARPED_NYLIUM, + WARPED_PLANKS, + WARPED_PRESSURE_PLATE, + WARPED_ROOTS, + WARPED_SIGN("SIGN_POST"), + WARPED_SLAB, + WARPED_STAIRS, + WARPED_STEM, + WARPED_TRAPDOOR, + WARPED_WALL_SIGN("WALL_SIGN"), + WARPED_WART_BLOCK, + /** + * This is used for blocks only. + * In 1.13- WATER will turn into STATIONARY_WATER after it finished spreading. + * After 1.13+ this uses + * https://hub.spigotmc.org/javadocs/spigot/org/bukkit/block/data/Levelled.html water flowing system. + */ + WATER("STATIONARY_WATER"), + WATER_BUCKET, + WATER_CAULDRON, + WAXED_COPPER_BLOCK, + WAXED_CUT_COPPER, + WAXED_CUT_COPPER_SLAB, + WAXED_CUT_COPPER_STAIRS, + WAXED_EXPOSED_COPPER, + WAXED_EXPOSED_CUT_COPPER, + WAXED_EXPOSED_CUT_COPPER_SLAB, + WAXED_EXPOSED_CUT_COPPER_STAIRS, + WAXED_OXIDIZED_COPPER, + WAXED_OXIDIZED_CUT_COPPER, + WAXED_OXIDIZED_CUT_COPPER_SLAB, + WAXED_OXIDIZED_CUT_COPPER_STAIRS, + WAXED_WEATHERED_COPPER, + WAXED_WEATHERED_CUT_COPPER, + WAXED_WEATHERED_CUT_COPPER_SLAB, + WAXED_WEATHERED_CUT_COPPER_STAIRS, + WEATHERED_COPPER, + WEATHERED_CUT_COPPER, + WEATHERED_CUT_COPPER_SLAB, + WEATHERED_CUT_COPPER_STAIRS, + WEEPING_VINES, + WEEPING_VINES_PLANT, + WET_SPONGE(1, "SPONGE"), + /** + * Wheat is a known material in pre-1.13 + */ + WHEAT("CROPS"), + WHEAT_SEEDS("SEEDS"), + WHITE_BANNER(15, "STANDING_BANNER", "BANNER"), + WHITE_BED("BED_BLOCK", "BED"), + WHITE_CANDLE, + WHITE_CANDLE_CAKE, + WHITE_CARPET("CARPET"), + WHITE_CONCRETE("CONCRETE"), + WHITE_CONCRETE_POWDER("CONCRETE_POWDER"), + WHITE_DYE(15, "INK_SACK", "BONE_MEAL"), + WHITE_GLAZED_TERRACOTTA, + WHITE_SHULKER_BOX, + WHITE_STAINED_GLASS("STAINED_GLASS"), + WHITE_STAINED_GLASS_PANE("THIN_GLASS", "STAINED_GLASS_PANE"), + WHITE_TERRACOTTA("STAINED_CLAY"), + WHITE_TULIP(6, "RED_ROSE"), + WHITE_WALL_BANNER(15, "WALL_BANNER"), + WHITE_WOOL("WOOL"), + WITCH_SPAWN_EGG(66, "MONSTER_EGG"), + WITHER_ROSE, + WITHER_SKELETON_SKULL(1, "SKULL", "SKULL_ITEM"), + WITHER_SKELETON_SPAWN_EGG(5, "MONSTER_EGG"), + WITHER_SKELETON_WALL_SKULL(1, "SKULL", "SKULL_ITEM"), + WOLF_SPAWN_EGG(95, "MONSTER_EGG"), + WOODEN_AXE("WOOD_AXE"), + WOODEN_HOE("WOOD_HOE"), + WOODEN_PICKAXE("WOOD_PICKAXE"), + WOODEN_SHOVEL("WOOD_SPADE"), + WOODEN_SWORD("WOOD_SWORD"), + WRITABLE_BOOK("BOOK_AND_QUILL"), + WRITTEN_BOOK, + YELLOW_BANNER(11, "STANDING_BANNER", "BANNER"), + YELLOW_BED(supports(12) ? 4 : 0, "BED_BLOCK", "BED"), + YELLOW_CANDLE, + YELLOW_CANDLE_CAKE, + YELLOW_CARPET(4, "CARPET"), + YELLOW_CONCRETE(4, "CONCRETE"), + YELLOW_CONCRETE_POWDER(4, "CONCRETE_POWDER"), + YELLOW_DYE(11, "INK_SACK", "DANDELION_YELLOW"), + YELLOW_GLAZED_TERRACOTTA, + YELLOW_SHULKER_BOX, + YELLOW_STAINED_GLASS(4, "STAINED_GLASS"), + YELLOW_STAINED_GLASS_PANE(4, "THIN_GLASS", "STAINED_GLASS_PANE"), + YELLOW_TERRACOTTA(4, "STAINED_CLAY"), + YELLOW_WALL_BANNER(11, "WALL_BANNER"), + YELLOW_WOOL(4, "WOOL"), + ZOGLIN_SPAWN_EGG, + ZOMBIE_HEAD(2, "SKULL", "SKULL_ITEM"), + ZOMBIE_HORSE_SPAWN_EGG(29, "MONSTER_EGG"), + ZOMBIE_SPAWN_EGG(54, "MONSTER_EGG"), + ZOMBIE_VILLAGER_SPAWN_EGG(27, "MONSTER_EGG"), + ZOMBIE_WALL_HEAD(2, "SKULL", "SKULL_ITEM"), + ZOMBIFIED_PIGLIN_SPAWN_EGG(57, "MONSTER_EGG", "ZOMBIE_PIGMAN_SPAWN_EGG"); + + + /** + * Cached array of {@link XMaterial#values()} to avoid allocating memory for + * calling the method every time. + * + * @since 2.0.0 + */ + public static final XMaterial[] VALUES = values(); + + /** + * We don't want to use {@link Enums#getIfPresent(Class, String)} to avoid a few checks. + * + * @since 5.1.0 + */ + private static final Map NAMES = new HashMap<>(); + + /** + * Guava (Google Core Libraries for Java)'s cache for performance and timed caches. + * For strings that match a certain XMaterial. Mostly cached for configs. + * + * @since 1.0.0 + */ + private static final Cache NAME_CACHE = CacheBuilder.newBuilder() + .expireAfterAccess(1, TimeUnit.HOURS) + .build(); + /** + * This is used for {@link #isOneOf(Collection)} + * + * @since 3.4.0 + */ + private static final Cache CACHED_REGEX = CacheBuilder.newBuilder() + .expireAfterAccess(3, TimeUnit.HOURS) + .build(); + /** + * The maximum data value in the pre-flattening update which belongs to {@link #VILLAGER_SPAWN_EGG}
+ * https://minecraftitemids.com/types/spawn-egg + * + * @see #matchXMaterialWithData(String) + * @since 8.0.0 + */ + private static final byte MAX_DATA_VALUE = 120; + /** + * Used to tell the system that the passed object's (name or material) data value + * is not provided or is invalid. + * + * @since 8.0.0 + */ + private static final byte UNKNOWN_DATA_VALUE = -1; + /** + * The maximum material ID before the pre-flattening update which belongs to {@link #MUSIC_DISC_WAIT} + * + * @since 8.1.0 + */ + private static final short MAX_ID = 2267; + /** + * XMaterial Paradox (Duplication Check) + *

+ * A set of duplicated material names in 1.13 and 1.12 that will conflict with the legacy names. + * Values are the new material names. This map also contains illegal elements. Check the static initializer for more info. + *

+ * Duplications are not useful at all in versions above the flattening update {@link Data#ISFLAT} + * This set is only used for matching materials, for parsing refer to {@link #isDuplicated()} + * + * @since 3.0.0 + */ + private static final Set DUPLICATED; + + static { + for (XMaterial material : VALUES) NAMES.put(material.name(), material); + } + + static { + if (Data.ISFLAT) { + // It's not needed at all if it's the newer version. We can save some memory. + DUPLICATED = null; + } else { + // MELON_SLICE, CARROTS, POTATOES, BEETROOTS, GRASS_BLOCK, BRICKS, NETHER_BRICKS, BROWN_MUSHROOM + // Using the constructor to add elements will decide to allocate more size which we don't need. + DUPLICATED = new HashSet<>(4); + DUPLICATED.add(GRASS.name()); + DUPLICATED.add(MELON.name()); + DUPLICATED.add(BRICK.name()); + DUPLICATED.add(NETHER_BRICK.name()); + } + } + + /** + * The data value of this material https://minecraft.gamepedia.com/Java_Edition_data_values/Pre-flattening + * It's never a negative number. + * + * @see #getData() + */ + private final byte data; + /** + * A list of material names that was being used for older verions. + * + * @see #getLegacy() + */ + @Nonnull + private final String[] legacy; + /** + * The cached Bukkit parsed material. + * + * @see #parseMaterial() + * @since 9.0.0 + */ + @Nullable + private final Material material; + + XMaterial(int data, @Nonnull String... legacy) { + this.data = (byte) data; + this.legacy = legacy; + + Material mat = null; + if ((!Data.ISFLAT && this.isDuplicated()) || (mat = Material.getMaterial(this.name())) == null) { + for (int i = legacy.length - 1; i >= 0; i--) { + mat = Material.getMaterial(legacy[i]); + if (mat != null) break; + } + } + this.material = mat; + } + + XMaterial(String... legacy) {this(0, legacy);} + + /** + * Checks if the version is 1.13 Aquatic Update or higher. + * An invocation of this method yields the cached result from the expression: + *

+ *

+ * {@link #supports(int) 13}} + *
+ * + * @return true if 1.13 or higher. + * @see #getVersion() + * @see #supports(int) + * @since 1.0.0 + * @deprecated Use {@code XMaterial.supports(13)} instead. This method name can be confusing. + */ + @Deprecated + public static boolean isNewVersion() { + return Data.ISFLAT; + } + + /** + * This is just an extra method that can be used for many cases. + * It can be used in {@link org.bukkit.event.player.PlayerInteractEvent} + * or when accessing {@link org.bukkit.entity.Player#getMainHand()}, + * or other compatibility related methods. + *

+ * An invocation of this method yields exactly the same result as the expression: + *

+ *

+ * !{@link #supports(int)} 9 + *
+ * + * @since 2.0.0 + * @deprecated Use {@code !XMaterial.supports(9)} instead. + */ + @Deprecated + public static boolean isOneEight() { + return !supports(9); + } + + /** + * Gets the XMaterial with this name similar to {@link #valueOf(String)} + * without throwing an exception. + * + * @param name the name of the material. + * + * @return an optional that can be empty. + * @since 5.1.0 + */ + @Nonnull + private static Optional getIfPresent(@Nonnull String name) { + return Optional.ofNullable(NAMES.get(name)); + } + + /** + * The current version of the server. + * + * @return the current server version minor number. + * @see #supports(int) + * @since 2.0.0 + */ + public static int getVersion() { + return Data.VERSION; + } + + /** + * When using 1.13+, this helps to find the old material name + * with its data value using a cached search for optimization. + * + * @see #matchDefinedXMaterial(String, byte) + * @since 1.0.0 + */ + @Nullable + private static XMaterial requestOldXMaterial(@Nonnull String name, byte data) { + String holder = name + data; + XMaterial cache = NAME_CACHE.getIfPresent(holder); + if (cache != null) return cache; + + for (XMaterial material : VALUES) { + // Not using material.name().equals(name) check is intended. + if ((data == UNKNOWN_DATA_VALUE || data == material.data) && material.anyMatchLegacy(name)) { + NAME_CACHE.put(holder, material); + return material; + } + } + + return null; + } + + /** + * Parses the given material name as an XMaterial with a given data + * value in the string if attached. Check {@link #matchXMaterialWithData(String)} for more info. + * + * @see #matchXMaterialWithData(String) + * @see #matchDefinedXMaterial(String, byte) + * @since 2.0.0 + */ + @Nonnull + public static Optional matchXMaterial(@Nonnull String name) { + if (name == null || name.isEmpty()) throw new IllegalArgumentException("Cannot match a material with null or empty material name"); + Optional oldMatch = matchXMaterialWithData(name); + return oldMatch.isPresent() ? oldMatch : matchDefinedXMaterial(format(name), UNKNOWN_DATA_VALUE); + } + + /** + * Parses material name and data value from the specified string. + * The separator for the material name and its data value is {@code :} + * Spaces are allowed. Mostly used when getting materials from config for old school minecrafters. + *

+ * Examples + *

+     *     {@code INK_SACK:1 -> RED_DYE}
+     *     {@code WOOL: 14  -> RED_WOOL}
+     * 
+ * + * @param name the material string that consists of the material name, data and separator character. + * + * @return the parsed XMaterial. + * @see #matchXMaterial(String) + * @since 3.0.0 + */ + @Nonnull + private static Optional matchXMaterialWithData(@Nonnull String name) { + int index = name.indexOf(':'); + if (index != -1) { + String mat = format(name.substring(0, index)); + try { + // We don't use Byte.parseByte because we have our own range check. + byte data = (byte) Integer.parseInt(name.substring(index + 1).replace(" ", "")); + return data >= 0 && data < MAX_DATA_VALUE ? matchDefinedXMaterial(mat, data) : matchDefinedXMaterial(mat, UNKNOWN_DATA_VALUE); + } catch (NumberFormatException ignored) { + return matchDefinedXMaterial(mat, UNKNOWN_DATA_VALUE); + } + } + + return Optional.empty(); + } + + /** + * Parses the given material as an XMaterial. + * + * @throws IllegalArgumentException may be thrown as an unexpected exception. + * @see #matchDefinedXMaterial(String, byte) + * @see #matchXMaterial(ItemStack) + * @since 2.0.0 + */ + @Nonnull + public static XMaterial matchXMaterial(@Nonnull Material material) { + Objects.requireNonNull(material, "Cannot match null material"); + return matchDefinedXMaterial(material.name(), UNKNOWN_DATA_VALUE) + .orElseThrow(() -> new IllegalArgumentException("Unsupported material with no data value: " + material.name())); + } + + /** + * Parses the given item as an XMaterial using its material and data value (durability) + * if not a damageable item {@link ItemStack#getDurability()}. + * + * @param item the ItemStack to match. + * + * @return an XMaterial if matched any. + * @throws IllegalArgumentException may be thrown as an unexpected exception. + * @see #matchXMaterial(Material) + * @since 2.0.0 + */ + @Nonnull + @SuppressWarnings("deprecation") + public static XMaterial matchXMaterial(@Nonnull ItemStack item) { + Objects.requireNonNull(item, "Cannot match null ItemStack"); + String material = item.getType().name(); + byte data = (byte) (Data.ISFLAT || item.getType().getMaxDurability() > 0 ? 0 : item.getDurability()); + + // Versions 1.9-1.12 didn't really use the items data value. + if (supports(9) && !supports(13) && item.hasItemMeta() && material.equals("MONSTER_EGG")) { + ItemMeta meta = item.getItemMeta(); + if (meta instanceof SpawnEggMeta) { + SpawnEggMeta egg = (SpawnEggMeta) meta; + material = egg.getSpawnedType().name() + "_SPAWN_EGG"; + } + } + + // Potions used the items data value to store + // information about the type of potion in 1.8 + if (!supports(9) && material.endsWith("ION")) { + // There's also 16000+ data value technique, but this is more reliable. + return Potion.fromItemStack(item).isSplash() ? SPLASH_POTION : POTION; + } + + // Refer to the enum for info. + // Currently this is the only material with a non-zero data value + // that has been renamed after the flattening update. + // If this happens to more materials in the future, + // I might have to change then system. + if (Data.ISFLAT && !supports(14) && material.equals("CACTUS_GREEN")) return GREEN_DYE; + + // Check FILLED_MAP enum for more info. + // if (!Data.ISFLAT && item.hasItemMeta() && item.getItemMeta() instanceof org.bukkit.inventory.meta.MapMeta) return FILLED_MAP; + + // No orElseThrow, I don't want to deal with Java's final variable bullshit. + Optional result = matchDefinedXMaterial(material, data); + if (result.isPresent()) return result.get(); + throw new IllegalArgumentException("Unsupported material from item: " + material + " (" + data + ')'); + } + + /** + * The main method that parses the given material name and data value as an XMaterial. + * All the values passed to this method will not be null or empty and are formatted correctly. + * + * @param name the formatted name of the material. + * @param data the data value of the material. Is always 0 or {@link #UNKNOWN_DATA_VALUE} when {@link Data#ISFLAT} + * + * @return an XMaterial (with the same data value if specified) + * @see #matchXMaterial(Material) + * @see #matchXMaterial(int, byte) + * @see #matchXMaterial(ItemStack) + * @since 3.0.0 + */ + @Nonnull + protected static Optional matchDefinedXMaterial(@Nonnull String name, byte data) { + // if (!Boolean.valueOf(Boolean.getBoolean(Boolean.TRUE.toString())).equals(Boolean.FALSE.booleanValue())) return null; + Boolean duplicated = null; + boolean isAMap = name.equalsIgnoreCase("MAP"); + + // Do basic number and boolean checks before accessing more complex enum stuff. + if (Data.ISFLAT || (!isAMap && data <= 0 && !(duplicated = isDuplicated(name)))) { + Optional xMaterial = getIfPresent(name); + if (xMaterial.isPresent()) return xMaterial; + } + // Usually flat versions wouldn't pass this point, but some special materials do. + + XMaterial oldXMaterial = requestOldXMaterial(name, data); + if (oldXMaterial == null) { + // Special case. Refer to FILLED_MAP for more info. + return (data >= 0 && isAMap) ? Optional.of(FILLED_MAP) : Optional.empty(); + } + + if (!Data.ISFLAT && oldXMaterial.isPlural() && (duplicated == null ? isDuplicated(name) : duplicated)) return getIfPresent(name); + return Optional.of(oldXMaterial); + } + + /** + * XMaterial Paradox (Duplication Check) + * Checks if the material has any duplicates. + *

+ * Example: + *

{@code MELON, CARROT, POTATO, BEETROOT -> true} + * + * @param name the name of the material to check. + * + * @return true if there's a duplicated material for this material, otherwise false. + * @since 2.0.0 + */ + private static boolean isDuplicated(@Nonnull String name) { + // Don't use matchXMaterial() since this method is being called from matchXMaterial() itself and will cause a StackOverflowError. + return DUPLICATED.contains(name); + } + + /** + * Gets the XMaterial based on the material's ID (Magic Value) and data value.
+ * You should avoid using this for performance issues. + * + * @param id the ID (Magic value) of the material. + * @param data the data value of the material. + * + * @return a parsed XMaterial with the same ID and data value. + * @see #matchXMaterial(ItemStack) + * @since 2.0.0 + * @deprecated this method loops through all the available materials and matches their ID using {@link #getId()} + * which takes a really long time. Plugins should no longer support IDs. If you want, you can make a {@link Map} cache yourself. + * This method obviously doesn't work for 1.13+ and will not be supported. This is only here for debugging purposes. + */ + @Nonnull + @Deprecated + public static Optional matchXMaterial(int id, byte data) { + if (id < 0 || id > MAX_ID || data < 0) return Optional.empty(); + for (XMaterial materials : VALUES) { + if (materials.data == data && materials.getId() == id) return Optional.of(materials); + } + return Optional.empty(); + } + + /** + * Attempts to build the string like an enum name. + * Removes all the spaces, and extra non-English characters. Also removes some config/in-game based strings. + * While this method is hard to maintain, it's extremely efficient. It's approximately more than x5 times faster than + * the normal RegEx + String Methods approach for both formatted and unformatted material names. + * + * @param name the material name to modify. + * + * @return an enum name. + * @since 2.0.0 + */ + @Nonnull + protected static String format(@Nonnull String name) { + int len = name.length(); + char[] chs = new char[len]; + int count = 0; + boolean appendUnderline = false; + + for (int i = 0; i < len; i++) { + char ch = name.charAt(i); + + if (!appendUnderline && count != 0 && (ch == '-' || ch == ' ' || ch == '_') && chs[count] != '_') + appendUnderline = true; + else { + boolean number = false; + // Old materials have numbers in them. + if ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (number = (ch >= '0' && ch <= '9'))) { + if (appendUnderline) { + chs[count++] = '_'; + appendUnderline = false; + } + + if (number) chs[count++] = ch; + else chs[count++] = (char) (ch & 0x5f); + } + } + } + + return new String(chs, 0, count); + } + + /** + * Checks if the specified version is the same version or higher than the current server version. + * + * @param version the major version to be checked. "1." is ignored. E.g. 1.12 = 12 | 1.9 = 9 + * + * @return true of the version is equal or higher than the current version. + * @since 2.0.0 + */ + public static boolean supports(int version) { + return Data.VERSION >= version; + } + + public String[] getLegacy() { + return this.legacy; + } + + /** + * XMaterial Paradox (Duplication Check) + * I've concluded that this is just an infinite loop that keeps + * going around the Singular Form and the Plural Form materials. A waste of brain cells and a waste of time. + * This solution works just fine anyway. + *

+ * A solution for XMaterial Paradox. + * Manually parses the duplicated materials to find the exact material based on the server version. + * If the name ends with "S" -> Plural Form Material. + * Plural methods are only plural if they're also {@link #DUPLICATED} + *

+ * The only special exceptions are {@link #BRICKS} and {@link #NETHER_BRICKS} + * + * @return true if this material is a plural form material, otherwise false. + * @since 8.0.0 + */ + private boolean isPlural() { + // this.name().charAt(this.name().length() - 1) == 'S' + return this == CARROTS || this == POTATOES; + } + + /** + * Checks if the list of given material names matches the given base material. + * Mostly used for configs. + *

+ * Supports {@link String#contains} {@code CONTAINS:NAME} and Regular Expression {@code REGEX:PATTERN} formats. + *

+ * Example: + *

+     *     XMaterial material = {@link #matchXMaterial(ItemStack)};
+     *     if (material.isOneOf(plugin.getConfig().getStringList("disabled-items")) return;
+     * 
+ *
+ * {@code CONTAINS} Examples: + *
+     *     {@code "CONTAINS:CHEST" -> CHEST, ENDERCHEST, TRAPPED_CHEST -> true}
+     *     {@code "cOnTaINS:dYe" -> GREEN_DYE, YELLOW_DYE, BLUE_DYE, INK_SACK -> true}
+     * 
+ *

+ * {@code REGEX} Examples + *

+     *     {@code "REGEX:^.+_.+_.+$" -> Every Material with 3 underlines or more: SHULKER_SPAWN_EGG, SILVERFISH_SPAWN_EGG, SKELETON_HORSE_SPAWN_EGG}
+     *     {@code "REGEX:^.{1,3}$" -> Material names that have 3 letters only: BED, MAP, AIR}
+     * 
+ *

+ * The reason that there are tags for {@code CONTAINS} and {@code REGEX} is for the performance. + * Although RegEx patterns are cached in this method, + * please avoid using the {@code REGEX} tag if you can use the {@code CONTAINS} tag instead. + * It'll have a huge impact on performance. + * Please avoid using {@code (capturing groups)} there's no use for them in this case. + * If you want to use groups, use {@code (?: non-capturing groups)}. It's faster. + *

+ * Want to learn RegEx? You can mess around in RegExr website. + * + * @param materials the material names to check base material on. + * + * @return true if one of the given material names is similar to the base material. + * @since 3.1.1 + */ + public boolean isOneOf(@Nullable Collection materials) { + if (materials == null || materials.isEmpty()) return false; + String name = this.name(); + + for (String comp : materials) { + String checker = comp.toUpperCase(Locale.ENGLISH); + if (checker.startsWith("CONTAINS:")) { + comp = format(checker.substring(9)); + if (name.contains(comp)) return true; + continue; + } + if (checker.startsWith("REGEX:")) { + comp = comp.substring(6); + Pattern pattern = CACHED_REGEX.getIfPresent(comp); + if (pattern == null) { + try { + pattern = Pattern.compile(comp); + CACHED_REGEX.put(comp, pattern); + } catch (PatternSyntaxException ex) { + ex.printStackTrace(); + } + } + if (pattern != null && pattern.matcher(name).matches()) return true; + continue; + } + + // Direct Object Equals + Optional xMat = matchXMaterial(comp); + if (xMat.isPresent() && xMat.get() == this) return true; + } + return false; + } + + /** + * Sets the {@link Material} (and data value on older versions) of an item. + * Damageable materials will not have their durability changed. + *

+ * Use {@link #parseItem()} instead when creating new ItemStacks. + * + * @param item the item to change its type. + * + * @see #parseItem() + * @since 3.0.0 + */ + @Nonnull + @SuppressWarnings("deprecation") + public ItemStack setType(@Nonnull ItemStack item) { + Objects.requireNonNull(item, "Cannot set material for null ItemStack"); + Material material = this.parseMaterial(); + Objects.requireNonNull(material, () -> "Unsupported material: " + this.name()); + + item.setType(material); + if (!Data.ISFLAT && material.getMaxDurability() <= 0) item.setDurability(this.data); + return item; + } + + /** + * Checks if the given material name matches any of this xmaterial's legacy material names. + * All the values passed to this method will not be null or empty and are formatted correctly. + * + * @param name the material name to check. + * + * @return true if it's one of the legacy names, otherwise false. + * @since 2.0.0 + */ + private boolean anyMatchLegacy(@Nonnull String name) { + for (int i = this.legacy.length - 1; i >= 0; i--) { + if (name.equals(this.legacy[i])) return true; + } + return false; + } + + /** + * Parses an enum name to a user-friendly name. + * These names will have underlines removed and with each word capitalized. + *

+ * Examples: + *

+     *     {@literal EMERALD                 -> Emerald}
+     *     {@literal EMERALD_BLOCK           -> Emerald Block}
+     *     {@literal ENCHANTED_GOLDEN_APPLE  -> Enchanted Golden Apple}
+     * 
+ * + * @return a more user-friendly enum name. + * @since 3.0.0 + */ + @Override + @Nonnull + public String toString() { + return Arrays.stream(name().split("_")) + .map(t -> t.charAt(0) + t.substring(1).toLowerCase()) + .collect(Collectors.joining(" ")); + } + + /** + * Gets the ID (Magic value) of the material. + * https://www.minecraftinfo.com/idlist.htm + *

+ * Spigot added material ID support back in 1.16+ + * + * @return the ID of the material or -1 if it's not a legacy material or the server doesn't support the material. + * @see #matchXMaterial(int, byte) + * @since 2.2.0 + */ + @SuppressWarnings("deprecation") + public int getId() { + // https://hub.spigotmc.org/stash/projects/SPIGOT/repos/bukkit/diff/src/main/java/org/bukkit/Material.java?until=1cb03826ebde4ef887519ce37b0a2a341494a183 + // Should start working again in 1.16+ + Material material = this.parseMaterial(); + if (material == null) return -1; + try { + return material.getId(); + } catch (IllegalArgumentException ignored) { + return -1; + } + } + + /** + * The data value of this material pre-flattening. + *

+ * Can be accessed with {@link ItemStack#getData()} then {@code MaterialData#getData()} + * or {@link ItemStack#getDurability()} if not damageable. + * + * @return data of this material, or 0 if none. + * @since 1.0.0 + */ + @SuppressWarnings("deprecation") + public byte getData() { + return data; + } + + /** + * Parses an item from this XMaterial. + * Uses data values on older versions. + * + * @return an ItemStack with the same material (and data value if in older versions.) + * @see #setType(ItemStack) + * @since 2.0.0 + */ + @Nullable + @SuppressWarnings("deprecation") + public ItemStack parseItem() { + Material material = this.parseMaterial(); + if (material == null) return null; + return Data.ISFLAT ? new ItemStack(material) : new ItemStack(material, 1, this.data); + } + + /** + * Parses the material of this XMaterial. + * + * @return the material related to this XMaterial based on the server version. + * @since 1.0.0 + */ + @Nullable + public Material parseMaterial() { + return this.material; + } + + /** + * Checks if an item has the same material (and data value on older versions). + * + * @param item item to check. + * + * @return true if the material is the same as the item's material (and data value if on older versions), otherwise false. + * @since 1.0.0 + */ + @SuppressWarnings("deprecation") + public boolean isSimilar(@Nonnull ItemStack item) { + Objects.requireNonNull(item, "Cannot compare with null ItemStack"); + if (item.getType() != this.parseMaterial()) return false; + return Data.ISFLAT || item.getDurability() == this.data || item.getType().getMaxDurability() > 0; + } + + /** + * Checks if this material is supported in the current version. + * Suggested materials will be ignored. + *

+ * Note that you should use {@link #parseMaterial()} or {@link #parseItem()} and check if it's null + * if you're going to parse and use the material/item later. + * + * @return true if the material exists in {@link Material} list. + * @since 2.0.0 + */ + public boolean isSupported() { + return this.material != null; + } + + /** + * Checks if this material is supported in the current version and + * returns itself if yes. + *

+ * In the other case, the alternate material will get returned, + * no matter if it is supported or not. + * + * @param alternateMaterial the material to get if this one is not supported. + * @return this material or the {@code alternateMaterial} if not supported. + */ + public XMaterial or(XMaterial alternateMaterial) { + return isSupported() ? this : alternateMaterial; + } + + /** + * This method is needed due to Java enum initialization limitations. + * It's really inefficient yes, but it's only used for initialization. + *

+ * Yes there are many other ways like comparing the hardcoded ordinal or using a boolean in the enum constructor, + * but it's not really a big deal. + *

+ * This method should not be called if the version is after the flattening update {@link Data#ISFLAT} + * and is only used for parsing materials, not matching, for matching check {@link #DUPLICATED} + */ + private boolean isDuplicated() { + switch (this.name()) { + case "MELON": + case "CARROT": + case "POTATO": + case "GRASS": + case "BRICK": + case "NETHER_BRICK": + + // Illegal Elements + // Since both 1.12 and 1.13 have _DOOR XMaterial will use it + // for 1.12 to parse the material, but it needs _DOOR_ITEM. + // We'll trick XMaterial into thinking this needs to be parsed + // using the old methods. + // Some of these materials have their enum name added to the legacy list as well. + case "DARK_OAK_DOOR": + case "ACACIA_DOOR": + case "BIRCH_DOOR": + case "JUNGLE_DOOR": + case "SPRUCE_DOOR": + case "MAP": + case "CAULDRON": + case "BREWING_STAND": + case "FLOWER_POT": + return true; + default: + return false; + } + } + + /** + * Used for data that need to be accessed during enum initialization. + * + * @since 9.0.0 + */ + private static final class Data { + /** + * The current version of the server in the form of a major version. + * If the static initialization for this fails, you know something's wrong with the server software. + * + * @since 1.0.0 + */ + private static final int VERSION; + + static { // This needs to be right below VERSION because of initialization order. + String version = Bukkit.getVersion(); + Matcher matcher = Pattern.compile("MC: \\d\\.(\\d+)").matcher(version); + + if (matcher.find()) VERSION = Integer.parseInt(matcher.group(1)); + else throw new IllegalArgumentException("Failed to parse server version from: " + version); + } + + /** + * Cached result if the server version is after the v1.13 flattening update. + * + * @since 3.0.0 + */ + private static final boolean ISFLAT = supports(13); + } } \ No newline at end of file diff --git a/core/src/main/java/fr/skytasul/quests/utils/compatibility/BQDecentHolograms.java b/core/src/main/java/fr/skytasul/quests/utils/compatibility/BQDecentHolograms.java new file mode 100644 index 00000000..ad0d5623 --- /dev/null +++ b/core/src/main/java/fr/skytasul/quests/utils/compatibility/BQDecentHolograms.java @@ -0,0 +1,105 @@ +package fr.skytasul.quests.utils.compatibility; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ThreadLocalRandom; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import fr.skytasul.quests.api.AbstractHolograms; +import eu.decentsoftware.holograms.api.DHAPI; +import eu.decentsoftware.holograms.api.holograms.Hologram; + +public class BQDecentHolograms extends AbstractHolograms { + + private int counter = Integer.MIN_VALUE + ThreadLocalRandom.current().nextInt(1000000); + + @Override + public boolean supportPerPlayerVisibility() { + return true; + } + + @Override + public boolean supportItems() { + return true; + } + + @Override + public DecentHologram createHologram(Location lc, boolean defaultVisible) { + Hologram hologram = DHAPI.createHologram("BQ_holo_" + counter++, lc, false); + hologram.setDefaultVisibleState(defaultVisible); + hologram.enable(); + return new DecentHologram(hologram); + } + + public class DecentHologram extends BQHologram { + + protected DecentHologram(Hologram hologram) { + super(hologram); + } + + @Override + public void appendTextLine(String text) { + DHAPI.addHologramLine(hologram, text); + } + + @Override + public void appendItem(ItemStack item) { + DHAPI.addHologramLine(hologram, item); + } + + @Override + public void setPlayerVisibility(Player p, boolean visible) { + if (visible) { + hologram.setShowPlayer(p); + hologram.show(p, 0); + }else { + hologram.removeShowPlayer(p); + hologram.hide(p); + } + } + + @Override + public void setPlayersVisible(List players) { + List leftover = new ArrayList<>(players); + for (Iterator iterator = hologram.getShowPlayers().iterator(); iterator + .hasNext();) { + UUID visible = iterator.next(); + Player player = Bukkit.getPlayer(visible); + if (player == null) { + iterator.remove(); + continue; + } + + if (leftover.remove(player)) { + // the player should see the hologram + // and can already see it: nothing happens + } else { + // the player should not see the hologram + iterator.remove(); + hologram.hide(player); + } + } + + for (Player invisible : leftover) { + hologram.setShowPlayer(invisible); + hologram.show(invisible, 0); + } + } + + @Override + public void teleport(Location lc) { + DHAPI.moveHologram(hologram, lc); + } + + @Override + public void delete() { + hologram.delete(); + } + + } + +} diff --git a/core/src/main/java/fr/skytasul/quests/utils/compatibility/DependenciesManager.java b/core/src/main/java/fr/skytasul/quests/utils/compatibility/DependenciesManager.java index bf2acc93..8a09e4a3 100644 --- a/core/src/main/java/fr/skytasul/quests/utils/compatibility/DependenciesManager.java +++ b/core/src/main/java/fr/skytasul/quests/utils/compatibility/DependenciesManager.java @@ -4,14 +4,12 @@ import java.util.Arrays; import java.util.List; import java.util.function.Predicate; - import org.apache.commons.lang.Validate; import org.bukkit.Bukkit; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.server.PluginEnableEvent; import org.bukkit.plugin.Plugin; - import fr.skytasul.quests.BeautyQuests; import fr.skytasul.quests.api.QuestsAPI; import fr.skytasul.quests.api.requirements.RequirementCreator; @@ -32,7 +30,10 @@ import fr.skytasul.quests.utils.XMaterial; import fr.skytasul.quests.utils.compatibility.maps.BQBlueMap; import fr.skytasul.quests.utils.compatibility.maps.BQDynmap; +import fr.skytasul.quests.utils.compatibility.mobs.BQAdvancedSpawners; import fr.skytasul.quests.utils.compatibility.mobs.BQBoss; +import fr.skytasul.quests.utils.compatibility.mobs.BQLevelledMobs; +import fr.skytasul.quests.utils.compatibility.mobs.BQWildStacker; import fr.skytasul.quests.utils.compatibility.mobs.CitizensFactory; import fr.skytasul.quests.utils.compatibility.mobs.MythicMobs; import fr.skytasul.quests.utils.compatibility.mobs.MythicMobs5; @@ -86,13 +87,17 @@ public class DependenciesManager implements Listener { public static final BQDependency mm = new BQDependency("MythicMobs", () -> { try { - Class.forName("io.lumine.xikage.mythicmobs.mobs.MythicMob"); - QuestsAPI.registerMobFactory(new MythicMobs()); - }catch (ClassNotFoundException ex) { + Class.forName("io.lumine.mythic.api.MythicPlugin"); QuestsAPI.registerMobFactory(new MythicMobs5()); + }catch (ClassNotFoundException ex) { + QuestsAPI.registerMobFactory(new MythicMobs()); } }); + public static final BQDependency advancedspawners = new BQDependency("AdvancedSpawners", () -> QuestsAPI.registerMobFactory(new BQAdvancedSpawners())); + public static final BQDependency LevelledMobs = + new BQDependency("LevelledMobs", () -> QuestsAPI.registerMobFactory(new BQLevelledMobs())); + public static final BQDependency holod2 = new BQDependency("HolographicDisplays", () -> QuestsAPI.setHologramsManager(new BQHolographicDisplays2()), null, plugin -> plugin.getClass().getName().equals("com.gmail.filoghost.holographicdisplays.HolographicDisplays")); public static final BQDependency holod3 = new BQDependency("HolographicDisplays", () -> QuestsAPI.setHologramsManager(new BQHolographicDisplays3()), null, plugin -> { if (!plugin.getClass().getName().equals("me.filoghost.holographicdisplays.plugin.HolographicDisplays")) return false; @@ -104,6 +109,7 @@ public class DependenciesManager implements Listener { return false; } }); + public static final BQDependency decentholograms = new BQDependency("DecentHolograms", () -> QuestsAPI.setHologramsManager(new BQDecentHolograms())); public static final BQDependency sentinel = new BQDependency("Sentinel", BQSentinel::initialize); @@ -118,6 +124,8 @@ public class DependenciesManager implements Listener { public static final BQDependency mclvl = new BQDependency("McCombatLevel", () -> QuestsAPI.getRequirements().register(new RequirementCreator("mcmmoCombatLevelRequirement", McCombatLevelRequirement.class, ItemUtils.item(XMaterial.IRON_SWORD, Lang.RCombatLvl.toString()), McCombatLevelRequirement::new))); public static final BQDependency tokenEnchant = new BQDependency("TokenEnchant", () -> Bukkit.getPluginManager().registerEvents(new BQTokenEnchant(), BeautyQuests.getInstance())); public static final BQDependency ultimateTimber = new BQDependency("UltimateTimber", () -> Bukkit.getPluginManager().registerEvents(new BQUltimateTimber(), BeautyQuests.getInstance())); + public static final BQDependency PlayerBlockTracker = new BQDependency("PlayerBlockTracker"); + public static final BQDependency WildStacker = new BQDependency("WildStacker", BQWildStacker::initialize); //public static final BQDependency par = new BQDependency("Parties"); //public static final BQDependency eboss = new BQDependency("EpicBosses", () -> Bukkit.getPluginManager().registerEvents(new EpicBosses(), BeautyQuests.getInstance())); @@ -129,7 +137,16 @@ public class DependenciesManager implements Listener { private boolean lockDependencies = false; public DependenciesManager() { - dependencies = new ArrayList<>(Arrays.asList(znpcs, citizens, wg, mm, vault, papi, skapi, jobs, fac, acc, dyn, BlueMap, /*par, eboss, */gps, mmo, mclvl, boss, cmi, holod2, holod3, tokenEnchant, ultimateTimber, sentinel)); + dependencies = new ArrayList<>(Arrays.asList( + /*par, eboss, */ + znpcs, citizens, // npcs + wg, gps, tokenEnchant, ultimateTimber, sentinel, PlayerBlockTracker, // other + mm, boss, advancedspawners, LevelledMobs, WildStacker, // mobs + vault, papi, acc, // hooks + skapi, jobs, fac, mmo, mclvl, // rewards and requirements + dyn, BlueMap, // maps + cmi, holod2, holod3, decentholograms // holograms + )); } public List getDependencies() { @@ -183,6 +200,8 @@ public static class BQDependency { private final Predicate isValid; private boolean enabled = false; private boolean forceDisable = false; + private boolean initialized = false; + private Plugin foundPlugin; public BQDependency(String pluginName) { this(pluginName, null); @@ -217,12 +236,14 @@ boolean testCompatibility(boolean after) { if (isValid != null && !isValid.test(plugin)) return false; DebugUtils.logMessage("Hooked into " + pluginNames + " v" + plugin.getDescription().getVersion() + (after ? " after primary initialization" : "")); enabled = true; + foundPlugin = plugin; return true; } void initialize() { try { if (initialize != null) initialize.run(); + initialized = true; }catch (Throwable ex) { BeautyQuests.logger.severe("An error occurred while initializing " + pluginNames.toString() + " integration", ex); enabled = false; @@ -233,7 +254,8 @@ public void disable() { forceDisable = true; if (enabled) { enabled = false; - if (disable != null) disable.run(); + if (disable != null && initialized) disable.run(); + initialized = false; } } @@ -241,6 +263,13 @@ public boolean isEnabled() { return enabled; } + public Plugin getFoundPlugin() { + if (!enabled) + throw new IllegalStateException( + "The dependency " + pluginNames + " is not enabled"); + return foundPlugin; + } + } } diff --git a/core/src/main/java/fr/skytasul/quests/utils/compatibility/Paper.java b/core/src/main/java/fr/skytasul/quests/utils/compatibility/Paper.java new file mode 100644 index 00000000..6a53d5ce --- /dev/null +++ b/core/src/main/java/fr/skytasul/quests/utils/compatibility/Paper.java @@ -0,0 +1,27 @@ +package fr.skytasul.quests.utils.compatibility; + +import java.util.Iterator; +import java.util.function.Predicate; + +import org.bukkit.event.entity.PlayerDeathEvent; +import org.bukkit.inventory.ItemStack; + +import fr.skytasul.quests.utils.nms.NMS; + +public final class Paper { + + private Paper() {} + + public static void handleDeathItems(PlayerDeathEvent event, Predicate predicate) { + if (NMS.getMCVersion() < 13) return; + + for (Iterator iterator = event.getDrops().iterator(); iterator.hasNext();) { + ItemStack item = iterator.next(); + if (predicate.test(item)) { + iterator.remove(); + event.getItemsToKeep().add(item); + } + } + } + +} diff --git a/core/src/main/java/fr/skytasul/quests/utils/compatibility/Post1_13.java b/core/src/main/java/fr/skytasul/quests/utils/compatibility/Post1_13.java index 674ff87a..5b9d71f8 100644 --- a/core/src/main/java/fr/skytasul/quests/utils/compatibility/Post1_13.java +++ b/core/src/main/java/fr/skytasul/quests/utils/compatibility/Post1_13.java @@ -29,7 +29,7 @@ public static Class getDustOptionClass() { } public static Object getDustColor(Color color, int size) { - return new Particle.DustOptions(Color.fromBGR(color.getBlue(), color.getGreen(), color.getRed()), size); + return new Particle.DustOptions(color, size); } public static boolean isBlockData(Object data) { @@ -60,7 +60,7 @@ public XMaterial retrieveMaterial() { @Override public String getAsString() { - return BQBlock.BLOCKDATA_HEADER + data.getAsString(); + return BQBlock.BLOCKDATA_HEADER + data.getAsString(true); } } diff --git a/core/src/main/java/fr/skytasul/quests/utils/compatibility/Post1_16.java b/core/src/main/java/fr/skytasul/quests/utils/compatibility/Post1_16.java new file mode 100644 index 00000000..341541cb --- /dev/null +++ b/core/src/main/java/fr/skytasul/quests/utils/compatibility/Post1_16.java @@ -0,0 +1,18 @@ +package fr.skytasul.quests.utils.compatibility; + +import org.bukkit.Bukkit; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.SmithItemEvent; + +import fr.skytasul.quests.api.events.BQCraftEvent; + +public class Post1_16 implements Listener { + + @EventHandler (priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onSmith(SmithItemEvent event) { + Bukkit.getPluginManager().callEvent(new BQCraftEvent(event, event.getCurrentItem(), event.getCurrentItem().getAmount())); + } + +} diff --git a/core/src/main/java/fr/skytasul/quests/utils/compatibility/QuestsPlaceholders.java b/core/src/main/java/fr/skytasul/quests/utils/compatibility/QuestsPlaceholders.java index 9fdcfa97..79d82c28 100644 --- a/core/src/main/java/fr/skytasul/quests/utils/compatibility/QuestsPlaceholders.java +++ b/core/src/main/java/fr/skytasul/quests/utils/compatibility/QuestsPlaceholders.java @@ -145,7 +145,7 @@ public String onRequest(OfflinePlayer off, String identifier) { } if (data.left.isEmpty()) { - data.left = QuestsAPI.getQuests().getQuestsStarted(data.acc, true); + data.left = QuestsAPI.getQuests().getQuestsStarted(data.acc, false, true); }else QuestsAPI.getQuests().updateQuestsStarted(acc, true, data.left); try { diff --git a/core/src/main/java/fr/skytasul/quests/utils/compatibility/maps/AbstractMapIntegration.java b/core/src/main/java/fr/skytasul/quests/utils/compatibility/maps/AbstractMapIntegration.java index 219789ec..a7a73130 100644 --- a/core/src/main/java/fr/skytasul/quests/utils/compatibility/maps/AbstractMapIntegration.java +++ b/core/src/main/java/fr/skytasul/quests/utils/compatibility/maps/AbstractMapIntegration.java @@ -1,11 +1,11 @@ package fr.skytasul.quests.utils.compatibility.maps; import org.bukkit.Location; - import fr.skytasul.quests.BeautyQuests; import fr.skytasul.quests.api.QuestsAPI; import fr.skytasul.quests.api.QuestsHandler; import fr.skytasul.quests.options.OptionStarterNPC; +import fr.skytasul.quests.options.OptionVisibility.VisibilityLocation; import fr.skytasul.quests.structure.Quest; import fr.skytasul.quests.utils.DebugUtils; @@ -13,7 +13,8 @@ public abstract class AbstractMapIntegration implements QuestsHandler { @Override public final void load() { - initializeMarkers(this::initializeQuests); + if (isEnabled()) + initializeMarkers(this::initializeQuests); } private void initializeQuests() { @@ -22,8 +23,11 @@ private void initializeQuests() { @Override public void questLoaded(Quest quest) { - if (!quest.hasOption(OptionStarterNPC.class)) return; - if (quest.isHidden()) { + if (!isEnabled()) + return; + if (!quest.hasOption(OptionStarterNPC.class)) + return; + if (quest.isHidden(VisibilityLocation.MAPS)) { DebugUtils.logMessage("No marker created for quest " + quest.getID() + ": quest is hidden"); return; } @@ -38,9 +42,11 @@ public void questLoaded(Quest quest) { @Override public void questUnload(Quest quest) { - if (!quest.isHidden() && quest.hasOption(OptionStarterNPC.class)) removeMarker(quest); + if (!quest.isHidden(VisibilityLocation.MAPS) && quest.hasOption(OptionStarterNPC.class)) removeMarker(quest); } + public abstract boolean isEnabled(); + protected abstract void initializeMarkers(Runnable initializeQuests); protected abstract void addMarker(Quest quest, Location lc); diff --git a/core/src/main/java/fr/skytasul/quests/utils/compatibility/maps/BQBlueMap.java b/core/src/main/java/fr/skytasul/quests/utils/compatibility/maps/BQBlueMap.java index ad48bcab..1f2023b7 100644 --- a/core/src/main/java/fr/skytasul/quests/utils/compatibility/maps/BQBlueMap.java +++ b/core/src/main/java/fr/skytasul/quests/utils/compatibility/maps/BQBlueMap.java @@ -1,44 +1,44 @@ package fr.skytasul.quests.utils.compatibility.maps; -import java.io.IOException; import java.util.function.Consumer; - import org.bukkit.Location; - import fr.skytasul.quests.BeautyQuests; import fr.skytasul.quests.QuestsConfiguration; import fr.skytasul.quests.api.QuestsAPI; import fr.skytasul.quests.options.OptionStarterNPC; import fr.skytasul.quests.structure.Quest; import fr.skytasul.quests.utils.DebugUtils; - import de.bluecolored.bluemap.api.BlueMapAPI; import de.bluecolored.bluemap.api.BlueMapMap; import de.bluecolored.bluemap.api.BlueMapWorld; -import de.bluecolored.bluemap.api.marker.MarkerAPI; -import de.bluecolored.bluemap.api.marker.MarkerSet; -import de.bluecolored.bluemap.api.marker.POIMarker; +import de.bluecolored.bluemap.api.markers.MarkerSet; +import de.bluecolored.bluemap.api.markers.POIMarker; public class BQBlueMap extends AbstractMapIntegration { private static final String MARKERSET_ID = "beautyquests.markerset"; private Consumer enableConsumer; + private MarkerSet set; + @Override + public boolean isEnabled() { + return QuestsConfiguration.dynmapMarkerIcon() != null && !QuestsConfiguration.dynmapMarkerIcon().isEmpty(); + } + @Override protected void initializeMarkers(Runnable initializeQuests) { BlueMapAPI.onEnable(enableConsumer = api -> { try { - MarkerAPI markerAPI = api.getMarkerAPI(); - MarkerSet set = markerAPI.createMarkerSet(MARKERSET_ID); - set.setLabel(QuestsConfiguration.dynmapSetName()); - set.setToggleable(true); - set.setDefaultHidden(false); - markerAPI.save(); + set = MarkerSet.builder() + .label(QuestsConfiguration.dynmapSetName()) + .defaultHidden(false) + .toggleable(true) + .build(); DebugUtils.logMessage("Enabled BlueMap integration."); initializeQuests.run(); - }catch (IOException e) { + }catch (Exception e) { BeautyQuests.logger.severe("An error occurred while loading BlueMap integration.", e); QuestsAPI.unregisterQuestsHandler(this); } @@ -49,52 +49,38 @@ protected void initializeMarkers(Runnable initializeQuests) { public void unload() { BlueMapAPI.unregisterListener(enableConsumer); BlueMapAPI.getInstance().ifPresent(api -> { - try { - api.getMarkerAPI().removeMarkerSet(MARKERSET_ID); - }catch (IOException e) { - e.printStackTrace(); - } + api.getMaps().forEach(map -> map.getMarkerSets().remove(MARKERSET_ID)); }); } @Override protected void addMarker(Quest quest, Location lc) { BlueMapAPI.getInstance().ifPresent(api -> { - try { - MarkerAPI markerAPI = api.getMarkerAPI(); - markerAPI.getMarkerSet(MARKERSET_ID).ifPresent(set -> { - api.getWorld(lc.getWorld().getUID()).map(BlueMapWorld::getMaps).ifPresent(maps -> { - int i = 0; - for (BlueMapMap map : maps) { - POIMarker marker = set.createPOIMarker("qu_" + quest.getID() + "_" + i++, map, lc.getX(), lc.getY(), lc.getZ()); - marker.setLabel(quest.getName()); - marker.setIcon(QuestsConfiguration.dynmapMarkerIcon(), 0, 0); - } - DebugUtils.logMessage("Added " + i + " BlueMap markers for quest " + quest.getID()); - }); - }); - markerAPI.save(); - }catch (IOException e) { - e.printStackTrace(); - } + api.getWorld(lc.getWorld()).map(BlueMapWorld::getMaps).ifPresent(maps -> { + int i = 0; + for (BlueMapMap map : maps) { + POIMarker marker = POIMarker.toBuilder() + .label(quest.getName()) + .icon(QuestsConfiguration.dynmapMarkerIcon(), 0, 0) + .position(lc.getBlockX(), lc.getBlockY(), lc.getBlockZ()) + .build(); + set.getMarkers().put("qu_" + quest.getID() + "_" + i++, marker); + map.getMarkerSets().putIfAbsent(MARKERSET_ID, set); + } + DebugUtils.logMessage("Added " + i + " BlueMap markers for quest " + quest.getID()); + }); }); } @Override public void removeMarker(Quest quest) { BlueMapAPI.getInstance().ifPresent(api -> { - try { - api.getMarkerAPI().getMarkerSet(MARKERSET_ID).ifPresent(set -> { - Location lc = quest.getOptionValueOrDef(OptionStarterNPC.class).getLocation(); - api.getWorld(lc.getWorld().getUID()).map(BlueMapWorld::getMaps).ifPresent(maps -> { - for (int i = 0; i < maps.size(); i++) { - set.removeMarker("qu_" + quest.getID() + "_" + i++); - } - }); - }); - }catch (IOException e) { - e.printStackTrace(); - } + Location lc = quest.getOptionValueOrDef(OptionStarterNPC.class).getLocation(); + api.getWorld(lc.getWorld()).map(BlueMapWorld::getMaps).ifPresent(maps -> { + for (int i = 0; i < maps.size(); i++) { + set.getMarkers().remove("qu_" + quest.getID() + "_" + i); + } + }); }); } diff --git a/core/src/main/java/fr/skytasul/quests/utils/compatibility/maps/BQDynmap.java b/core/src/main/java/fr/skytasul/quests/utils/compatibility/maps/BQDynmap.java index 611bb275..5bb90e4d 100644 --- a/core/src/main/java/fr/skytasul/quests/utils/compatibility/maps/BQDynmap.java +++ b/core/src/main/java/fr/skytasul/quests/utils/compatibility/maps/BQDynmap.java @@ -7,12 +7,10 @@ import org.dynmap.markers.MarkerAPI; import org.dynmap.markers.MarkerIcon; import org.dynmap.markers.MarkerSet; - import fr.skytasul.quests.BeautyQuests; import fr.skytasul.quests.QuestsConfiguration; import fr.skytasul.quests.structure.Quest; import fr.skytasul.quests.utils.DebugUtils; - import net.md_5.bungee.api.ChatColor; public class BQDynmap extends AbstractMapIntegration { @@ -20,6 +18,11 @@ public class BQDynmap extends AbstractMapIntegration { private MarkerIcon icon; private MarkerSet markers; + @Override + public boolean isEnabled() { + return QuestsConfiguration.dynmapMarkerIcon() != null && !QuestsConfiguration.dynmapMarkerIcon().isEmpty(); + } + @Override protected void initializeMarkers(Runnable initializeQuests) { DynmapAPI dynmap = (DynmapAPI) Bukkit.getPluginManager().getPlugin("dynmap"); diff --git a/core/src/main/java/fr/skytasul/quests/utils/compatibility/mobs/BQAdvancedSpawners.java b/core/src/main/java/fr/skytasul/quests/utils/compatibility/mobs/BQAdvancedSpawners.java new file mode 100644 index 00000000..6f356900 --- /dev/null +++ b/core/src/main/java/fr/skytasul/quests/utils/compatibility/mobs/BQAdvancedSpawners.java @@ -0,0 +1,68 @@ +package fr.skytasul.quests.utils.compatibility.mobs; + +import java.util.function.Consumer; + +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.inventory.ItemStack; + +import fr.skytasul.quests.api.mobs.MobFactory; +import fr.skytasul.quests.editors.TextEditor; +import fr.skytasul.quests.gui.ItemUtils; +import fr.skytasul.quests.utils.Lang; +import fr.skytasul.quests.utils.XMaterial; + +import gcspawners.AdvancedEntityDeathEvent; + +public class BQAdvancedSpawners implements MobFactory { + + @Override + public String getID() { + return "advanced-spawners"; + } + + @Override + public boolean bukkitMobApplies(String first, Entity entity) { + return false; + } + + @Override + public ItemStack getFactoryItem() { + return ItemUtils.item(XMaterial.SPAWNER, Lang.advancedSpawners.toString()); + } + + @Override + public void itemClick(Player p, Consumer run) { + Lang.ADVANCED_SPAWNERS_MOB.send(p); + new TextEditor<>(p, () -> run.accept(null), run).enter(); + } + + @Override + public String fromValue(String value) { + return value; + } + + @Override + public String getValue(String data) { + return data; + } + + @Override + public String getName(String data) { + return data; + } + + @Override + public EntityType getEntityType(String data) { + return EntityType.PIG; + } + + @EventHandler + public void onMobDeath(AdvancedEntityDeathEvent event) { + if (!(event.getDamager() instanceof Player)) return; + callEvent(event, event.getEntityType(), null, (Player) event.getDamager()); + } + +} diff --git a/core/src/main/java/fr/skytasul/quests/utils/compatibility/mobs/BQBoss.java b/core/src/main/java/fr/skytasul/quests/utils/compatibility/mobs/BQBoss.java index c5e05b07..0df02e9f 100644 --- a/core/src/main/java/fr/skytasul/quests/utils/compatibility/mobs/BQBoss.java +++ b/core/src/main/java/fr/skytasul/quests/utils/compatibility/mobs/BQBoss.java @@ -5,12 +5,14 @@ import java.util.function.Consumer; import org.bukkit.DyeColor; +import org.bukkit.entity.Entity; import org.bukkit.entity.EntityType; import org.bukkit.entity.LivingEntity; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.inventory.ClickType; import org.bukkit.inventory.ItemStack; +import org.mineacademy.boss.api.BossAPI; import org.mineacademy.boss.api.event.BossDeathEvent; import org.mineacademy.boss.model.Boss; @@ -20,6 +22,7 @@ import fr.skytasul.quests.gui.templates.PagedGUI; import fr.skytasul.quests.utils.Lang; import fr.skytasul.quests.utils.MinecraftNames; +import fr.skytasul.quests.utils.Utils; import fr.skytasul.quests.utils.XMaterial; public class BQBoss implements MobFactory { @@ -40,7 +43,7 @@ public void itemClick(Player p, Consumer run) { new PagedGUI("List of Bosses", DyeColor.ORANGE, org.mineacademy.boss.api.BossAPI.getBosses(), null, x -> x.getName()) { @Override public ItemStack getItemStack(Boss object) { - return ItemUtils.item(XMaterial.mobItem(object.getType()), object.getName()); + return ItemUtils.item(Utils.mobItem(object.getType()), object.getName()); } @Override @@ -55,6 +58,11 @@ public void click(Boss existing, ItemStack item, ClickType clickType) { public Boss fromValue(String value) { return org.mineacademy.boss.api.BossAPI.getBoss(value); } + + @Override + public boolean bukkitMobApplies(Boss first, Entity entity) { + return BossAPI.getBoss(entity).getBoss().equals(first); + } @Override public String getValue(Boss data) { diff --git a/core/src/main/java/fr/skytasul/quests/utils/compatibility/mobs/BQLevelledMobs.java b/core/src/main/java/fr/skytasul/quests/utils/compatibility/mobs/BQLevelledMobs.java new file mode 100644 index 00000000..2ce6fc19 --- /dev/null +++ b/core/src/main/java/fr/skytasul/quests/utils/compatibility/mobs/BQLevelledMobs.java @@ -0,0 +1,46 @@ +package fr.skytasul.quests.utils.compatibility.mobs; + +import java.util.function.Consumer; +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; +import org.bukkit.event.entity.EntityDeathEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.plugin.java.JavaPlugin; +import fr.skytasul.quests.api.mobs.LeveledMobFactory; +import fr.skytasul.quests.gui.ItemUtils; +import fr.skytasul.quests.gui.mobs.EntityTypeGUI; +import fr.skytasul.quests.utils.XMaterial; +import me.lokka30.levelledmobs.LevelledMobs; + +public class BQLevelledMobs extends BukkitEntityFactory implements LeveledMobFactory { + + @Override + public String getID() { + return "levelledMobs"; + } + + private ItemStack item = ItemUtils.item(XMaterial.CHICKEN_SPAWN_EGG, "§eLevelledMobs"); + + @Override + public ItemStack getFactoryItem() { + return item; + } + + @Override + public void itemClick(Player p, Consumer run) { + new EntityTypeGUI(run, x -> x != null && x.isAlive()).create(p); + } + + @Override + public double getMobLevel(EntityType type, Entity entity) { + return JavaPlugin.getPlugin(LevelledMobs.class).levelInterface.getLevelOfMob((LivingEntity) entity); + } + + @Override + public void onEntityKilled(EntityDeathEvent e) { + // do nothing as the original BukkitEntityFactory will manage the event itself + } + +} diff --git a/core/src/main/java/fr/skytasul/quests/utils/compatibility/mobs/BQWildStacker.java b/core/src/main/java/fr/skytasul/quests/utils/compatibility/mobs/BQWildStacker.java new file mode 100644 index 00000000..bc3a8150 --- /dev/null +++ b/core/src/main/java/fr/skytasul/quests/utils/compatibility/mobs/BQWildStacker.java @@ -0,0 +1,20 @@ +package fr.skytasul.quests.utils.compatibility.mobs; + +import org.bukkit.entity.LivingEntity; +import org.bukkit.event.Listener; +import com.bgsoftware.wildstacker.api.WildStackerAPI; +import fr.skytasul.quests.api.QuestsAPI; + +public class BQWildStacker implements Listener { + + private BQWildStacker() {} + + public static void initialize() { + QuestsAPI.registerMobStacker(entity -> { + if (entity instanceof LivingEntity) + return WildStackerAPI.getEntityAmount((LivingEntity) entity); + return 1; + }); + } + +} diff --git a/core/src/main/java/fr/skytasul/quests/utils/compatibility/mobs/BukkitEntityFactory.java b/core/src/main/java/fr/skytasul/quests/utils/compatibility/mobs/BukkitEntityFactory.java index 1990b7d1..5a1cb5da 100644 --- a/core/src/main/java/fr/skytasul/quests/utils/compatibility/mobs/BukkitEntityFactory.java +++ b/core/src/main/java/fr/skytasul/quests/utils/compatibility/mobs/BukkitEntityFactory.java @@ -4,6 +4,7 @@ import java.util.List; import java.util.function.Consumer; +import org.bukkit.entity.Entity; import org.bukkit.entity.EntityType; import org.bukkit.entity.LivingEntity; import org.bukkit.entity.Player; @@ -36,6 +37,11 @@ public ItemStack getFactoryItem() { public void itemClick(Player p, Consumer run) { new EntityTypeGUI(run, x -> x != null).create(p); } + + @Override + public boolean bukkitMobApplies(EntityType first, Entity entity) { + return entity.getType() == first; + } @Override public EntityType fromValue(String value) { diff --git a/core/src/main/java/fr/skytasul/quests/utils/compatibility/mobs/CitizensFactory.java b/core/src/main/java/fr/skytasul/quests/utils/compatibility/mobs/CitizensFactory.java index 6575f0d2..411ebf90 100644 --- a/core/src/main/java/fr/skytasul/quests/utils/compatibility/mobs/CitizensFactory.java +++ b/core/src/main/java/fr/skytasul/quests/utils/compatibility/mobs/CitizensFactory.java @@ -4,6 +4,7 @@ import java.util.List; import java.util.function.Consumer; +import org.bukkit.entity.Entity; import org.bukkit.entity.EntityType; import org.bukkit.entity.LivingEntity; import org.bukkit.entity.Player; @@ -61,6 +62,11 @@ public void begin() { public NPC fromValue(String value) { return CitizensAPI.getNPCRegistry().getById(Integer.parseInt(value)); } + + @Override + public boolean bukkitMobApplies(NPC first, Entity entity) { + return first.isSpawned() && first.getEntity().equals(entity); + } @Override public String getValue(NPC data) { diff --git a/core/src/main/java/fr/skytasul/quests/utils/compatibility/mobs/CompatMobDeathEvent.java b/core/src/main/java/fr/skytasul/quests/utils/compatibility/mobs/CompatMobDeathEvent.java index c3a031bb..fa5cf22d 100644 --- a/core/src/main/java/fr/skytasul/quests/utils/compatibility/mobs/CompatMobDeathEvent.java +++ b/core/src/main/java/fr/skytasul/quests/utils/compatibility/mobs/CompatMobDeathEvent.java @@ -10,26 +10,33 @@ public class CompatMobDeathEvent extends Event { private final Object pluginEntity; private final Player killer; - private Entity bukkitEntity; + private final Entity bukkitEntity; + private final int amount; - public CompatMobDeathEvent(Object pluginEntity, Player killer, Entity bukkitEntity) { + public CompatMobDeathEvent(Object pluginEntity, Player killer, Entity bukkitEntity, int amount) { this.pluginEntity = pluginEntity; this.killer = killer; this.bukkitEntity = bukkitEntity; - } - - public Entity getBukkitEntity(){ - return bukkitEntity; + this.amount = amount; } public Object getPluginMob() { - return this.pluginEntity; + return pluginEntity; } public Player getKiller() { - return this.killer; + return killer; + } + + public Entity getBukkitEntity() { + return bukkitEntity; + } + + public int getAmount() { + return amount; } + @Override public HandlerList getHandlers() { return handlers; } diff --git a/core/src/main/java/fr/skytasul/quests/utils/compatibility/mobs/MythicMobs.java b/core/src/main/java/fr/skytasul/quests/utils/compatibility/mobs/MythicMobs.java index 1cbcff98..dcee1ce0 100644 --- a/core/src/main/java/fr/skytasul/quests/utils/compatibility/mobs/MythicMobs.java +++ b/core/src/main/java/fr/skytasul/quests/utils/compatibility/mobs/MythicMobs.java @@ -3,16 +3,15 @@ import java.util.Arrays; import java.util.List; import java.util.function.Consumer; - import org.bukkit.DyeColor; +import org.bukkit.entity.Entity; import org.bukkit.entity.EntityType; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.inventory.ClickType; import org.bukkit.inventory.ItemStack; - import fr.skytasul.quests.BeautyQuests; -import fr.skytasul.quests.api.mobs.MobFactory; +import fr.skytasul.quests.api.mobs.LeveledMobFactory; import fr.skytasul.quests.api.options.QuestOption; import fr.skytasul.quests.gui.Inventories; import fr.skytasul.quests.gui.ItemUtils; @@ -21,12 +20,11 @@ import fr.skytasul.quests.utils.Utils; import fr.skytasul.quests.utils.XMaterial; import fr.skytasul.quests.utils.nms.NMS; - import io.lumine.xikage.mythicmobs.api.bukkit.events.MythicMobDeathEvent; import io.lumine.xikage.mythicmobs.mobs.MythicMob; import io.lumine.xikage.mythicmobs.skills.placeholders.parsers.PlaceholderString; -public class MythicMobs implements MobFactory { +public class MythicMobs implements LeveledMobFactory { @Override public String getID() { @@ -46,7 +44,7 @@ public void itemClick(Player p, Consumer run) { public ItemStack getItemStack(MythicMob object) { XMaterial mobItem; try { - mobItem = XMaterial.mobItem(getEntityType(object)); + mobItem = Utils.mobItem(getEntityType(object)); }catch (Exception ex) { mobItem = XMaterial.SPONGE; BeautyQuests.logger.warning("Unknow entity type for MythicMob " + object.getInternalName(), ex); @@ -66,6 +64,19 @@ public void click(MythicMob existing, ItemStack item, ClickType clickType) { public MythicMob fromValue(String value) { return io.lumine.xikage.mythicmobs.MythicMobs.inst().getMobManager().getMythicMob(value); } + + @Override + public boolean bukkitMobApplies(MythicMob first, Entity entity) { + return io.lumine.xikage.mythicmobs.MythicMobs.inst().getMobManager().getActiveMob(entity.getUniqueId()) + .map(mob -> mob.getType().equals(first)) + .orElse(false); + } + + @Override + public double getMobLevel(MythicMob type, Entity entity) { + return io.lumine.xikage.mythicmobs.MythicMobs.inst().getMobManager().getActiveMob(entity.getUniqueId()).get() + .getLevel(); + } @Override public String getValue(MythicMob data) { diff --git a/core/src/main/java/fr/skytasul/quests/utils/compatibility/mobs/MythicMobs5.java b/core/src/main/java/fr/skytasul/quests/utils/compatibility/mobs/MythicMobs5.java index 73528157..93ea0242 100644 --- a/core/src/main/java/fr/skytasul/quests/utils/compatibility/mobs/MythicMobs5.java +++ b/core/src/main/java/fr/skytasul/quests/utils/compatibility/mobs/MythicMobs5.java @@ -2,17 +2,17 @@ import java.util.Arrays; import java.util.List; +import java.util.Objects; import java.util.function.Consumer; - import org.bukkit.DyeColor; +import org.bukkit.entity.Entity; import org.bukkit.entity.EntityType; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.inventory.ClickType; import org.bukkit.inventory.ItemStack; - import fr.skytasul.quests.BeautyQuests; -import fr.skytasul.quests.api.mobs.MobFactory; +import fr.skytasul.quests.api.mobs.LeveledMobFactory; import fr.skytasul.quests.api.options.QuestOption; import fr.skytasul.quests.gui.Inventories; import fr.skytasul.quests.gui.ItemUtils; @@ -21,13 +21,12 @@ import fr.skytasul.quests.utils.Utils; import fr.skytasul.quests.utils.XMaterial; import fr.skytasul.quests.utils.nms.NMS; - import io.lumine.mythic.api.mobs.MythicMob; import io.lumine.mythic.api.skills.placeholders.PlaceholderString; import io.lumine.mythic.bukkit.MythicBukkit; import io.lumine.mythic.bukkit.events.MythicMobDeathEvent; -public class MythicMobs5 implements MobFactory { +public class MythicMobs5 implements LeveledMobFactory { @Override public String getID() { @@ -47,7 +46,7 @@ public void itemClick(Player p, Consumer run) { public ItemStack getItemStack(MythicMob object) { XMaterial mobItem; try { - mobItem = XMaterial.mobItem(getEntityType(object)); + mobItem = Utils.mobItem(getEntityType(object)); }catch (Exception ex) { mobItem = XMaterial.SPONGE; BeautyQuests.logger.warning("Unknow entity type for MythicMob " + object.getInternalName(), ex); @@ -67,6 +66,18 @@ public void click(MythicMob existing, ItemStack item, ClickType clickType) { public MythicMob fromValue(String value) { return MythicBukkit.inst().getMobManager().getMythicMob(value).orElse(null); } + + @Override + public boolean bukkitMobApplies(MythicMob first, Entity entity) { + return MythicBukkit.inst().getMobManager().getActiveMob(entity.getUniqueId()) + .map(mob -> mob.getType().equals(first)) + .orElse(false); + } + + @Override + public double getMobLevel(MythicMob type, Entity entity) { + return MythicBukkit.inst().getMobManager().getActiveMob(entity.getUniqueId()).get().getLevel(); + } @Override public String getValue(MythicMob data) { @@ -86,8 +97,11 @@ public EntityType getEntityType(MythicMob data) { if (data.getEntityType() == null) { typeName = data.getMythicEntity() == null ? null : data.getMythicEntity().getClass().getSimpleName().substring(6); }else { - typeName = data.getEntityType().toUpperCase(); + typeName = Objects.toString(data.getEntityType()).toUpperCase(); } + if (typeName == null) + return null; + if (typeName.contains("BABY_")) typeName = typeName.substring(5); if (typeName.equalsIgnoreCase("MPET")) typeName = data.getConfig().getString("MPet.Anchor"); if (NMS.getMCVersion() < 11 && typeName.equals("WITHER_SKELETON")) typeName = "SKELETON"; @@ -104,11 +118,12 @@ public EntityType getEntityType(MythicMob data) { public List getDescriptiveLore(MythicMob data) { try { return Arrays.asList( - QuestOption.formatDescription("Base Health: " + data.getHealth().get()), - QuestOption.formatDescription("Base Damage: " + data.getDamage().get()), - QuestOption.formatDescription("Base Armor: " + data.getArmor().get())); - }catch (NoSuchMethodError e) { - return Arrays.asList("§cError when retrieving mob informations", "§c-> §oPlease update MythicMobs"); + QuestOption.formatDescription("Base Health: " + Objects.toString(data.getHealth())), + QuestOption.formatDescription("Base Damage: " + Objects.toString(data.getDamage())), + QuestOption.formatDescription("Base Armor: " + Objects.toString(data.getArmor()))); + } catch (Throwable ex) { + BeautyQuests.logger.warning("An error occurred while showing mob description", ex); + return Arrays.asList("§cError when retrieving mob informations"); } } diff --git a/core/src/main/java/fr/skytasul/quests/utils/compatibility/npcs/BQCitizens.java b/core/src/main/java/fr/skytasul/quests/utils/compatibility/npcs/BQCitizens.java index b7ed3894..d81a5960 100644 --- a/core/src/main/java/fr/skytasul/quests/utils/compatibility/npcs/BQCitizens.java +++ b/core/src/main/java/fr/skytasul/quests/utils/compatibility/npcs/BQCitizens.java @@ -50,12 +50,14 @@ public Collection getIDs() { @EventHandler (priority = EventPriority.HIGHEST) public void onNPCRightClick(NPCRightClickEvent e) { - super.clickEvent(e, e.getNPC().getId(), e.getClicker(), ClickType.RIGHT); + if (e.getNPC().getOwningRegistry() != CitizensAPI.getNPCRegistry()) return; + super.clickEvent(e, e.getNPC().getId(), e.getClicker(), e.getClicker().isSneaking() ? ClickType.SHIFT_RIGHT : ClickType.RIGHT); } @EventHandler (priority = EventPriority.HIGHEST) public void onNPCLeftClick(NPCLeftClickEvent e) { - super.clickEvent(e, e.getNPC().getId(), e.getClicker(), ClickType.LEFT); + if (e.getNPC().getOwningRegistry() != CitizensAPI.getNPCRegistry()) return; + super.clickEvent(e, e.getNPC().getId(), e.getClicker(), e.getClicker().isSneaking() ? ClickType.SHIFT_LEFT : ClickType.LEFT); } @EventHandler diff --git a/core/src/main/java/fr/skytasul/quests/utils/compatibility/npcs/BQServerNPCs.java b/core/src/main/java/fr/skytasul/quests/utils/compatibility/npcs/BQServerNPCs.java index 26b870e3..7623fb64 100644 --- a/core/src/main/java/fr/skytasul/quests/utils/compatibility/npcs/BQServerNPCs.java +++ b/core/src/main/java/fr/skytasul/quests/utils/compatibility/npcs/BQServerNPCs.java @@ -62,7 +62,7 @@ protected BQNPC create(Location location, EntityType type, String name) { @EventHandler public void onInteract(NPCInteractEvent e) { - super.clickEvent(null, e.getNpc().getNpcPojo().getId(), e.getPlayer(), e.isLeftClick() ? ClickType.LEFT : ClickType.RIGHT); + super.clickEvent(null, e.getNpc().getNpcPojo().getId(), e.getPlayer(), ClickType.of(e.isLeftClick(), e.getPlayer().isSneaking())); } public static class BQServerNPC extends BQNPC { diff --git a/core/src/main/java/fr/skytasul/quests/utils/compatibility/worldguard/BQWorldGuard.java b/core/src/main/java/fr/skytasul/quests/utils/compatibility/worldguard/BQWorldGuard.java index 5ea3c299..96bd0afb 100644 --- a/core/src/main/java/fr/skytasul/quests/utils/compatibility/worldguard/BQWorldGuard.java +++ b/core/src/main/java/fr/skytasul/quests/utils/compatibility/worldguard/BQWorldGuard.java @@ -15,8 +15,10 @@ import fr.skytasul.quests.api.QuestsAPI; import fr.skytasul.quests.api.requirements.RequirementCreator; +import fr.skytasul.quests.api.stages.StageType; import fr.skytasul.quests.gui.ItemUtils; import fr.skytasul.quests.requirements.RegionRequirement; +import fr.skytasul.quests.stages.StageArea; import fr.skytasul.quests.utils.DebugUtils; import fr.skytasul.quests.utils.Lang; import fr.skytasul.quests.utils.XMaterial; @@ -118,6 +120,8 @@ public ProtectedRegion getRegion(String name, World w) { public static void init() { Validate.isTrue(instance == null, "BQ WorldGuard integration already initialized."); instance = new BQWorldGuard(); + + QuestsAPI.registerStage(new StageType<>("REGION", StageArea.class, Lang.Find.name(), StageArea::deserialize, ItemUtils.item(XMaterial.WOODEN_AXE, Lang.stageGoTo.toString()), StageArea.Creator::new)); QuestsAPI.getRequirements().register(new RequirementCreator("regionRequired", RegionRequirement.class, ItemUtils.item(XMaterial.WOODEN_AXE, Lang.RRegion.toString()), RegionRequirement::new)); } diff --git a/core/src/main/java/fr/skytasul/quests/utils/logger/ILoggerHandler.java b/core/src/main/java/fr/skytasul/quests/utils/logger/ILoggerHandler.java index 1dd3e497..25889652 100644 --- a/core/src/main/java/fr/skytasul/quests/utils/logger/ILoggerHandler.java +++ b/core/src/main/java/fr/skytasul/quests/utils/logger/ILoggerHandler.java @@ -1,17 +1,27 @@ package fr.skytasul.quests.utils.logger; +import java.util.logging.Handler; + public interface ILoggerHandler { public static final ILoggerHandler EMPTY_LOGGER = new LoggerHandlerEmpty(); - void write(String msg); + void write(String msg, String... prefixes); + + Handler getSubhandler(String prefix); class LoggerHandlerEmpty implements ILoggerHandler { private LoggerHandlerEmpty() {} @Override - public void write(String msg) {} + public void write(String msg, String... prefixes) {} + + @Override + public Handler getSubhandler(String prefix) { + return null; + } + } } \ No newline at end of file diff --git a/core/src/main/java/fr/skytasul/quests/utils/logger/LoggerHandler.java b/core/src/main/java/fr/skytasul/quests/utils/logger/LoggerHandler.java index b1ab086f..b4493c41 100644 --- a/core/src/main/java/fr/skytasul/quests/utils/logger/LoggerHandler.java +++ b/core/src/main/java/fr/skytasul/quests/utils/logger/LoggerHandler.java @@ -60,9 +60,13 @@ public boolean isEnabled() { @Override public void publish(LogRecord logRecord) { + log(logRecord, null); + } + + private void log(LogRecord logRecord, String prefix) { try { if (logRecord != null) { - write("[" + (logRecord.getLevel() == null ? "NONE" : logRecord.getLevel().getName()) + "]: " + getFormatter().format(logRecord)); + write(getFormatter().format(logRecord), prefix, logRecord.getLevel() == null ? "NONE" : logRecord.getLevel().getName()); if (logRecord.getThrown() != null) { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); @@ -72,9 +76,9 @@ public void publish(LogRecord logRecord) { int index = errors.indexOf(throwable); if (index == -1) { index = errors.size(); - write("[ERROR] new #" + index + ": " + throwable); + write("new #" + index + ": " + throwable, "ERROR", prefix); errors.add(throwable); - }else write("[ERROR] existing #" + index); + }else write("existing #" + index, "ERROR", prefix); } } }catch (Exception ex) { @@ -83,10 +87,14 @@ public void publish(LogRecord logRecord) { } @Override - public void write(String msg){ + public synchronized void write(String msg, String... prefixes) { if (!isEnabled()) return; date.setTime(System.currentTimeMillis()); - stream.println(format.format(date) + msg); + stream.print(format.format(date)); + for (String prefix : prefixes) { + if (prefix != null && !prefix.isEmpty()) stream.print("[" + prefix + "] "); + } + stream.println(msg); something = true; } @@ -124,4 +132,30 @@ public void flush() { run.run(); } + @Override + public Handler getSubhandler(String prefix) { + return new Subhandler(prefix); + } + + private class Subhandler extends Handler { + + private String prefix; + + private Subhandler(String prefix) { + this.prefix = prefix; + } + + @Override + public void publish(LogRecord logRecord) { + log(logRecord, prefix); + } + + @Override + public void flush() {} + + @Override + public void close() throws SecurityException {} + + } + } diff --git a/core/src/main/java/fr/skytasul/quests/utils/nms/NMS.java b/core/src/main/java/fr/skytasul/quests/utils/nms/NMS.java index e6abbf6c..3c312c82 100644 --- a/core/src/main/java/fr/skytasul/quests/utils/nms/NMS.java +++ b/core/src/main/java/fr/skytasul/quests/utils/nms/NMS.java @@ -7,7 +7,7 @@ import org.bukkit.Bukkit; import org.bukkit.Material; -import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Entity; import org.bukkit.entity.Player; import org.bukkit.inventory.meta.ItemMeta; @@ -40,7 +40,7 @@ public NMS() { public abstract Object bookPacket(ByteBuf buf); - public abstract double entityNameplateHeight(LivingEntity en); // can be remplaced by Entity.getHeight from 1.11 + public abstract double entityNameplateHeight(Entity en); // can be remplaced by Entity.getHeight from 1.11 public List getAvailableBlockProperties(Material material){ throw new UnsupportedOperationException(); diff --git a/core/src/main/java/fr/skytasul/quests/utils/nms/NullNMS.java b/core/src/main/java/fr/skytasul/quests/utils/nms/NullNMS.java index c18bea22..7ff8d9c1 100644 --- a/core/src/main/java/fr/skytasul/quests/utils/nms/NullNMS.java +++ b/core/src/main/java/fr/skytasul/quests/utils/nms/NullNMS.java @@ -1,5 +1,6 @@ package fr.skytasul.quests.utils.nms; +import org.bukkit.entity.Entity; import org.bukkit.entity.LivingEntity; import org.bukkit.entity.Player; import org.bukkit.inventory.meta.ItemMeta; @@ -30,8 +31,8 @@ public ReflectUtils getReflect(){ } @Override - public double entityNameplateHeight(LivingEntity en){ - return en.getEyeHeight() + 1; + public double entityNameplateHeight(Entity en){ + return en instanceof LivingEntity ? ((LivingEntity) en).getEyeHeight() + 1 : 1; } public Object getIChatBaseComponent(String text){ diff --git a/core/src/main/java/fr/skytasul/quests/utils/types/BQBlock.java b/core/src/main/java/fr/skytasul/quests/utils/types/BQBlock.java index c4311b8b..9730e0e6 100644 --- a/core/src/main/java/fr/skytasul/quests/utils/types/BQBlock.java +++ b/core/src/main/java/fr/skytasul/quests/utils/types/BQBlock.java @@ -1,7 +1,17 @@ package fr.skytasul.quests.utils.types; +import java.util.Collection; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.Spliterator; +import java.util.Spliterators; + import org.bukkit.block.Block; +import fr.skytasul.quests.api.stages.types.Locatable; +import fr.skytasul.quests.api.stages.types.Locatable.Located; +import fr.skytasul.quests.api.stages.types.Locatable.Located.LocatedBlock; +import fr.skytasul.quests.api.stages.types.Locatable.LocatedType; import fr.skytasul.quests.utils.MinecraftNames; import fr.skytasul.quests.utils.XBlock; import fr.skytasul.quests.utils.XMaterial; @@ -40,6 +50,67 @@ public static BQBlock fromString(String string) { return new BQBlockMaterial(XMaterial.valueOf(string)); } + public static Spliterator getNearbyBlocks(Locatable.MultipleLocatable.NearbyFetcher fetcher, Collection types) { + if (!fetcher.isTargeting(LocatedType.BLOCK)) return Spliterators.emptySpliterator(); + + int minY = (int) Math.max(fetcher.getCenter().getWorld().getMinHeight(), fetcher.getCenter().getY() - fetcher.getMaxDistance()); + double maxY = Math.min(fetcher.getCenter().getWorld().getMaxHeight(), fetcher.getCenter().getY() + fetcher.getMaxDistance()); + + int centerX = fetcher.getCenter().getBlockX(); + int centerZ = fetcher.getCenter().getBlockZ(); + + return Spliterators.spliteratorUnknownSize(new Iterator() { + + int x = centerX; + int z = centerZ; + + int i = 0; + int y = minY; + + Locatable.Located.LocatedBlock found = null; + + private boolean findNext() { + for (; y <= maxY; y++) { + Block blockAt = fetcher.getCenter().getWorld().getBlockAt(x, y, z); + if (types.stream().anyMatch(type -> type.applies(blockAt))) { + found = Locatable.Located.LocatedBlock.create(blockAt); + y++; + return true; + } + } + if (Math.abs(x - centerX) <= Math.abs(z - centerZ) && ((x - centerX) != (z - centerZ) || x >= centerX)) + x += ((z >= centerZ) ? 1 : -1); + else + z += ((x >= centerX) ? -1 : 1); + + i++; + if (i >= fetcher.getMaxDistance() * fetcher.getMaxDistance()) return false; + y = minY; + return findNext(); + + // used the N spiral algorithm from here: https://stackoverflow.com/a/31864777 + } + + @Override + public boolean hasNext() { + if (found != null) return true; + return findNext(); + } + + @Override + public Located next() { + if (found == null) findNext(); + if (found != null) { + LocatedBlock tmpFound = found; + found = null; + return tmpFound; + } + throw new NoSuchElementException(); + } + + }, Spliterator.ORDERED & Spliterator.IMMUTABLE & Spliterator.DISTINCT & Spliterator.NONNULL); + } + public static class BQBlockMaterial extends BQBlock { private final XMaterial material; @@ -55,7 +126,7 @@ public XMaterial retrieveMaterial() { @Override public boolean applies(Block block) { - return XBlock.isType(block, material); + return XBlock.isSimilar(block, material); } @Override diff --git a/core/src/main/java/fr/skytasul/quests/utils/types/BQLocation.java b/core/src/main/java/fr/skytasul/quests/utils/types/BQLocation.java index ebb63836..6b8c3887 100644 --- a/core/src/main/java/fr/skytasul/quests/utils/types/BQLocation.java +++ b/core/src/main/java/fr/skytasul/quests/utils/types/BQLocation.java @@ -1,16 +1,20 @@ package fr.skytasul.quests.utils.types; import java.util.Map; +import java.util.Objects; import java.util.regex.Pattern; - import org.apache.commons.lang.Validate; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.World; +import org.bukkit.block.Block; import org.bukkit.util.NumberConversions; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import fr.skytasul.quests.api.stages.types.Locatable; +import fr.skytasul.quests.api.stages.types.Locatable.LocatedType; -public class BQLocation extends Location { +public class BQLocation extends Location implements Locatable.Located { private Pattern worldPattern; @@ -51,6 +55,16 @@ public void setWorld(World world) { throw new UnsupportedOperationException(); } + @Override + public Location getLocation() { + return new Location(getWorld(), getX(), getY(), getZ()); + } + + @Override + public LocatedType getType() { + return LocatedType.OTHER; + } + public boolean isWorld(World world) { Validate.notNull(world); if (super.getWorld() != null) return super.getWorld().equals(world); @@ -60,6 +74,18 @@ public boolean isWorld(World world) { public String getWorldName() { return getWorld() == null ? worldPattern.pattern() : getWorld().getName(); } + + @Nullable + public Block getMatchingBlock() { + if (super.getWorld() != null) return super.getBlock(); + if (worldPattern == null) return null; + return Bukkit.getWorlds() + .stream() + .filter(world -> worldPattern.matcher(world.getName()).matches()) + .findFirst() + .map(world -> world.getBlockAt(getBlockX(), getBlockY(), getBlockZ())) + .orElse(null); + } @Override public double distanceSquared(Location o) { @@ -71,11 +97,29 @@ public double distanceSquared(Location o) { @Override public boolean equals(Object obj) { - if (!super.equals(obj)) return false; - BQLocation other = (BQLocation) obj; - if (worldPattern == null) return other.worldPattern == null; - if (other.worldPattern == null) return false; - return worldPattern.pattern().equals(other.worldPattern.pattern()); + if (!(obj instanceof Location)) return false; + Location other = (Location) obj; + + if (Double.doubleToLongBits(this.getX()) != Double.doubleToLongBits(other.getX())) return false; + if (Double.doubleToLongBits(this.getY()) != Double.doubleToLongBits(other.getY())) return false; + if (Double.doubleToLongBits(this.getZ()) != Double.doubleToLongBits(other.getZ())) return false; + if (Float.floatToIntBits(this.getPitch()) != Float.floatToIntBits(other.getPitch())) return false; + if (Float.floatToIntBits(this.getYaw()) != Float.floatToIntBits(other.getYaw())) return false; + + if (obj instanceof BQLocation) { + BQLocation otherBQ = (BQLocation) obj; + if (worldPattern == null) return otherBQ.worldPattern == null; + if (otherBQ.worldPattern == null) return false; + return worldPattern.pattern().equals(otherBQ.worldPattern.pattern()); + } + + if (!Objects.equals(other.getWorld(), getWorld())) { + if (other.getWorld() == null) return false; + if (worldPattern == null) return false; + return worldPattern.matcher(other.getWorld().getName()).matches(); + } + + return true; } @Override diff --git a/core/src/main/java/fr/skytasul/quests/utils/types/DialogRunner.java b/core/src/main/java/fr/skytasul/quests/utils/types/DialogRunner.java index 5f9daa77..ec662ab5 100644 --- a/core/src/main/java/fr/skytasul/quests/utils/types/DialogRunner.java +++ b/core/src/main/java/fr/skytasul/quests/utils/types/DialogRunner.java @@ -6,11 +6,9 @@ import java.util.Map; import java.util.function.Consumer; import java.util.function.Predicate; - import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.bukkit.scheduler.BukkitTask; - import fr.skytasul.quests.BeautyQuests; import fr.skytasul.quests.QuestsConfiguration; import fr.skytasul.quests.api.events.DialogSendEvent; @@ -96,10 +94,10 @@ public TestResult onClick(Player p) { end(p); return TestResult.ALLOW; } - return handleNext(p); + return handleNext(p, DialogNextReason.NPC_CLICK); } - public TestResult handleNext(Player p) { + public TestResult handleNext(Player p, DialogNextReason reason) { TestResult test = test(p); if (test == TestResult.ALLOW) { // player fulfills conditions to start or continue the dialog @@ -116,7 +114,7 @@ public TestResult handleNext(Player p) { PlayerStatus status = addPlayer(p); status.cancel(); - if (send(p, status)) { + if (send(p, status, reason)) { // when dialog finished removePlayer(p); end(p); @@ -128,7 +126,7 @@ public TestResult handleNext(Player p) { status.task = null; // we test if the player is within the authorized distance from the NPC if (canContinue(p)) { - handleNext(p); + handleNext(p, DialogNextReason.AUTO_TIME); }else { Lang.DIALOG_TOO_FAR.send(p, dialog.getNPCName(npc)); removePlayer(p); @@ -147,17 +145,22 @@ public TestResult handleNext(Player p) { /** * Sends the next dialog line for a player, or the first message if it has just begun the dialog. + * * @param p player to send the dialog to + * @param reason reason the message has to be sent * @return true if the dialog ends following this call, falseotherwise */ - private boolean send(Player p, PlayerStatus status) { + private boolean send(Player p, PlayerStatus status, DialogNextReason reason) { if (dialog.messages.isEmpty()) return true; int id = ++status.lastId; - if (id == dialog.messages.size()) { - // dialog ended correctly - return true; - } + boolean endOfDialog = id == dialog.messages.size(); + + if (status.runningMsg != null) + status.runningMsg.finished(p, endOfDialog, reason != DialogNextReason.AUTO_TIME); + if (status.runningMsgTask != null) status.runningMsgTask.cancel(); + + if (endOfDialog) return true; Message msg = dialog.messages.get(id); if (msg == null) { @@ -165,9 +168,11 @@ private boolean send(Player p, PlayerStatus status) { return true; } + status.runningMsg = msg; DialogSendMessageEvent event = new DialogSendMessageEvent(dialog, msg, npc, p); Bukkit.getPluginManager().callEvent(event); - if (!event.isCancelled()) msg.sendMessage(p, dialog.getNPCName(npc), id, dialog.messages.size()); + if (!event.isCancelled()) + status.runningMsgTask = msg.sendMessage(p, dialog.getNPCName(npc), id, dialog.messages.size()); return false; } @@ -213,7 +218,7 @@ private void handlePlayerChanges() { } public void unload() { - players.values().forEach(PlayerStatus::cancel); + if (!players.isEmpty()) players.values().forEach(PlayerStatus::cancel); players.clear(); handlePlayerChanges(); } @@ -221,6 +226,8 @@ public void unload() { class PlayerStatus { int lastId = -1; BukkitTask task = null; + BukkitTask runningMsgTask = null; + Message runningMsg = null; void cancel() { if (task != null) { @@ -230,6 +237,10 @@ void cancel() { } } + public enum DialogNextReason { + NPC_CLICK, AUTO_TIME, COMMAND, PLUGIN; + } + public enum TestResult { ALLOW, DENY, DENY_CANCEL; diff --git a/core/src/main/java/fr/skytasul/quests/utils/types/Message.java b/core/src/main/java/fr/skytasul/quests/utils/types/Message.java index 200196fb..6354d41c 100644 --- a/core/src/main/java/fr/skytasul/quests/utils/types/Message.java +++ b/core/src/main/java/fr/skytasul/quests/utils/types/Message.java @@ -6,6 +6,7 @@ import org.apache.commons.lang.StringUtils; import org.bukkit.entity.Player; import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.scheduler.BukkitTask; import fr.skytasul.quests.BeautyQuests; import fr.skytasul.quests.QuestsConfiguration; @@ -31,13 +32,15 @@ public int getWaitTime() { return wait == -1 ? QuestsConfiguration.getDialogsConfig().getDefaultTime() : wait; } - public void sendMessage(Player p, String npc, int id, int size) { + public BukkitTask sendMessage(Player p, String npc, int id, int size) { + BukkitTask task = null; + String sent = formatMessage(p, npc, id, size); if (QuestsConfiguration.getDialogsConfig().sendInActionBar()) { BaseComponent[] components = TextComponent.fromLegacyText(sent.replace("{nl}", " ")); p.spigot().sendMessage(ChatMessageType.ACTION_BAR, components); if (getWaitTime() > 60) { - new BukkitRunnable() { + task = new BukkitRunnable() { int time = 40; @Override @@ -56,18 +59,25 @@ public void run() { }else p.sendMessage(StringUtils.splitByWholeSeparator(sent, "{nl}")); if (!"none".equals(sound)) { - String sentSound = sound; - if (sentSound == null) { - if (sender == Sender.PLAYER) { - sentSound = QuestsConfiguration.getDialogsConfig().getDefaultPlayerSound(); - }else if (sender == Sender.NPC) { - sentSound = QuestsConfiguration.getDialogsConfig().getDefaultNPCSound(); - } - } + String sentSound = getSound(); if (sentSound != null) p.playSound(p.getLocation(), sentSound, 1, 1); } + + return task; } + private String getSound() { + String sentSound = sound; + if (sentSound == null) { + if (sender == Sender.PLAYER) { + sentSound = QuestsConfiguration.getDialogsConfig().getDefaultPlayerSound(); + }else if (sender == Sender.NPC) { + sentSound = QuestsConfiguration.getDialogsConfig().getDefaultNPCSound(); + } + } + return sentSound; + } + public String formatMessage(Player p, String npc, int id, int size) { String sent = null; switch (sender) { @@ -84,6 +94,12 @@ public String formatMessage(Player p, String npc, int id, int size) { return sent; } + public void finished(Player p, boolean endOfDialog, boolean forced) { + if (endOfDialog || !forced) return; + String sentSound = getSound(); + if (sentSound != null) p.stopSound(sentSound); + } + @Override public Message clone() { Message clone = new Message(text, sender); diff --git a/core/src/main/java/fr/skytasul/quests/utils/types/Title.java b/core/src/main/java/fr/skytasul/quests/utils/types/Title.java index 73941011..d58cd28d 100644 --- a/core/src/main/java/fr/skytasul/quests/utils/types/Title.java +++ b/core/src/main/java/fr/skytasul/quests/utils/types/Title.java @@ -1,8 +1,6 @@ package fr.skytasul.quests.utils.types; -import java.util.HashMap; -import java.util.Map; - +import org.bukkit.configuration.ConfigurationSection; import org.bukkit.entity.Player; import fr.skytasul.quests.utils.Lang; @@ -48,24 +46,20 @@ public String toString() { return title + ", " + subtitle + ", " + Lang.Ticks.format(fadeIn + stay + fadeOut); } - public Map serialize(){ - Map map = new HashMap<>(); - - if (title != null) map.put("title", title); - if (subtitle != null) map.put("subtitle", subtitle); - if (fadeIn != FADE_IN) map.put("fadeIn", fadeIn); - if (stay != STAY) map.put("stay", stay); - if (fadeOut != FADE_OUT) map.put("fadeOut", fadeOut); - - return map; + public void serialize(ConfigurationSection section) { + if (title != null) section.set("title", title); + if (subtitle != null) section.set("subtitle", subtitle); + if (fadeIn != FADE_IN) section.set("fadeIn", fadeIn); + if (stay != STAY) section.set("stay", stay); + if (fadeOut != FADE_OUT) section.set("fadeOut", fadeOut); } - public static Title deserialize(Map map) { - String title = (String) map.getOrDefault("title", null); - String subtitle = (String) map.getOrDefault("subtitle", null); - int fadeIn = map.containsKey("fadeIn") ? (int) map.get("fadeIn") : FADE_IN; - int stay = map.containsKey("stay") ? (int) map.get("stay") : STAY; - int fadeOut = map.containsKey("fadeOut") ? (int) map.get("fadeOut") : FADE_OUT; + public static Title deserialize(ConfigurationSection section) { + String title = section.getString("title", null); + String subtitle = section.getString("subtitle", null); + int fadeIn = section.getInt("fadeIn", FADE_IN); + int stay = section.getInt("stay", STAY); + int fadeOut = section.getInt("fadeOut", FADE_OUT); return new Title(title, subtitle, fadeIn, stay, fadeOut); } diff --git a/core/src/main/resources/config.yml b/core/src/main/resources/config.yml index 8e883dcf..aab4579a 100644 --- a/core/src/main/resources/config.yml +++ b/core/src/main/resources/config.yml @@ -13,7 +13,10 @@ enablePrefix: true saveCycle: 15 # Enable "periodic save" message in console saveCycleMessage: true -# Max amount of quests who can be started at the same time by a player (you can bypass this limit for some quests) +# Maximum amount of quests that can be started at the same time by a player. +# This limit can also be set by player by giving the permission "beautyquests.start." +# to a group or a player with your permissions plugin. +# It is possible not to count some quests in this limit with the "Bypass limit" quest option. maxLaunchedQuests: 0 # Database configuration database: @@ -50,9 +53,10 @@ fireworks: true mobsProgressBar: false # Amount of seconds before the progress bar will disappear (set it to 0 to make it persistent) progressBarTimeoutSeconds: 15 -# Which click the player has to do on the NPC to start a quest, follow a dialog... (can be: RIGHT, LEFT, ANY) -npcClick: RIGHT -# Item which represents a quest in the Choose Quest GUI +# Which clicks are acceptable for a player to do on the NPC in order to start a quest, follow a dialog... +# (can be: RIGHT, LEFT, SHIFT_RIGHT, SHIFT_LEFT) +npcClick: [RIGHT, SHIFT_RIGHT] +# Default item shown for a quest in the menus item: BOOK # Page item material pageItem: ARROW @@ -102,6 +106,9 @@ gps: false skillAPIoverride: true # Enable or disable AccountsHook managing player accounts accountsHook: false +# If set to "true" and the PlayerBlockTracker plugin is enabled on this server, then player-placed blocks will be tracked +# using PlayerBlockTracker: it allows for persistence after restart, piston tracking, and more. +usePlayerBlockTracker: true # (HolographicDisplays) Disable the hologram above NPC's head disableTextHologram: false # (HolographicDisplays) Value added to the hologram height (decimal value) diff --git a/core/src/main/resources/hikari.properties b/core/src/main/resources/hikari.properties new file mode 100644 index 00000000..01d762db --- /dev/null +++ b/core/src/main/resources/hikari.properties @@ -0,0 +1,10 @@ +dataSource.cachePrepStmts=true +dataSource.prepStmtCacheSize=250 +dataSource.prepStmtCacheSqlLimit=2048 +dataSource.useServerPrepStmts=true +dataSource.useLocalSessionState=true +dataSource.rewriteBatchedStatements=true +dataSource.cacheResultSetMetadata=true +dataSource.cacheServerConfiguration=true +dataSource.elideSetAutoCommits=true +dataSource.maintainTimeStats=false \ No newline at end of file diff --git a/core/src/main/resources/locales/en_US.yml b/core/src/main/resources/locales/en_US.yml index 6ba729b7..7f86d124 100644 --- a/core/src/main/resources/locales/en_US.yml +++ b/core/src/main/resources/locales/en_US.yml @@ -14,6 +14,7 @@ msg: invalidID: §cThe quest with the id {0} doesn't exist. invalidPoolID: §cThe pool {0} does not exist. alreadyStarted: §cYou have already started the quest! + notStarted: §cYou are not currently doing this quest. quests: maxLaunched: §cYou cannot have more than {0} quest(s) at the same time... nopStep: §cThis quest doesn't have any step. @@ -88,6 +89,7 @@ msg: negative: §cYou must enter a positive number! zero: §cYou must enter a number other than 0! invalid: §c{0} isn't a valid number. + notInBounds: §cYour number must be between {0} and {1}. errorOccurred: '§cAn error occurred, contact an adminstrator! §4§lError code: {0}' commandsDisabled: §cCurrently you aren't allowed to execute commands! indexOutOfBounds: §cThe number {0} is out of bounds! It has to be between {1} and @@ -108,7 +110,7 @@ msg: downloadTranslations: syntax: '§cYou must specify a language to download. Example: "/quests downloadTranslations en_US".' notFound: '§cLanguage {0} not found for version {1}.' - exists: '§cThe file {0} already exists. Append "true" to your command to overwrite it. (/quests downloadTranslations true)' + exists: '§cThe file {0} already exists. Append "-overwrite" to your command to overwrite it. (/quests downloadTranslations -overwrite)' downloaded: '§aLanguage {0} has been downloaded! §7You must now edit the file "/plugins/BeautyQuests/config.yml" to change the value of §ominecraftTranslationsFile§7 with {0}, then restart the server.' checkpoint: @@ -147,7 +149,7 @@ msg: resetQuest: §6Removed datas of the quest for {0} players. startQuest: '§6You have forced starting of quest {0} (Player UUID: {1}).' startQuestNoRequirements: '§cThe player does not meet the requirements for the quest {0}... - Append "true" at the end of your command to bypass the requirements check.' + Append "-overrideRequirements" at the end of your command to bypass the requirements check.' cancelQuest: §6You have cancelled the quest {0}. cancelQuestUnavailable: §cThe quest {0} can't be cancelled. backupCreated: §6You have successfully created backups of all quests and player @@ -203,6 +205,7 @@ msg: blockData: '§aWrite the blockdata (available blockdatas: §7{0}§a):' blockTag: '§aWrite the block tag (available tags: §7{0}§a):' typeBucketAmount: '§aWrite the amount of buckets to fill:' + typeDamageAmount: 'Write the amount of damage player have to deal:' goToLocation: §aGo to the wanted location for the stage. typeLocationRadius: '§aWrite the required distance from the location:' typeGameTicks: '§aWrite the required game ticks:' @@ -296,7 +299,7 @@ msg: list: '§aA list of all Mythic Mobs:' isntMythicMob: §cThis Mythic Mob doesn't exist. disabled: §cMythicMob is disabled. - epicBossDoesntExist: §cThis Epic Boss doesn't exist. + advancedSpawnersMob: 'Write the name of the custom spawner mob to kill:' textList: syntax: '§cCorrect syntax: ' added: §aText "§7{0}§a" added. @@ -307,6 +310,7 @@ msg: remove: '§6remove : §eRemove a text.' list: '§6list: §eView all added texts.' close: '§6close: §eValidate the added texts.' + availableElements: 'Available elements: §e{0}' noSuchElement: '§cThere is no such element. Allowed elements are: §e{0}' invalidPattern: '§cInvalid regex pattern §4{0}§c.' comparisonTypeDefault: '§aChoose the comparison type you want among {0}. Default comparison is: §e§l{1}§r§a. Type §onull§r§a to use it.' @@ -322,6 +326,9 @@ msg: fadeIn: 'Write the "fade-in" duration, in ticks (20 ticks = 1 second).' stay: 'Write the "stay" duration, in ticks (20 ticks = 1 second).' fadeOut: 'Write the "fade-out" duration, in ticks (20 ticks = 1 second).' + colorNamed: 'Enter the name of a color.' + color: 'Enter a color in the hexadecimal format (#XXXXX) or RGB format (RED GRE BLU).' + invalidColor: 'The color you entered is invalid. It must be either hexadecimal or RGB.' firework: invalid: 'This item is not a valid firework.' invalidHand: 'You must hold a firework in your main hand.' @@ -364,6 +371,9 @@ inv: playTime: §ePlay time breedAnimals: §aBreed animals tameAnimals: §aTame animals + death: §cDie + dealDamage: §cDeal damage to mobs + eatDrink: §aEat or drink food or potions NPCText: §eEdit dialog dialogLines: '{0} lines' NPCSelect: §eChoose or create NPC @@ -402,6 +412,15 @@ inv: location: worldPattern: '§aSet world name pattern §d(ADVANCED)' worldPatternLore: 'If you want the stage to be completed for any world matching a specific pattern, enter a regex (regular expression) here.' + death: + causes: '§aSet death causes §d(ADVANCED)' + anyCause: Any death cause + setCauses: '{0} death cause(s)' + dealDamage: + damage: '§eDamage to deal' + targetMobs: '§cMobs to damage' + eatDrink: + items: '§eEdit items to eat or drink' stages: name: Create stages nextPage: §eNext page @@ -433,8 +452,6 @@ inv: startableFromGUILore: Allows the player to start the quest from the Quests Menu. scoreboardItem: Enable scoreboard scoreboardItemLore: If disabled, the quest will not be tracked through the scoreboard. - hideItem: Hide menu and dynmap - hideItemLore: If enabled, the quest will not be displayed on the dynmap nor in the Quests Menu. hideNoRequirementsItem: Hide when requirements not met hideNoRequirementsItemLore: If enabled, the quest will not be displayed in the Quests Menu when the requirements are not met. bypassLimit: Don't count quest limit @@ -491,6 +508,9 @@ inv: firework: §dEnding Firework fireworkLore: Firework launched when the player finishes the quest fireworkLoreDrop: Drop your custom firework here + visibility: §bQuest visibility + visibilityLore: Choose in which tabs of the menu will the quest be shown, and if + the quest is visible on dynamic maps. keepDatas: Preserve players datas keepDatasLore: |- Force the plugin to preserve players datas, even though stages have been edited. @@ -532,6 +552,7 @@ inv: §a§lLeft-click§a to edit amount. §a§l§nShift§a§l + left-click§a to edit mob name. §c§lRight-click§c to delete. + setLevel: Set minimum level mobSelect: name: Select mob type bukkitEntityType: §eSelect entity type @@ -687,6 +708,26 @@ inv: fadeIn: §aFade-in duration stay: §bStay duration fadeOut: §aFade-out duration + particleEffect: + name: Create particle effect + shape: §dParticle shape + type: §eParticle type + color: §bParticle color + particleList: + name: Particles list + colored: Colored particle + damageCause: + name: Damage cause + damageCausesList: + name: Damage causes list + visibility: + name: Quest visibility + notStarted: '"Not started" menu tab' + inProgress: '"In progress" menu tab' + finished: '"Finished" menu tab' + maps: 'Maps (such as dynmap or BlueMap)' + equipmentSlots: + name: Equipment slots scoreboard: name: §6§lQuests @@ -714,6 +755,11 @@ scoreboard: playTimeFormatted: §ePlay §6{0} breed: §eBreed §6{0} tame: §eTame §6{0} + die: §cDie + dealDamage: + any: §cDeal {0} damage + mobs: §cDeal {0} damage to {1} + eatDrink: §eConsume §6{0} indication: startQuest: §7Do you want to start the quest {0}? closeInventory: §7Are you sure you want to close the GUI? @@ -741,6 +787,7 @@ misc: editorPrefix: §a errorPrefix: §4✖ §c successPrefix: §2✔ §a + requirementNotMetPrefix: §c time: weeks: '{0} weeks' days: '{0} days' @@ -765,6 +812,9 @@ misc: playTime: Play time breedAnimals: Breed animals tameAnimals: Tame animals + die: Die + dealDamage: Deal damage + eatDrink: Eat or drink comparison: equals: equal to {0} different: different to {0} @@ -787,6 +837,7 @@ misc: quest: §aQuest required mcMMOSkillLevel: §dSkill level required money: §dMoney required + equipment: §eEquipment required bucket: water: Water bucket lava: Lava bucket @@ -798,6 +849,12 @@ misc: shift-right: Shift-right click shift-left: Shift-left click middle: Middle click + amounts: + items: '{0} item(s)' + comparisons: '{0} comparaison(s)' + dialogLines: '{0} line(s)' + permissions: '{0} permission(s)' + mobs: '{0} mob(s)' ticks: '{0} ticks' questItemLore: §e§oQuest Item hologramText: §8§lQuest NPC diff --git a/core/src/main/resources/locales/es_ES.yml b/core/src/main/resources/locales/es_ES.yml index 6e5962c4..01ee910b 100644 --- a/core/src/main/resources/locales/es_ES.yml +++ b/core/src/main/resources/locales/es_ES.yml @@ -2,12 +2,12 @@ msg: quest: finished: - base: '§aFelicitaciones! Has finalizado la misión§e{0}§a!' - obtain: '§aObtuviste {0}!' - started: '§aHas comenzado la misión §r§e{0}§o§6!' - created: '§aFelicitaciones! Tu has creado la misión §e{0}§a el cual incluye {1} rama(s)!' - edited: '§aFelicitaciones! Has editado la misión §e{0}§a el cual incluye ahora {1} rama(s)!' - createCancelled: '§cLa creación o edición a sido cancelada por otro plugin!' + base: '§a¡Felicitaciones! ¡Has finalizado la misión §e{0}§a!' + obtain: '§a¡Obtuviste {0}!' + started: '§a¡Has comenzado la misión §r§e{0}§o§6!' + created: '§a¡Felicitaciones! ¡Has creado la misión §e{0}§a el cual incluye {1} rama(s)!' + edited: '§a¡Felicitaciones! ¡Has editado la misión §e{0}§a el cual incluye ahora {1} rama(s)!' + createCancelled: '§c¡La creación o edición a sido cancelada por otro plugin!' cancelling: '§cProceso de creación de la misión cancelada.' editCancelling: '§cProceso de edición de misión cancelada.' invalidID: '§cLa misión con la id {0} no existe.' @@ -27,6 +27,7 @@ msg: questItem: drop: '§c¡No puedes soltar un objeto de misión!' craft: '§c¡No puedes usar un objeto de misión para fabricar!' + eat: '§c¡No puedes comer un objeto de misión!' stageMobs: noMobs: '§cEsta etapa no necesita ningún mob para matar.' listMobs: '§aDebes matar {0}.' @@ -39,6 +40,7 @@ msg: writeMessage: '§aEscribe el mensaje que se enviará al jugador' writeStartMessage: '§aEscribe el mensaje que será enviado al iniciar la misión, "null" si quieres el predeterminado o "none" si no quieres ninguno' writeEndMsg: '§aEscribe el mensaje que se enviará al final de la misión, "null" si quieres el predeterminado o "none" si no quieres ninguno. Puedes usar "{0}" que será reemplazado por las recompensas obtenidas.' + writeEndSound: '§aEscribe el nombre del sonido que se reproducirá al jugador al final de la misión, "null" si quieres el predeterminado o "none" si no quieres ninguno:' writeDescriptionText: '§aEscribe el texto que describe el objetivo de la etapa:' writeStageText: '§aEscribe el texto que será enviado al jugador al comienzo del paso:' moveToTeleportPoint: '§aIr a la ubicación de teletransporte deseada.' @@ -72,6 +74,7 @@ msg: negative: '§cDebes introducir un número positivo!' zero: '§cDebes introducir un número distinto de 0!' invalid: '§c{0} no es un número válido.' + notInBounds: '§cTu número debe estar entre {0} y {1}.' errorOccurred: '§cHa ocurrido un error, ¡contacta con un administrador! §4§lError: {0}' commandsDisabled: '§cActualmente no tienes permisos para ejecutar comandos!' indexOutOfBounds: '§cEl número {0} está fuera de límites! Debe estar entre {1} y {2}.' @@ -83,6 +86,10 @@ msg: playerNotOnline: '§cEl jugador {0} está desconectado.' playerDataNotFound: '§cDatas del jugador {0} no encontrados.' versionRequired: 'Versión requerida: §l{0}' + restartServer: '§7Reinicia tu servidor para ver las modificaciones.' + dialogs: + skipped: '§8§o Diálogo omitido.' + tooFar: '§7§oEstás demasiado lejos de {0}...' command: downloadTranslations: syntax: '§cDebes especificar un idioma para descargar. Ejemplo: "/quests downloadTranslations en_US".' @@ -98,6 +105,11 @@ msg: next: '§aLa etapa ha sido omitida.' nextUnavailable: '§cLa opción "skip" no está disponible cuando el jugador está al final de una rama.' set: '§aFase {0} lanzada.' + startDialog: + impossible: '§cNo es posible iniciar el diálogo ahora.' + noDialog: '§cEl jugador no tiene un diálogo pendiente.' + alreadyIn: '§cEl jugador ya está reproduciendo un diálogo.' + success: '§aEmpezó el diálogo para el jugador {0} en la misión {1}!' playerNeeded: '§cDebes ser un jugador para ejecutar este comando!' incorrectSyntax: '§cSintaxis incorrecta.' noPermission: '§cNo tienes suficientes permisos para ejecutar este comando! (Requiere: {0})' @@ -146,12 +158,14 @@ msg: remove: '§6/{0} remove : §eElimina una misión con un id especificado o haz clic en el NPC cuando no se haya definido.' finishAll: '§6/{0} finishAll : §eFinish all quests of a player.' setStage: '§6/{0} setStage [new branch] [new stage]: §eSkip the current stage/start the branch/set a stage for a branch.' + startDialog: '§6/{0} startDialog : §eComienza el diálogo pendiente para una etapa del NPC o el diálogo de inicio para una misión.' resetPlayer: '§6/{0} restablecido jugador : §eElimina toda la información sobre un jugador.' resetPlayerQuest: '§6/{0} restablecida la Misión de Jugador [id]: §eElimina información de una misión para un jugador.' seePlayer: '§6/{0} seePlayer : §eVer la informacion sobre un jugador' reload: '§6/{0} reload: §eGuarda y recarga todas las configuraciones y archivos. (§cdeprecado§e)' start: '§6/{0} start [id]: §eForzar el inicio de una mision' setItem: '§6/{0} setItem : §eGuarda el elemento del holograma.' + setFirework: '§6/{0} setFirework: §eEdita el fuego artificial por defecto al finalizar.' adminMode: '§6/{0} adminMode: §eActiva el Modo de administración. (Útil para mostrar mensajes de poco registro.)' version: '§6/{0} version: §eVer la versión actual del plugin.' downloadTranslations: '§6/{0} descargaTraducciones : §eDescargas un archivo de traducción de vaina' @@ -168,6 +182,9 @@ msg: typeLocationRadius: '§aEscribe la distancia requerida de la ubicación:' typeGameTicks: '§aEscribe los ticks requeridos:' already: '§cYa estás en el editor.' + stage: + location: + typeWorldPattern: '§aEscribe una expresión para nombres de mundo:' enter: title: '§6~ Modo editor ~' subtitle: '§2♣§d§kii§a§lSkyblock§d§kii§2♣ §aEscribe "/quests exitEditor" para forzar la salida del editor.' @@ -235,11 +252,15 @@ msg: close: '&6close: &eGuardas y validas los mensajes.' setTime: '§6setTime + + org.apache.maven.plugins + maven-shade-plugin + 3.3.0 + + + + revxrsal.commands + fr.skytasul.quests.commands.revxrsal + + + + + + package + + shade + + + + diff --git a/v1_19_R1/src/main/java/fr/skytasul/quests/utils/nms/v1_19_R1.java b/v1_19_R1/src/main/java/fr/skytasul/quests/utils/nms/v1_19_R1.java index 06295bc7..5c6aeb96 100644 --- a/v1_19_R1/src/main/java/fr/skytasul/quests/utils/nms/v1_19_R1.java +++ b/v1_19_R1/src/main/java/fr/skytasul/quests/utils/nms/v1_19_R1.java @@ -5,7 +5,7 @@ import org.apache.commons.lang.Validate; import org.bukkit.Material; import org.bukkit.craftbukkit.v1_19_R1.entity.CraftPlayer; -import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Entity; import org.bukkit.entity.Player; import net.minecraft.core.Registry; @@ -34,7 +34,7 @@ public void sendPacket(Player p, Object packet){ } @Override - public double entityNameplateHeight(LivingEntity en){ + public double entityNameplateHeight(Entity en) { return en.getHeight(); } diff --git a/v1_9_R1/pom.xml b/v1_9_R1/pom.xml index 16b1b7c8..b6060706 100644 --- a/v1_9_R1/pom.xml +++ b/v1_9_R1/pom.xml @@ -7,7 +7,7 @@ fr.skytasul beautyquests-parent - 0.19.7 + 0.20.0 true diff --git a/v1_9_R1/src/main/java/fr/skytasul/quests/utils/nms/v1_9_R1.java b/v1_9_R1/src/main/java/fr/skytasul/quests/utils/nms/v1_9_R1.java index 418e92cb..bdddae93 100644 --- a/v1_9_R1/src/main/java/fr/skytasul/quests/utils/nms/v1_9_R1.java +++ b/v1_9_R1/src/main/java/fr/skytasul/quests/utils/nms/v1_9_R1.java @@ -1,9 +1,9 @@ package fr.skytasul.quests.utils.nms; import org.apache.commons.lang.Validate; -import org.bukkit.craftbukkit.v1_9_R1.entity.CraftLivingEntity; +import org.bukkit.craftbukkit.v1_9_R1.entity.CraftEntity; import org.bukkit.craftbukkit.v1_9_R1.entity.CraftPlayer; -import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Entity; import org.bukkit.entity.Player; import net.minecraft.server.v1_9_R1.EnumChatFormat; @@ -28,8 +28,8 @@ public void sendPacket(Player p, Object packet){ } @Override - public double entityNameplateHeight(LivingEntity en){ - return ((CraftLivingEntity) en).getHandle().length; + public double entityNameplateHeight(Entity en){ + return ((CraftEntity) en).getHandle().length; } public Object getIChatBaseComponent(String text){ diff --git a/v1_9_R2/pom.xml b/v1_9_R2/pom.xml index 3773c909..a5904118 100644 --- a/v1_9_R2/pom.xml +++ b/v1_9_R2/pom.xml @@ -7,7 +7,7 @@ fr.skytasul beautyquests-parent - 0.19.7 + 0.20.0 true diff --git a/v1_9_R2/src/main/java/fr/skytasul/quests/utils/nms/v1_9_R2.java b/v1_9_R2/src/main/java/fr/skytasul/quests/utils/nms/v1_9_R2.java index b90ef2fb..fe858742 100644 --- a/v1_9_R2/src/main/java/fr/skytasul/quests/utils/nms/v1_9_R2.java +++ b/v1_9_R2/src/main/java/fr/skytasul/quests/utils/nms/v1_9_R2.java @@ -1,9 +1,9 @@ package fr.skytasul.quests.utils.nms; import org.apache.commons.lang.Validate; -import org.bukkit.craftbukkit.v1_9_R2.entity.CraftLivingEntity; +import org.bukkit.craftbukkit.v1_9_R2.entity.CraftEntity; import org.bukkit.craftbukkit.v1_9_R2.entity.CraftPlayer; -import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Entity; import org.bukkit.entity.Player; import net.minecraft.server.v1_9_R2.EnumChatFormat; @@ -28,8 +28,8 @@ public void sendPacket(Player p, Object packet){ } @Override - public double entityNameplateHeight(LivingEntity en){ - return ((CraftLivingEntity) en).getHandle().length; + public double entityNameplateHeight(Entity en){ + return ((CraftEntity) en).getHandle().length; } public Object getIChatBaseComponent(String text){