Compare commits

..

No commits in common. "master" and "1.5.0" have entirely different histories.

53 changed files with 1279 additions and 2570 deletions

68
pom.xml
View File

@ -4,9 +4,9 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>me.xginko</groupId> <groupId>me.xginko.VillagerOptimizer</groupId>
<artifactId>VillagerOptimizer</artifactId> <artifactId>VillagerOptimizer</artifactId>
<version>1.7.0</version> <version>1.5.0</version>
<packaging>jar</packaging> <packaging>jar</packaging>
<name>VillagerOptimizer</name> <name>VillagerOptimizer</name>
@ -23,7 +23,7 @@
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId> <artifactId>maven-compiler-plugin</artifactId>
<version>3.12.1</version> <version>3.11.0</version>
<configuration> <configuration>
<source>${java.version}</source> <source>${java.version}</source>
<target>${java.version}</target> <target>${java.version}</target>
@ -41,6 +41,10 @@
</goals> </goals>
<configuration> <configuration>
<relocations> <relocations>
<relocation>
<pattern>com.tcoded.folialib</pattern>
<shadedPattern>me.xginko.villageroptimizer.libs.folialib</shadedPattern>
</relocation>
<relocation> <relocation>
<pattern>com.github.benmanes.caffeine</pattern> <pattern>com.github.benmanes.caffeine</pattern>
<shadedPattern>me.xginko.villageroptimizer.libs.caffeine</shadedPattern> <shadedPattern>me.xginko.villageroptimizer.libs.caffeine</shadedPattern>
@ -57,27 +61,11 @@
<pattern>io.github.thatsmusic99.configurationmaster</pattern> <pattern>io.github.thatsmusic99.configurationmaster</pattern>
<shadedPattern>me.xginko.villageroptimizer.libs.configmaster</shadedPattern> <shadedPattern>me.xginko.villageroptimizer.libs.configmaster</shadedPattern>
</relocation> </relocation>
<relocation>
<pattern>org.reflections</pattern>
<shadedPattern>me.xginko.villageroptimizer.libs.reflections</shadedPattern>
</relocation>
<relocation>
<pattern>com.cryptomorin.xseries</pattern>
<shadedPattern>me.xginko.villageroptimizer.libs.xseries</shadedPattern>
</relocation>
<relocation>
<pattern>space.arim.morepaperlib</pattern>
<shadedPattern>me.xginko.villageroptimizer.libs.morepaperlib</shadedPattern>
</relocation>
</relocations> </relocations>
<filters> <filters>
<filter> <filter>
<artifact>*:*</artifact> <artifact>*:*</artifact>
<excludes> <excludes>
<exclude>com/cryptomorin/xseries/XBiome*</exclude>
<exclude>com/cryptomorin/xseries/NMSExtras*</exclude>
<exclude>com/cryptomorin/xseries/NoteBlockMusic*</exclude>
<exclude>com/cryptomorin/xseries/SkullCacheListener*</exclude>
<exclude>META-INF/MANIFEST.MF</exclude> <exclude>META-INF/MANIFEST.MF</exclude>
<exclude>META-INF/LICENSE</exclude> <exclude>META-INF/LICENSE</exclude>
<exclude>META-INF/LICENSE.txt</exclude> <exclude>META-INF/LICENSE.txt</exclude>
@ -112,8 +100,8 @@
<url>https://ci.pluginwiki.us/plugin/repository/everything/</url> <url>https://ci.pluginwiki.us/plugin/repository/everything/</url>
</repository> </repository>
<repository> <repository>
<id>morepaperlib-repo</id> <id>folialib-repo</id>
<url>https://mvn-repo.arim.space/lesser-gpl3/</url> <url>https://nexuslite.gcnt.net/repos/other/</url>
</repository> </repository>
</repositories> </repositories>
@ -125,39 +113,39 @@
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.apache.logging.log4j</groupId> <groupId>com.google.auto.service</groupId>
<artifactId>log4j-core</artifactId> <artifactId>auto-service</artifactId>
<version>2.23.1</version> <version>1.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.reflections</groupId>
<artifactId>reflections</artifactId>
<version>0.10.2</version>
</dependency> </dependency>
<!-- Adventure API for easier cross-version compatibility --> <!-- Adventure API for easier cross-version compatibility -->
<dependency> <dependency>
<groupId>net.kyori</groupId> <groupId>net.kyori</groupId>
<artifactId>adventure-platform-bukkit</artifactId> <artifactId>adventure-platform-bukkit</artifactId>
<version>4.3.3</version> <version>4.3.2</version>
</dependency> </dependency>
<!-- Adventure MiniMessage for parsing fancy tags in lang files --> <!-- Adventure MiniMessage for parsing fancy tags in lang files -->
<dependency> <dependency>
<groupId>net.kyori</groupId> <groupId>net.kyori</groupId>
<artifactId>adventure-text-minimessage</artifactId> <artifactId>adventure-text-minimessage</artifactId>
<version>4.17.0</version> <version>4.16.0</version>
</dependency> </dependency>
<!-- Needed to actually display colors in ComponentLogger --> <!-- Needed to actually display colors in ComponentLogger -->
<dependency> <dependency>
<groupId>net.kyori</groupId> <groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-ansi</artifactId> <artifactId>adventure-text-serializer-ansi</artifactId>
<version>4.17.0</version> <version>4.16.0</version>
</dependency> </dependency>
<!-- Adventure ComponentLogger for colorful slf4j logging --> <!-- Adventure ComponentLogger for colorful slf4j logging -->
<dependency> <dependency>
<groupId>net.kyori</groupId> <groupId>net.kyori</groupId>
<artifactId>adventure-text-logger-slf4j</artifactId> <artifactId>adventure-text-logger-slf4j</artifactId>
<version>4.17.0</version> <version>4.16.0</version>
</dependency>
<!-- Adventure plaintext serializer for reading unstyled content of components -->
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-plain</artifactId>
<version>4.16.0</version>
</dependency> </dependency>
<!-- Bukkit bStats --> <!-- Bukkit bStats -->
<dependency> <dependency>
@ -179,15 +167,9 @@
</dependency> </dependency>
<!-- Folia Support --> <!-- Folia Support -->
<dependency> <dependency>
<groupId>space.arim.morepaperlib</groupId> <groupId>com.tcoded</groupId>
<artifactId>morepaperlib</artifactId> <artifactId>FoliaLib</artifactId>
<version>0.4.3</version> <version>0.3.1</version>
</dependency>
<!-- Cross-Version Support -->
<dependency>
<groupId>com.github.cryptomorin</groupId>
<artifactId>XSeries</artifactId>
<version>11.2.1</version>
</dependency> </dependency>
</dependencies> </dependencies>
</project> </project>

View File

@ -0,0 +1,49 @@
package me.xginko.villageroptimizer;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.bukkit.entity.Villager;
import org.jetbrains.annotations.NotNull;
import java.time.Duration;
import java.util.UUID;
import java.util.concurrent.ConcurrentMap;
public final class VillagerCache {
private final @NotNull Cache<UUID, WrappedVillager> villagerCache;
public VillagerCache(long expireAfterWriteSeconds) {
this.villagerCache = Caffeine.newBuilder().expireAfterWrite(Duration.ofSeconds(expireAfterWriteSeconds)).build();
}
public @NotNull ConcurrentMap<UUID, WrappedVillager> cacheMap() {
return this.villagerCache.asMap();
}
public @NotNull WrappedVillager getOrAdd(@NotNull Villager villager) {
WrappedVillager wrappedVillager = this.villagerCache.getIfPresent(villager.getUniqueId());
return wrappedVillager == null ? this.add(new WrappedVillager(villager)) : this.add(wrappedVillager);
}
public @NotNull WrappedVillager add(@NotNull WrappedVillager villager) {
this.villagerCache.put(villager.villager().getUniqueId(), villager);
return villager;
}
public @NotNull WrappedVillager add(@NotNull Villager villager) {
return this.add(new WrappedVillager(villager));
}
public boolean contains(@NotNull UUID uuid) {
return this.villagerCache.getIfPresent(uuid) != null;
}
public boolean contains(@NotNull WrappedVillager villager) {
return this.contains(villager.villager().getUniqueId());
}
public boolean contains(@NotNull Villager villager) {
return this.contains(villager.getUniqueId());
}
}

View File

@ -1,146 +1,97 @@
package me.xginko.villageroptimizer; package me.xginko.villageroptimizer;
import com.github.benmanes.caffeine.cache.Cache; import com.tcoded.folialib.FoliaLib;
import com.github.benmanes.caffeine.cache.Caffeine;
import me.xginko.villageroptimizer.commands.VillagerOptimizerCommand; import me.xginko.villageroptimizer.commands.VillagerOptimizerCommand;
import me.xginko.villageroptimizer.config.Config; import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.config.LanguageCache; import me.xginko.villageroptimizer.config.LanguageCache;
import me.xginko.villageroptimizer.struct.enums.Permissions;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule; import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import me.xginko.villageroptimizer.utils.Util; import me.xginko.villageroptimizer.utils.GenericUtil;
import me.xginko.villageroptimizer.wrapper.WrappedVillager;
import net.kyori.adventure.platform.bukkit.BukkitAudiences; import net.kyori.adventure.platform.bukkit.BukkitAudiences;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextDecoration; import net.kyori.adventure.text.format.TextDecoration;
import net.kyori.adventure.text.logger.slf4j.ComponentLogger; import net.kyori.adventure.text.logger.slf4j.ComponentLogger;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.core.config.Configurator;
import org.bstats.bukkit.Metrics; import org.bstats.bukkit.Metrics;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.entity.Villager;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import space.arim.morepaperlib.MorePaperLib;
import space.arim.morepaperlib.commands.CommandRegistration;
import space.arim.morepaperlib.scheduling.GracefulScheduling;
import java.io.File; import java.io.File;
import java.io.FileNotFoundException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.util.Arrays; import java.util.*;
import java.util.Collections;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.jar.JarFile; import java.util.jar.JarFile;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
public final class VillagerOptimizer extends JavaPlugin { public final class VillagerOptimizer extends JavaPlugin {
private static VillagerOptimizer instance; private static VillagerOptimizer instance;
private static CommandRegistration commandRegistration; private static VillagerCache villagerCache;
private static GracefulScheduling scheduling; private static FoliaLib foliaLib;
private static Cache<Villager, WrappedVillager> wrapperCache;
private static Map<String, LanguageCache> languageCacheMap; private static Map<String, LanguageCache> languageCacheMap;
private static Config config; private static Config config;
private static BukkitAudiences audiences; private static BukkitAudiences audiences;
private static ComponentLogger logger; private static ComponentLogger logger;
private static Metrics bStats; private static Metrics bStats;
@Override
public void onLoad() {
// Disable reflection logging
String shadedLibs = getClass().getPackage().getName() + ".libs";
Configurator.setLevel(shadedLibs + ".reflections.Reflections", Level.OFF);
}
@Override @Override
public void onEnable() { public void onEnable() {
instance = this; instance = this;
MorePaperLib morePaperLib = new MorePaperLib(this); foliaLib = new FoliaLib(this);
commandRegistration = morePaperLib.commandRegistration();
scheduling = morePaperLib.scheduling();
audiences = BukkitAudiences.create(this); audiences = BukkitAudiences.create(this);
logger = ComponentLogger.logger(getLogger().getName()); logger = ComponentLogger.logger(getLogger().getName());
bStats = new Metrics(this, 19954); bStats = new Metrics(this, 19954);
if (getServer().getPluginManager().getPlugin("AntiVillagerLag") != null) { logger.info(Component.text("╭────────────────────────────────────────────────────────────╮").style(GenericUtil.STYLE));
logger.warn("While VillagerOptimizer can read data previously created by AVL, running"); logger.info(Component.text("│ │").style(GenericUtil.STYLE));
logger.warn("both plugins at the same time is unsafe and definitely will cause issues."); logger.info(Component.text("│ │").style(GenericUtil.STYLE));
logger.warn("To protect your game from corruption, VillagerOptimizer will now disable!"); logger.info(Component.text("│ _ __ _ __ __ │").style(GenericUtil.STYLE));
logger.warn("Please decide for one of the plugins!"); logger.info(Component.text("│ | | / /(_)/ // /___ _ ___ _ ___ ____ │").style(GenericUtil.STYLE));
getServer().getPluginManager().disablePlugin(this); logger.info(Component.text("│ | |/ // // // // _ `// _ `// -_)/ __/ │").style(GenericUtil.STYLE));
return; logger.info(Component.text("│ |___//_//_//_/ \\_,_/ \\_, / \\__//_/ │").style(GenericUtil.STYLE));
} logger.info(Component.text("│ ____ __ _ /___/_ │").style(GenericUtil.STYLE));
logger.info(Component.text("│ / __ \\ ___ / /_ (_)__ _ (_)___ ___ ____ │").style(GenericUtil.STYLE));
try { logger.info(Component.text("│ / /_/ // _ \\/ __// // ' \\ / //_ // -_)/ __/ │").style(GenericUtil.STYLE));
getDataFolder().mkdirs(); logger.info(Component.text("\\____// .__/\\__//_//_/_/_//_/ /__/\\__//_/ │").style(GenericUtil.STYLE));
} catch (Exception e) { logger.info(Component.text("│ /_/ by xGinko │").style(GenericUtil.STYLE));
logger.error("Failed to create plugin directory! Cannot enable!", e); logger.info(Component.text("│ │").style(GenericUtil.STYLE));
getServer().getPluginManager().disablePlugin(this); logger.info(Component.text("│ │").style(GenericUtil.STYLE));
return;
}
logger.info(Component.text("╭────────────────────────────────────────────────────────────╮").style(Util.PL_STYLE));
logger.info(Component.text("│ │").style(Util.PL_STYLE));
logger.info(Component.text("│ │").style(Util.PL_STYLE));
logger.info(Component.text("│ _ __ _ __ __ │").style(Util.PL_STYLE));
logger.info(Component.text("│ | | / /(_)/ // /___ _ ___ _ ___ ____ │").style(Util.PL_STYLE));
logger.info(Component.text("│ | |/ // // // // _ `// _ `// -_)/ __/ │").style(Util.PL_STYLE));
logger.info(Component.text("│ |___//_//_//_/ \\_,_/ \\_, / \\__//_/ │").style(Util.PL_STYLE));
logger.info(Component.text("│ ____ __ _ /___/_ │").style(Util.PL_STYLE));
logger.info(Component.text("│ / __ \\ ___ / /_ (_)__ _ (_)___ ___ ____ │").style(Util.PL_STYLE));
logger.info(Component.text("│ / /_/ // _ \\/ __// // ' \\ / //_ // -_)/ __/ │").style(Util.PL_STYLE));
logger.info(Component.text("\\____// .__/\\__//_//_/_/_//_/ /__/\\__//_/ │").style(Util.PL_STYLE));
logger.info(Component.text("│ /_/ by xGinko │").style(Util.PL_STYLE));
logger.info(Component.text("│ │").style(Util.PL_STYLE));
logger.info(Component.text("│ │").style(Util.PL_STYLE));
logger.info(Component.text("") logger.info(Component.text("")
.style(Util.PL_STYLE).append(Component.text("https://github.com/xGinko/VillagerOptimizer") .style(GenericUtil.STYLE).append(Component.text("https://github.com/xGinko/VillagerOptimizer")
.color(NamedTextColor.GRAY)).append(Component.text("").style(Util.PL_STYLE))); .color(NamedTextColor.GRAY)).append(Component.text("").style(GenericUtil.STYLE)));
logger.info(Component.text("│ │").style(Util.PL_STYLE)); logger.info(Component.text("│ │").style(GenericUtil.STYLE));
logger.info(Component.text("│ │").style(Util.PL_STYLE)); logger.info(Component.text("│ │").style(GenericUtil.STYLE));
Permissions.registerAll();
logger.info(Component.text("") logger.info(Component.text("")
.style(Util.PL_STYLE).append(Component.text(" ➤ Loading Config...").style(Util.PL_STYLE)) .style(GenericUtil.STYLE).append(Component.text(" ➤ Loading Translations...").style(GenericUtil.STYLE))
.append(Component.text("").style(Util.PL_STYLE))); .append(Component.text("").style(GenericUtil.STYLE)));
reloadConfiguration();
logger.info(Component.text("")
.style(Util.PL_STYLE).append(Component.text(" ➤ Loading Translations...").style(Util.PL_STYLE))
.append(Component.text("").style(Util.PL_STYLE)));
reloadLang(true); reloadLang(true);
logger.info(Component.text("") logger.info(Component.text("")
.style(Util.PL_STYLE).append(Component.text(" ✓ Done.").color(NamedTextColor.WHITE).decorate(TextDecoration.BOLD)) .style(GenericUtil.STYLE).append(Component.text(" ➤ Loading Config...").style(GenericUtil.STYLE))
.append(Component.text("").style(Util.PL_STYLE))); .append(Component.text("").style(GenericUtil.STYLE)));
logger.info(Component.text("│ │").style(Util.PL_STYLE)); reloadConfiguration();
logger.info(Component.text("│ │").style(Util.PL_STYLE)); logger.info(Component.text("")
logger.info(Component.text("╰────────────────────────────────────────────────────────────╯").style(Util.PL_STYLE)); .style(GenericUtil.STYLE).append(Component.text(" ✓ Done.").color(NamedTextColor.WHITE).decorate(TextDecoration.BOLD))
.append(Component.text("").style(GenericUtil.STYLE)));
logger.info(Component.text("│ │").style(GenericUtil.STYLE));
logger.info(Component.text("│ │").style(GenericUtil.STYLE));
logger.info(Component.text("╰────────────────────────────────────────────────────────────╯").style(GenericUtil.STYLE));
} }
@Override @Override
public void onDisable() { public void onDisable() {
VillagerOptimizerModule.ENABLED_MODULES.forEach(VillagerOptimizerModule::disable); VillagerOptimizerModule.modules.forEach(VillagerOptimizerModule::disable);
VillagerOptimizerModule.ENABLED_MODULES.clear(); VillagerOptimizerModule.modules.clear();
VillagerOptimizerCommand.COMMANDS.forEach(VillagerOptimizerCommand::disable); if (foliaLib != null) {
VillagerOptimizerCommand.COMMANDS.clear(); foliaLib.getImpl().cancelAllTasks();
if (wrapperCache != null) { foliaLib = null;
wrapperCache.cleanUp();
wrapperCache = null;
} }
if (scheduling != null) { if (villagerCache != null) {
scheduling.cancelGlobalTasks(); villagerCache.cacheMap().clear();
scheduling = null; villagerCache = null;
} }
if (audiences != null) { if (audiences != null) {
audiences.close(); audiences.close();
@ -150,49 +101,36 @@ public final class VillagerOptimizer extends JavaPlugin {
bStats.shutdown(); bStats.shutdown();
bStats = null; bStats = null;
} }
commandRegistration = null;
languageCacheMap = null;
instance = null;
config = null; config = null;
languageCacheMap = null;
logger = null; logger = null;
instance = null;
} }
public static @NotNull VillagerOptimizer getInstance() { public static @NotNull VillagerOptimizer getInstance() {
return instance; return instance;
} }
public static @NotNull Config getConfiguration() {
public static @NotNull GracefulScheduling scheduling() {
return scheduling;
}
public static @NotNull CommandRegistration commandRegistration() {
return commandRegistration;
}
public static @NotNull Cache<Villager, WrappedVillager> wrappers() {
return wrapperCache;
}
public static @NotNull Config config() {
return config; return config;
} }
public static @NotNull VillagerCache getCache() {
public static @NotNull ComponentLogger logger() { return villagerCache;
}
public static @NotNull FoliaLib getFoliaLib() {
return foliaLib;
}
public static @NotNull ComponentLogger getLog() {
return logger; return logger;
} }
public static @NotNull BukkitAudiences getAudiences() {
public static @NotNull BukkitAudiences audiences() {
return audiences; return audiences;
} }
public static @NotNull LanguageCache getLang(Locale locale) { public static @NotNull LanguageCache getLang(Locale locale) {
return getLang(locale.toString().toLowerCase()); return getLang(locale.toString().toLowerCase());
} }
public static @NotNull LanguageCache getLang(CommandSender commandSender) { public static @NotNull LanguageCache getLang(CommandSender commandSender) {
return commandSender instanceof Player ? getLang(((Player) commandSender).locale()) : getLang(config.default_lang); return commandSender instanceof Player ? getLang(((Player) commandSender).locale()) : getLang(config.default_lang);
} }
public static @NotNull LanguageCache getLang(String lang) { public static @NotNull LanguageCache getLang(String lang) {
if (!config.auto_lang) return languageCacheMap.get(config.default_lang.toString().toLowerCase()); if (!config.auto_lang) return languageCacheMap.get(config.default_lang.toString().toLowerCase());
return languageCacheMap.getOrDefault(lang.replace("-", "_"), languageCacheMap.get(config.default_lang.toString().toLowerCase())); return languageCacheMap.getOrDefault(lang.replace("-", "_"), languageCacheMap.get(config.default_lang.toString().toLowerCase()));
@ -206,54 +144,62 @@ public final class VillagerOptimizer extends JavaPlugin {
private void reloadConfiguration() { private void reloadConfiguration() {
try { try {
config = new Config(); config = new Config();
if (wrapperCache != null) wrapperCache.cleanUp(); villagerCache = new VillagerCache(config.cache_keep_time_seconds);
wrapperCache = Caffeine.newBuilder().expireAfterWrite(config.cache_keep_time).build();
VillagerOptimizerCommand.reloadCommands(); VillagerOptimizerCommand.reloadCommands();
VillagerOptimizerModule.reloadModules(); VillagerOptimizerModule.reloadModules();
config.saveConfig(); config.saveConfig();
} catch (Exception exception) { } catch (Exception exception) {
logger.error("Error during config reload!", exception); logger.error("Error loading config!", exception);
} }
} }
private void reloadLang(boolean logFancy) { private void reloadLang(boolean startup) {
languageCacheMap = new HashMap<>();
try { try {
final SortedSet<String> availableLocales = getAvailableTranslations(); File langDirectory = new File(getDataFolder() + File.separator + "lang");
if (!config.auto_lang) { Files.createDirectories(langDirectory.toPath());
final String defaultLang = config.default_lang.toString().replace("-", "_").toLowerCase(); for (String fileName : getDefaultLanguageFiles()) {
if (!availableLocales.contains(defaultLang)) final String localeString = fileName.substring(fileName.lastIndexOf('/') + 1, fileName.lastIndexOf('.'));
throw new FileNotFoundException("Could not find any translation file for language '" + config.default_lang + "'"); if (startup) logger.info(
availableLocales.removeIf(localeString -> !localeString.equalsIgnoreCase(defaultLang)); Component.text("").style(GenericUtil.STYLE)
}
languageCacheMap = new HashMap<>(availableLocales.size());
for (String localeString : availableLocales) {
if (logFancy) logger.info(Component.text("").style(Util.PL_STYLE)
.append(Component.text(" "+localeString).color(NamedTextColor.WHITE).decorate(TextDecoration.BOLD)) .append(Component.text(" "+localeString).color(NamedTextColor.WHITE).decorate(TextDecoration.BOLD))
.append(Component.text("").style(Util.PL_STYLE))); .append(Component.text("").style(GenericUtil.STYLE)));
else logger.info(String.format("Found language file for %s", localeString)); else logger.info(String.format("Found language file for %s", localeString));
languageCacheMap.put(localeString, new LanguageCache(localeString)); languageCacheMap.put(localeString, new LanguageCache(localeString));
} }
} catch (Throwable t) { final Pattern langPattern = Pattern.compile("([a-z]{1,3}_[a-z]{1,3})(\\.yml)", Pattern.CASE_INSENSITIVE);
if (logFancy) logger.error(Component.text("").style(Util.PL_STYLE) for (File langFile : langDirectory.listFiles()) {
final Matcher langMatcher = langPattern.matcher(langFile.getName());
if (langMatcher.find()) {
String localeString = langMatcher.group(1).toLowerCase();
if (!languageCacheMap.containsKey(localeString)) { // make sure it wasn't a default file that we already loaded
if (startup) logger.info(
Component.text("").style(GenericUtil.STYLE)
.append(Component.text(" "+localeString).color(NamedTextColor.WHITE).decorate(TextDecoration.BOLD))
.append(Component.text("").style(GenericUtil.STYLE)));
else logger.info(String.format("Found language file for %s", localeString));
languageCacheMap.put(localeString, new LanguageCache(localeString));
}
}
}
} catch (Exception e) {
if (startup) logger.error(
Component.text("").style(GenericUtil.STYLE)
.append(Component.text("LANG ERROR").color(NamedTextColor.RED).decorate(TextDecoration.BOLD)) .append(Component.text("LANG ERROR").color(NamedTextColor.RED).decorate(TextDecoration.BOLD))
.append(Component.text("").style(Util.PL_STYLE)), t); .append(Component.text("").style(GenericUtil.STYLE)), e);
else logger.error("Error while loading translation files!", t); else logger.error("Error loading language files!", e);
} }
} }
private @NotNull SortedSet<String> getAvailableTranslations() { private @NotNull Set<String> getDefaultLanguageFiles() {
try (final JarFile pluginJar = new JarFile(getFile())) { try (final JarFile pluginJarFile = new JarFile(this.getFile())) {
final File langDirectory = new File(getDataFolder() + "/lang"); return pluginJarFile.stream()
Files.createDirectories(langDirectory.toPath()); .map(ZipEntry::getName)
final Pattern langPattern = Pattern.compile("([a-z]{1,3}_[a-z]{1,3})(\\.yml)", Pattern.CASE_INSENSITIVE); .filter(name -> name.startsWith("lang/") && name.endsWith(".yml"))
return Stream.concat(pluginJar.stream().map(ZipEntry::getName), Arrays.stream(langDirectory.listFiles()).map(File::getName)) .collect(Collectors.toSet());
.map(langPattern::matcher) } catch (IOException e) {
.filter(Matcher::find) logger.error("Failed getting default lang files!", e);
.map(matcher -> matcher.group(1)) return Collections.emptySet();
.collect(Collectors.toCollection(TreeSet::new));
} catch (Throwable t) {
logger.error("Failed while searching for available translations!", t);
return Collections.emptySortedSet();
} }
} }
} }

View File

@ -0,0 +1,326 @@
package me.xginko.villageroptimizer;
import me.xginko.villageroptimizer.enums.Keyring;
import me.xginko.villageroptimizer.enums.OptimizationType;
import org.bukkit.Location;
import org.bukkit.Particle;
import org.bukkit.Sound;
import org.bukkit.entity.Villager;
import org.bukkit.entity.memory.MemoryKey;
import org.bukkit.inventory.MerchantRecipe;
import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.persistence.PersistentDataType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.concurrent.TimeUnit;
@SuppressWarnings("ALL")
public final class WrappedVillager {
private final @NotNull Villager villager;
private final @NotNull PersistentDataContainer dataContainer;
private final boolean parseOther;
WrappedVillager(@NotNull Villager villager) {
this.villager = villager;
this.dataContainer = villager.getPersistentDataContainer();
this.parseOther = VillagerOptimizer.getConfiguration().support_other_plugins;
}
/**
* @return The villager inside the wrapper.
*/
public @NotNull Villager villager() {
return villager;
}
/**
* @return The data container inside the wrapper.
*/
public @NotNull PersistentDataContainer dataContainer() {
return dataContainer;
}
/**
* @return True if the villager is optimized by either this plugin or a supported alternative, otherwise false.
*/
public boolean isOptimized() {
if (!parseOther) {
return isOptimized(Keyring.Spaces.VillagerOptimizer);
}
for (Keyring.Spaces pluginNamespaces : Keyring.Spaces.values()) {
if (isOptimized(pluginNamespaces)) return true;
}
return false;
}
/**
* @return True if the villager is optimized by the supported plugin, otherwise false.
*/
public boolean isOptimized(Keyring.Spaces namespace) {
if (namespace == Keyring.Spaces.VillagerOptimizer) {
return dataContainer.has(Keyring.VillagerOptimizer.OPTIMIZATION_TYPE.getKey(), PersistentDataType.STRING);
} else {
return dataContainer.has(Keyring.AntiVillagerLag.OPTIMIZED_ANY.getKey(), PersistentDataType.STRING)
|| dataContainer.has(Keyring.AntiVillagerLag.OPTIMIZED_WORKSTATION.getKey(), PersistentDataType.STRING)
|| dataContainer.has(Keyring.AntiVillagerLag.OPTIMIZED_BLOCK.getKey(), PersistentDataType.STRING);
}
}
/**
* @param cooldown_millis The configured cooldown in millis until the next optimization is allowed to occur.
* @return True if villager can be optimized again, otherwise false.
*/
public boolean canOptimize(final long cooldown_millis) {
if (parseOther) {
if (
dataContainer.has(Keyring.AntiVillagerLag.NEXT_OPTIMIZATION_SYSTIME_SECONDS.getKey(), PersistentDataType.LONG)
&& System.currentTimeMillis() <= 1000 * dataContainer.get(Keyring.AntiVillagerLag.NEXT_OPTIMIZATION_SYSTIME_SECONDS.getKey(), PersistentDataType.LONG)
) {
return false;
}
}
return System.currentTimeMillis() > getLastOptimize() + cooldown_millis;
}
/**
* @param type OptimizationType the villager should be set to.
*/
public void setOptimizationType(final OptimizationType type) {
VillagerOptimizer.getFoliaLib().getImpl().runAtEntityTimer(villager, setOptimization -> {
// Keep repeating task until villager is no longer trading with a player
if (villager.isTrading()) return;
if (type == OptimizationType.NONE) {
if (isOptimized(Keyring.Spaces.VillagerOptimizer)) {
dataContainer.remove(Keyring.VillagerOptimizer.OPTIMIZATION_TYPE.getKey());
}
if (parseOther) {
if (dataContainer.has(Keyring.AntiVillagerLag.OPTIMIZED_ANY.getKey(), PersistentDataType.STRING))
dataContainer.remove(Keyring.AntiVillagerLag.OPTIMIZED_ANY.getKey());
if (dataContainer.has(Keyring.AntiVillagerLag.OPTIMIZED_WORKSTATION.getKey(), PersistentDataType.STRING))
dataContainer.remove(Keyring.AntiVillagerLag.OPTIMIZED_WORKSTATION.getKey());
if (dataContainer.has(Keyring.AntiVillagerLag.OPTIMIZED_BLOCK.getKey(), PersistentDataType.STRING))
dataContainer.remove(Keyring.AntiVillagerLag.OPTIMIZED_BLOCK.getKey());
}
villager.setAware(true);
villager.setAI(true);
} else {
dataContainer.set(Keyring.VillagerOptimizer.OPTIMIZATION_TYPE.getKey(), PersistentDataType.STRING, type.name());
villager.setAware(false);
}
// End repeating task once logic is finished
setOptimization.cancel();
}, 0L, 1L, TimeUnit.SECONDS);
}
/**
* @return The current OptimizationType of the villager.
*/
public @NotNull OptimizationType getOptimizationType() {
if (!parseOther) {
return getOptimizationType(Keyring.Spaces.VillagerOptimizer);
}
OptimizationType optimizationType = getOptimizationType(Keyring.Spaces.VillagerOptimizer);
if (optimizationType != OptimizationType.NONE) {
return optimizationType;
}
return getOptimizationType(Keyring.Spaces.AntiVillagerLag);
}
public @NotNull OptimizationType getOptimizationType(Keyring.Spaces namespaces) {
if (namespaces == Keyring.Spaces.VillagerOptimizer) {
if (!isOptimized(Keyring.Spaces.VillagerOptimizer)) {
return OptimizationType.valueOf(dataContainer.get(Keyring.VillagerOptimizer.OPTIMIZATION_TYPE.getKey(), PersistentDataType.STRING));
} else {
return OptimizationType.NONE;
}
}
if (namespaces == Keyring.Spaces.AntiVillagerLag) {
if (dataContainer.has(Keyring.AntiVillagerLag.OPTIMIZED_BLOCK.getKey(), PersistentDataType.STRING)) {
return OptimizationType.BLOCK;
}
if (dataContainer.has(Keyring.AntiVillagerLag.OPTIMIZED_WORKSTATION.getKey(), PersistentDataType.STRING)) {
return OptimizationType.WORKSTATION;
}
if (dataContainer.has(Keyring.AntiVillagerLag.OPTIMIZED_ANY.getKey(), PersistentDataType.STRING)) {
return OptimizationType.COMMAND; // Best we can do
}
return OptimizationType.NONE;
}
return OptimizationType.NONE;
}
/**
* Saves the system time in millis when the villager was last optimized.
*/
public void saveOptimizeTime() {
dataContainer.set(Keyring.VillagerOptimizer.LAST_OPTIMIZE.getKey(), PersistentDataType.LONG, System.currentTimeMillis());
}
/**
* @return The system time in millis when the villager was last optimized, 0L if the villager was never optimized.
*/
public long getLastOptimize() {
if (dataContainer.has(Keyring.VillagerOptimizer.LAST_OPTIMIZE.getKey(), PersistentDataType.LONG)) {
return dataContainer.get(Keyring.VillagerOptimizer.LAST_OPTIMIZE.getKey(), PersistentDataType.LONG);
}
return 0L;
}
/**
* Here for convenience so the remaining millis since the last stored optimize time
* can be easily calculated.
* This enables new configured cooldowns to instantly apply instead of them being persistent.
*
* @param cooldown_millis The configured cooldown in milliseconds you want to check against.
* @return The time left in millis until the villager can be optimized again.
*/
public long getOptimizeCooldownMillis(final long cooldown_millis) {
long remainingMillis = 0L;
if (parseOther) {
if (dataContainer.has(Keyring.AntiVillagerLag.NEXT_OPTIMIZATION_SYSTIME_SECONDS.getKey(), PersistentDataType.LONG)) {
remainingMillis = System.currentTimeMillis() - dataContainer.get(Keyring.AntiVillagerLag.NEXT_OPTIMIZATION_SYSTIME_SECONDS.getKey(), PersistentDataType.LONG);
}
}
if (remainingMillis > 0) return remainingMillis;
if (dataContainer.has(Keyring.VillagerOptimizer.LAST_OPTIMIZE.getKey(), PersistentDataType.LONG)) {
return System.currentTimeMillis() - (dataContainer.get(Keyring.VillagerOptimizer.LAST_OPTIMIZE.getKey(), PersistentDataType.LONG) + cooldown_millis);
}
return cooldown_millis;
}
/**
* Here for convenience so the remaining millis since the last stored restock time
* can be easily calculated.
*
* @param cooldown_millis The configured cooldown in milliseconds you want to check against.
* @return True if the villager has been loaded long enough.
*/
public boolean canRestock(final long cooldown_millis) {
return getLastRestock() + cooldown_millis <= System.currentTimeMillis();
}
/**
* Restock all trading recipes.
*/
public void restock() {
for (MerchantRecipe recipe : villager.getRecipes()) {
recipe.setUses(0);
}
}
/**
* Saves the time of the in-game world when the entity was last restocked.
*/
public void saveRestockTime() {
dataContainer.set(Keyring.VillagerOptimizer.LAST_RESTOCK.getKey(), PersistentDataType.LONG, System.currentTimeMillis());
}
/**
* @return The time of the in-game world when the entity was last restocked.
*/
public long getLastRestock() {
long lastRestock = 0L;
if (dataContainer.has(Keyring.VillagerOptimizer.LAST_RESTOCK.getKey(), PersistentDataType.LONG)) {
lastRestock = dataContainer.get(Keyring.VillagerOptimizer.LAST_RESTOCK.getKey(), PersistentDataType.LONG);
}
if (parseOther) {
if (dataContainer.has(Keyring.AntiVillagerLag.LAST_RESTOCK_WORLDFULLTIME.getKey(), PersistentDataType.LONG)) {
long lastAVLRestock = dataContainer.get(Keyring.AntiVillagerLag.LAST_RESTOCK_WORLDFULLTIME.getKey(), PersistentDataType.LONG);
if (lastRestock < lastAVLRestock) {
lastRestock = lastAVLRestock;
}
}
}
return lastRestock;
}
public long getRestockCooldownMillis(final long cooldown_millis) {
if (dataContainer.has(Keyring.VillagerOptimizer.LAST_RESTOCK.getKey(), PersistentDataType.LONG))
return System.currentTimeMillis() - (dataContainer.get(Keyring.VillagerOptimizer.LAST_RESTOCK.getKey(), PersistentDataType.LONG) + cooldown_millis);
return cooldown_millis;
}
/**
* @return The level between 1-5 calculated from the villagers experience.
*/
public int calculateLevel() {
// https://minecraft.fandom.com/wiki/Trading#Mechanics
int vilEXP = villager.getVillagerExperience();
if (vilEXP >= 250) return 5;
if (vilEXP >= 150) return 4;
if (vilEXP >= 70) return 3;
if (vilEXP >= 10) return 2;
return 1;
}
/**
* @return true if the villager can loose his acquired profession by having their workstation destroyed.
*/
public boolean canLooseProfession() {
// A villager with a level of 1 and no trading experience is liable to lose its profession.
return villager.getVillagerLevel() <= 1 && villager.getVillagerExperience() <= 0;
}
/**
* @param cooldown_millis The configured cooldown in milliseconds you want to check against.
* @return Whether the villager can be leveled up or not with the checked milliseconds
*/
public boolean canLevelUp(final long cooldown_millis) {
if (System.currentTimeMillis() < getLastLevelUpTime() + cooldown_millis) {
return false;
}
if (parseOther) {
return !dataContainer.has(Keyring.AntiVillagerLag.NEXT_LEVELUP_SYSTIME_SECONDS.getKey(), PersistentDataType.LONG)
|| System.currentTimeMillis() > dataContainer.get(Keyring.AntiVillagerLag.NEXT_LEVELUP_SYSTIME_SECONDS.getKey(), PersistentDataType.LONG) * 1000;
}
return true;
}
/**
* Saves the time of the in-game world when the entity was last leveled up.
*/
public void saveLastLevelUp() {
dataContainer.set(Keyring.VillagerOptimizer.LAST_LEVELUP.getKey(), PersistentDataType.LONG, System.currentTimeMillis());
}
/**
* Here for convenience so the remaining millis since the last stored level-up time
* can be easily calculated.
*
* @return The time of the in-game world when the entity was last leveled up.
*/
public long getLastLevelUpTime() {
if (dataContainer.has(Keyring.VillagerOptimizer.LAST_LEVELUP.getKey(), PersistentDataType.LONG))
return dataContainer.get(Keyring.VillagerOptimizer.LAST_LEVELUP.getKey(), PersistentDataType.LONG);
return 0L;
}
public long getLevelCooldownMillis(final long cooldown_millis) {
if (dataContainer.has(Keyring.VillagerOptimizer.LAST_LEVELUP.getKey(), PersistentDataType.LONG))
return System.currentTimeMillis() - (dataContainer.get(Keyring.VillagerOptimizer.LAST_LEVELUP.getKey(), PersistentDataType.LONG) + cooldown_millis);
return cooldown_millis;
}
public void sayNo() {
try {
villager.shakeHead();
} catch (NoSuchMethodError e) {
villager.getWorld().playSound(villager.getEyeLocation(), Sound.ENTITY_VILLAGER_NO, 1.0F, 1.0F);
villager.getWorld().spawnParticle(Particle.CLOUD, villager.getEyeLocation(), 4);
}
}
public @Nullable Location getJobSite() {
return villager.getMemory(MemoryKey.JOB_SITE);
}
}

View File

@ -1,36 +1,11 @@
package me.xginko.villageroptimizer.commands; package me.xginko.villageroptimizer.commands;
import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.TextComponent;
import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays; public abstract class SubCommand {
public abstract String getLabel();
public abstract class SubCommand implements CommandExecutor, TabCompleter { public abstract TextComponent getDescription();
public abstract TextComponent getSyntax();
private final String label; public abstract void perform(CommandSender sender, String[] args);
private final TextComponent syntax, description;
public SubCommand(String label, TextComponent syntax, TextComponent description) {
this.label = label;
this.syntax = syntax;
this.description = description;
}
public @NotNull String mergeArgs(@NotNull String[] args, int start) {
return String.join(" ", Arrays.copyOfRange(args, start, args.length));
}
public @NotNull String label() {
return label;
}
public @NotNull TextComponent syntax() {
return syntax;
}
public @NotNull TextComponent description() {
return description;
}
} }

View File

@ -1,62 +1,37 @@
package me.xginko.villageroptimizer.commands; package me.xginko.villageroptimizer.commands;
import me.xginko.villageroptimizer.VillagerOptimizer; import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.struct.Disableable; import me.xginko.villageroptimizer.commands.optimizevillagers.OptVillagersRadius;
import me.xginko.villageroptimizer.struct.Enableable; import me.xginko.villageroptimizer.commands.unoptimizevillagers.UnOptVillagersRadius;
import org.bukkit.command.CommandException; import me.xginko.villageroptimizer.commands.villageroptimizer.VillagerOptimizerCmd;
import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandExecutor;
import org.bukkit.command.PluginCommand; import org.bukkit.command.CommandMap;
import org.bukkit.command.TabCompleter; import org.bukkit.command.TabCompleter;
import org.jetbrains.annotations.NotNull;
import org.reflections.Reflections;
import org.reflections.scanners.Scanners;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set;
public abstract class VillagerOptimizerCommand implements Enableable, Disableable, CommandExecutor, TabCompleter { public interface VillagerOptimizerCommand extends CommandExecutor, TabCompleter {
public static final Set<VillagerOptimizerCommand> COMMANDS = new HashSet<>(); String label();
public static final List<String> RADIUS_SUGGESTIONS = Arrays.asList("5", "10", "25", "50");
public static final Reflections COMMANDS_PACKAGE = new Reflections(VillagerOptimizerCommand.class.getPackage().getName());
public final PluginCommand pluginCommand; List<String> NO_TABCOMPLETES = Collections.emptyList();
List<String> RADIUS_TABCOMPLETES = Arrays.asList("5", "10", "25", "50");
protected VillagerOptimizerCommand(@NotNull String name) throws CommandException { HashSet<VillagerOptimizerCommand> commands = new HashSet<>();
PluginCommand pluginCommand = VillagerOptimizer.getInstance().getCommand(name);
if (pluginCommand != null) this.pluginCommand = pluginCommand;
else throw new CommandException("Command cannot be enabled because it's not defined in the plugin.yml.");
}
public static void reloadCommands() { static void reloadCommands() {
COMMANDS.forEach(VillagerOptimizerCommand::disable); VillagerOptimizer plugin = VillagerOptimizer.getInstance();
COMMANDS.clear(); CommandMap commandMap = plugin.getServer().getCommandMap();
commands.forEach(command -> plugin.getCommand(command.label()).unregister(commandMap));
commands.clear();
for (Class<?> clazz : COMMANDS_PACKAGE.get(Scanners.SubTypes.of(VillagerOptimizerCommand.class).asClass())) { commands.add(new VillagerOptimizerCmd());
if (clazz.isInterface() || Modifier.isAbstract(clazz.getModifiers())) continue; commands.add(new OptVillagersRadius());
commands.add(new UnOptVillagersRadius());
try { commands.forEach(command -> plugin.getCommand(command.label()).setExecutor(command));
COMMANDS.add((VillagerOptimizerCommand) clazz.getDeclaredConstructor().newInstance());
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
VillagerOptimizer.logger().error("Failed initialising command class '{}'.", clazz.getSimpleName(), e);
}
}
COMMANDS.forEach(VillagerOptimizerCommand::enable);
}
@Override
public void enable() {
pluginCommand.setExecutor(this);
pluginCommand.setTabCompleter(this);
}
@Override
public void disable() {
pluginCommand.unregister(VillagerOptimizer.commandRegistration().getServerCommandMap());
} }
} }

View File

@ -1,13 +1,14 @@
package me.xginko.villageroptimizer.commands.optimizevillagers; package me.xginko.villageroptimizer.commands.optimizevillagers;
import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.VillagerOptimizer; import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.WrappedVillager;
import me.xginko.villageroptimizer.commands.VillagerOptimizerCommand; import me.xginko.villageroptimizer.commands.VillagerOptimizerCommand;
import me.xginko.villageroptimizer.config.Config; import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.struct.enums.OptimizationType; import me.xginko.villageroptimizer.enums.OptimizationType;
import me.xginko.villageroptimizer.struct.enums.Permissions; import me.xginko.villageroptimizer.enums.Permissions;
import me.xginko.villageroptimizer.events.VillagerOptimizeEvent; import me.xginko.villageroptimizer.events.VillagerOptimizeEvent;
import me.xginko.villageroptimizer.utils.KyoriUtil; import me.xginko.villageroptimizer.utils.KyoriUtil;
import me.xginko.villageroptimizer.wrapper.WrappedVillager;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextReplacementConfig; import net.kyori.adventure.text.TextReplacementConfig;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
@ -21,17 +22,15 @@ import org.bukkit.entity.Villager;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.Collections;
import java.util.List; import java.util.List;
public class OptVillagersRadius extends VillagerOptimizerCommand { public class OptVillagersRadius implements VillagerOptimizerCommand {
private final long cooldown; private final long cooldown;
private final int max_radius; private final int max_radius;
public OptVillagersRadius() { public OptVillagersRadius() {
super("optimizevillagers"); Config config = VillagerOptimizer.getConfiguration();
Config config = VillagerOptimizer.config();
this.max_radius = config.getInt("optimization-methods.commands.optimizevillagers.max-block-radius", 100); this.max_radius = config.getInt("optimization-methods.commands.optimizevillagers.max-block-radius", 100);
this.cooldown = config.getInt("optimization-methods.commands.optimizevillagers.cooldown-seconds", 600, this.cooldown = config.getInt("optimization-methods.commands.optimizevillagers.cooldown-seconds", 600,
"Cooldown in seconds until a villager can be optimized again using the command.\n" + "Cooldown in seconds until a villager can be optimized again using the command.\n" +
@ -39,16 +38,17 @@ public class OptVillagersRadius extends VillagerOptimizerCommand {
} }
@Override @Override
public @Nullable List<String> onTabComplete( public String label() {
@NotNull CommandSender sender, @NotNull Command command, @NotNull String commandLabel, @NotNull String[] args return "optimizevillagers";
) {
return args.length == 1 ? RADIUS_SUGGESTIONS : Collections.emptyList();
} }
@Override @Override
public boolean onCommand( public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
@NotNull CommandSender sender, @NotNull Command command, @NotNull String commandLabel, @NotNull String[] args return args.length == 1 ? RADIUS_TABCOMPLETES : NO_TABCOMPLETES;
) { }
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
if (!sender.hasPermission(Permissions.Commands.OPTIMIZE_RADIUS.get())) { if (!sender.hasPermission(Permissions.Commands.OPTIMIZE_RADIUS.get())) {
KyoriUtil.sendMessage(sender, VillagerOptimizer.getLang(sender).no_permission); KyoriUtil.sendMessage(sender, VillagerOptimizer.getLang(sender).no_permission);
return true; return true;
@ -89,6 +89,7 @@ public class OptVillagersRadius extends VillagerOptimizerCommand {
return true; return true;
} }
VillagerCache villagerCache = VillagerOptimizer.getCache();
int successCount = 0; int successCount = 0;
int failCount = 0; int failCount = 0;
final boolean player_has_cooldown_bypass = player.hasPermission(Permissions.Bypass.COMMAND_COOLDOWN.get()); final boolean player_has_cooldown_bypass = player.hasPermission(Permissions.Bypass.COMMAND_COOLDOWN.get());
@ -99,7 +100,7 @@ public class OptVillagersRadius extends VillagerOptimizerCommand {
Villager.Profession profession = villager.getProfession(); Villager.Profession profession = villager.getProfession();
if (profession.equals(Villager.Profession.NITWIT) || profession.equals(Villager.Profession.NONE)) continue; if (profession.equals(Villager.Profession.NITWIT) || profession.equals(Villager.Profession.NONE)) continue;
WrappedVillager wVillager = VillagerOptimizer.wrappers().get(villager, WrappedVillager::new); WrappedVillager wVillager = villagerCache.getOrAdd(villager);
if (player_has_cooldown_bypass || wVillager.canOptimize(cooldown)) { if (player_has_cooldown_bypass || wVillager.canOptimize(cooldown)) {
VillagerOptimizeEvent optimizeEvent = new VillagerOptimizeEvent(wVillager, OptimizationType.COMMAND, player); VillagerOptimizeEvent optimizeEvent = new VillagerOptimizeEvent(wVillager, OptimizationType.COMMAND, player);

View File

@ -1,12 +1,13 @@
package me.xginko.villageroptimizer.commands.unoptimizevillagers; package me.xginko.villageroptimizer.commands.unoptimizevillagers;
import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.VillagerOptimizer; import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.WrappedVillager;
import me.xginko.villageroptimizer.commands.VillagerOptimizerCommand; import me.xginko.villageroptimizer.commands.VillagerOptimizerCommand;
import me.xginko.villageroptimizer.struct.enums.OptimizationType; import me.xginko.villageroptimizer.enums.OptimizationType;
import me.xginko.villageroptimizer.struct.enums.Permissions; import me.xginko.villageroptimizer.enums.Permissions;
import me.xginko.villageroptimizer.events.VillagerUnoptimizeEvent; import me.xginko.villageroptimizer.events.VillagerUnoptimizeEvent;
import me.xginko.villageroptimizer.utils.KyoriUtil; import me.xginko.villageroptimizer.utils.KyoriUtil;
import me.xginko.villageroptimizer.wrapper.WrappedVillager;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextReplacementConfig; import net.kyori.adventure.text.TextReplacementConfig;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
@ -20,30 +21,29 @@ import org.bukkit.entity.Villager;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.Collections;
import java.util.List; import java.util.List;
public class UnOptVillagersRadius extends VillagerOptimizerCommand { public class UnOptVillagersRadius implements VillagerOptimizerCommand {
private final int max_radius; private final int max_radius;
public UnOptVillagersRadius() { public UnOptVillagersRadius() {
super("unoptimizevillagers"); this.max_radius = VillagerOptimizer.getConfiguration()
this.max_radius = VillagerOptimizer.config()
.getInt("optimization-methods.commands.unoptimizevillagers.max-block-radius", 100); .getInt("optimization-methods.commands.unoptimizevillagers.max-block-radius", 100);
} }
@Override @Override
public @Nullable List<String> onTabComplete( public String label() {
@NotNull CommandSender sender, @NotNull Command command, @NotNull String commandLabel, @NotNull String[] args return "unoptimizevillagers";
) {
return args.length == 1 ? RADIUS_SUGGESTIONS : Collections.emptyList();
} }
@Override @Override
public boolean onCommand( public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
@NotNull CommandSender sender, @NotNull Command command, @NotNull String commandLabel, @NotNull String[] args return args.length == 1 ? RADIUS_TABCOMPLETES : NO_TABCOMPLETES;
) { }
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
if (!sender.hasPermission(Permissions.Commands.UNOPTIMIZE_RADIUS.get())) { if (!sender.hasPermission(Permissions.Commands.UNOPTIMIZE_RADIUS.get())) {
KyoriUtil.sendMessage(sender, VillagerOptimizer.getLang(sender).no_permission); KyoriUtil.sendMessage(sender, VillagerOptimizer.getLang(sender).no_permission);
return true; return true;
@ -84,6 +84,7 @@ public class UnOptVillagersRadius extends VillagerOptimizerCommand {
return true; return true;
} }
VillagerCache villagerCache = VillagerOptimizer.getCache();
int successCount = 0; int successCount = 0;
for (Entity entity : player.getNearbyEntities(safeRadius, safeRadius, safeRadius)) { for (Entity entity : player.getNearbyEntities(safeRadius, safeRadius, safeRadius)) {
@ -92,7 +93,7 @@ public class UnOptVillagersRadius extends VillagerOptimizerCommand {
Villager.Profession profession = villager.getProfession(); Villager.Profession profession = villager.getProfession();
if (profession.equals(Villager.Profession.NITWIT) || profession.equals(Villager.Profession.NONE)) continue; if (profession.equals(Villager.Profession.NITWIT) || profession.equals(Villager.Profession.NONE)) continue;
WrappedVillager wVillager = VillagerOptimizer.wrappers().get(villager, WrappedVillager::new); WrappedVillager wVillager = villagerCache.getOrAdd(villager);
if (wVillager.isOptimized()) { if (wVillager.isOptimized()) {
VillagerUnoptimizeEvent unOptimizeEvent = new VillagerUnoptimizeEvent(wVillager, player, OptimizationType.COMMAND); VillagerUnoptimizeEvent unOptimizeEvent = new VillagerUnoptimizeEvent(wVillager, player, OptimizationType.COMMAND);

View File

@ -5,81 +5,71 @@ import me.xginko.villageroptimizer.commands.VillagerOptimizerCommand;
import me.xginko.villageroptimizer.commands.villageroptimizer.subcommands.DisableSubCmd; import me.xginko.villageroptimizer.commands.villageroptimizer.subcommands.DisableSubCmd;
import me.xginko.villageroptimizer.commands.villageroptimizer.subcommands.ReloadSubCmd; import me.xginko.villageroptimizer.commands.villageroptimizer.subcommands.ReloadSubCmd;
import me.xginko.villageroptimizer.commands.villageroptimizer.subcommands.VersionSubCmd; import me.xginko.villageroptimizer.commands.villageroptimizer.subcommands.VersionSubCmd;
import me.xginko.villageroptimizer.struct.enums.Permissions; import me.xginko.villageroptimizer.enums.Permissions;
import me.xginko.villageroptimizer.utils.GenericUtil;
import me.xginko.villageroptimizer.utils.KyoriUtil; import me.xginko.villageroptimizer.utils.KyoriUtil;
import me.xginko.villageroptimizer.utils.Util;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.Command; import org.bukkit.command.Command;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class VillagerOptimizerCmd extends VillagerOptimizerCommand { public class VillagerOptimizerCmd implements VillagerOptimizerCommand {
private final List<SubCommand> subCommands; private final List<SubCommand> subCommands;
private final List<String> tabCompletes; private final List<String> tabCompleter;
public VillagerOptimizerCmd() { public VillagerOptimizerCmd() {
super("villageroptimizer");
subCommands = Arrays.asList(new ReloadSubCmd(), new VersionSubCmd(), new DisableSubCmd()); subCommands = Arrays.asList(new ReloadSubCmd(), new VersionSubCmd(), new DisableSubCmd());
tabCompletes = subCommands.stream().map(SubCommand::label).collect(Collectors.toList()); tabCompleter = subCommands.stream().map(SubCommand::getLabel).collect(Collectors.toList());
} }
@Override @Override
public @Nullable List<String> onTabComplete( public String label() {
@NotNull CommandSender sender, @NotNull Command command, @NotNull String commandLabel, @NotNull String[] args return "villageroptimizer";
) {
if (args.length == 1) {
return tabCompletes;
}
if (args.length >= 2) {
for (SubCommand subCommand : subCommands) {
if (args[0].equalsIgnoreCase(subCommand.label())) {
return subCommand.onTabComplete(sender, command, commandLabel, args);
}
}
}
return Collections.emptyList();
} }
@Override @Override
public boolean onCommand( public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, String[] args) {
@NotNull CommandSender sender, @NotNull Command command, @NotNull String commandLabel, @NotNull String[] args return args.length == 1 ? tabCompleter : NO_TABCOMPLETES;
) {
if (args.length >= 1) {
for (SubCommand subCommand : subCommands) {
if (args[0].equalsIgnoreCase(subCommand.label())) {
return subCommand.onCommand(sender, command, commandLabel, args);
}
}
} }
overview(sender); @Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
if (args.length == 0) {
sendCommandOverview(sender);
return true; return true;
} }
private void overview(CommandSender sender) { for (final SubCommand subCommand : subCommands) {
if (args[0].equalsIgnoreCase(subCommand.getLabel())) {
subCommand.perform(sender, args);
return true;
}
}
sendCommandOverview(sender);
return true;
}
private void sendCommandOverview(CommandSender sender) {
if (!sender.hasPermission(Permissions.Commands.RELOAD.get()) && !sender.hasPermission(Permissions.Commands.VERSION.get())) return; if (!sender.hasPermission(Permissions.Commands.RELOAD.get()) && !sender.hasPermission(Permissions.Commands.VERSION.get())) return;
KyoriUtil.sendMessage(sender, Component.text("-----------------------------------------------------").color(NamedTextColor.GRAY)); KyoriUtil.sendMessage(sender, Component.text("-----------------------------------------------------").color(NamedTextColor.GRAY));
KyoriUtil.sendMessage(sender, Component.text("VillagerOptimizer Commands").color(Util.PL_COLOR)); KyoriUtil.sendMessage(sender, Component.text("VillagerOptimizer Commands").color(GenericUtil.COLOR));
KyoriUtil.sendMessage(sender, Component.text("-----------------------------------------------------").color(NamedTextColor.GRAY)); KyoriUtil.sendMessage(sender, Component.text("-----------------------------------------------------").color(NamedTextColor.GRAY));
subCommands.forEach(subCommand -> KyoriUtil.sendMessage(sender, subCommands.forEach(subCommand -> KyoriUtil.sendMessage(sender,
subCommand.syntax().append(Component.text(" - ").color(NamedTextColor.DARK_GRAY)).append(subCommand.description()))); subCommand.getSyntax().append(Component.text(" - ").color(NamedTextColor.DARK_GRAY)).append(subCommand.getDescription())));
KyoriUtil.sendMessage(sender, KyoriUtil.sendMessage(sender,
Component.text("/optimizevillagers <blockradius>").color(Util.PL_COLOR) Component.text("/optimizevillagers <blockradius>").color(GenericUtil.COLOR)
.append(Component.text(" - ").color(NamedTextColor.DARK_GRAY)) .append(Component.text(" - ").color(NamedTextColor.DARK_GRAY))
.append(Component.text("Optimize villagers in a radius").color(NamedTextColor.GRAY)) .append(Component.text("Optimize villagers in a radius").color(NamedTextColor.GRAY))
); );
KyoriUtil.sendMessage(sender, KyoriUtil.sendMessage(sender,
Component.text("/unoptmizevillagers <blockradius>").color(Util.PL_COLOR) Component.text("/unoptmizevillagers <blockradius>").color(GenericUtil.COLOR)
.append(Component.text(" - ").color(NamedTextColor.DARK_GRAY)) .append(Component.text(" - ").color(NamedTextColor.DARK_GRAY))
.append(Component.text("Unoptimize villagers in a radius").color(NamedTextColor.GRAY)) .append(Component.text("Unoptimize villagers in a radius").color(NamedTextColor.GRAY))
); );

View File

@ -2,51 +2,44 @@ package me.xginko.villageroptimizer.commands.villageroptimizer.subcommands;
import me.xginko.villageroptimizer.VillagerOptimizer; import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.commands.SubCommand; import me.xginko.villageroptimizer.commands.SubCommand;
import me.xginko.villageroptimizer.struct.enums.Permissions; import me.xginko.villageroptimizer.enums.Permissions;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule; import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import me.xginko.villageroptimizer.utils.GenericUtil;
import me.xginko.villageroptimizer.utils.KyoriUtil; import me.xginko.villageroptimizer.utils.KyoriUtil;
import me.xginko.villageroptimizer.utils.Util;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collections;
import java.util.List;
public class DisableSubCmd extends SubCommand { public class DisableSubCmd extends SubCommand {
public DisableSubCmd() { @Override
super( public String getLabel() {
"disable", return "disable";
Component.text("/villageroptimizer disable").color(Util.PL_COLOR),
Component.text("Disable all plugin tasks and listeners.").color(NamedTextColor.GRAY)
);
} }
@Override @Override
public @Nullable List<String> onTabComplete( public TextComponent getDescription() {
@NotNull CommandSender sender, @NotNull Command command, @NotNull String commandLabel, @NotNull String[] args return Component.text("Disable all plugin tasks and listeners.").color(NamedTextColor.GRAY);
) {
return Collections.emptyList();
} }
@Override @Override
public boolean onCommand( public TextComponent getSyntax() {
@NotNull CommandSender sender, @NotNull Command command, @NotNull String commandLabel, @NotNull String[] args return Component.text("/villageroptimizer disable").color(GenericUtil.COLOR);
) { }
@Override
public void perform(CommandSender sender, String[] args) {
if (!sender.hasPermission(Permissions.Commands.DISABLE.get())) { if (!sender.hasPermission(Permissions.Commands.DISABLE.get())) {
KyoriUtil.sendMessage(sender, VillagerOptimizer.getLang(sender).no_permission); KyoriUtil.sendMessage(sender, VillagerOptimizer.getLang(sender).no_permission);
return true; return;
} }
KyoriUtil.sendMessage(sender, Component.text("Disabling VillagerOptimizer...").color(NamedTextColor.RED)); KyoriUtil.sendMessage(sender, Component.text("Disabling VillagerOptimizer...").color(NamedTextColor.RED));
VillagerOptimizerModule.ENABLED_MODULES.forEach(VillagerOptimizerModule::disable); VillagerOptimizerModule.modules.forEach(VillagerOptimizerModule::disable);
VillagerOptimizerModule.ENABLED_MODULES.clear(); VillagerOptimizerModule.modules.clear();
VillagerOptimizer.getCache().cacheMap().clear();
KyoriUtil.sendMessage(sender, Component.text("Disabled all plugin listeners and tasks.").color(NamedTextColor.GREEN)); KyoriUtil.sendMessage(sender, Component.text("Disabled all plugin listeners and tasks.").color(NamedTextColor.GREEN));
KyoriUtil.sendMessage(sender, Component.text("You can enable the plugin again using the reload command.").color(NamedTextColor.YELLOW)); KyoriUtil.sendMessage(sender, Component.text("You can enable the plugin again using the reload command.").color(NamedTextColor.YELLOW));
return true;
} }
} }

View File

@ -2,49 +2,42 @@ package me.xginko.villageroptimizer.commands.villageroptimizer.subcommands;
import me.xginko.villageroptimizer.VillagerOptimizer; import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.commands.SubCommand; import me.xginko.villageroptimizer.commands.SubCommand;
import me.xginko.villageroptimizer.struct.enums.Permissions; import me.xginko.villageroptimizer.enums.Permissions;
import me.xginko.villageroptimizer.utils.GenericUtil;
import me.xginko.villageroptimizer.utils.KyoriUtil; import me.xginko.villageroptimizer.utils.KyoriUtil;
import me.xginko.villageroptimizer.utils.Util;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collections;
import java.util.List;
public class ReloadSubCmd extends SubCommand { public class ReloadSubCmd extends SubCommand {
public ReloadSubCmd() { @Override
super( public String getLabel() {
"reload", return "reload";
Component.text("/villageroptimizer reload").color(Util.PL_COLOR),
Component.text("Reload the plugin configuration.").color(NamedTextColor.GRAY));
} }
@Override @Override
public @Nullable List<String> onTabComplete( public TextComponent getDescription() {
@NotNull CommandSender sender, @NotNull Command command, @NotNull String commandLabel, @NotNull String[] args return Component.text("Reload the plugin configuration.").color(NamedTextColor.GRAY);
) {
return Collections.emptyList();
} }
@Override @Override
public boolean onCommand( public TextComponent getSyntax() {
@NotNull CommandSender sender, @NotNull Command command, @NotNull String commandLabel, @NotNull String[] args return Component.text("/villageroptimizer reload").color(GenericUtil.COLOR);
) { }
@Override
public void perform(CommandSender sender, String[] args) {
if (!sender.hasPermission(Permissions.Commands.RELOAD.get())) { if (!sender.hasPermission(Permissions.Commands.RELOAD.get())) {
KyoriUtil.sendMessage(sender, VillagerOptimizer.getLang(sender).no_permission); KyoriUtil.sendMessage(sender, VillagerOptimizer.getLang(sender).no_permission);
return true; return;
} }
KyoriUtil.sendMessage(sender, Component.text("Reloading VillagerOptimizer...").color(NamedTextColor.WHITE)); KyoriUtil.sendMessage(sender, Component.text("Reloading VillagerOptimizer...").color(NamedTextColor.WHITE));
VillagerOptimizer.scheduling().asyncScheduler().run(reload -> { VillagerOptimizer.getFoliaLib().getImpl().runNextTick(reload -> { // Reload in sync with the server
VillagerOptimizer.getInstance().reloadPlugin(); VillagerOptimizer.getInstance().reloadPlugin();
KyoriUtil.sendMessage(sender, Component.text("Reload complete.").color(NamedTextColor.GREEN)); KyoriUtil.sendMessage(sender, Component.text("Reload complete.").color(NamedTextColor.GREEN));
}); });
return true;
} }
} }

View File

@ -3,45 +3,39 @@ package me.xginko.villageroptimizer.commands.villageroptimizer.subcommands;
import io.papermc.paper.plugin.configuration.PluginMeta; import io.papermc.paper.plugin.configuration.PluginMeta;
import me.xginko.villageroptimizer.VillagerOptimizer; import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.commands.SubCommand; import me.xginko.villageroptimizer.commands.SubCommand;
import me.xginko.villageroptimizer.struct.enums.Permissions; import me.xginko.villageroptimizer.enums.Permissions;
import me.xginko.villageroptimizer.utils.GenericUtil;
import me.xginko.villageroptimizer.utils.KyoriUtil; import me.xginko.villageroptimizer.utils.KyoriUtil;
import me.xginko.villageroptimizer.utils.Util;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.event.ClickEvent; import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
import org.bukkit.plugin.PluginDescriptionFile; import org.bukkit.plugin.PluginDescriptionFile;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collections;
import java.util.List;
public class VersionSubCmd extends SubCommand { public class VersionSubCmd extends SubCommand {
public VersionSubCmd() { @Override
super( public String getLabel() {
"version", return "version";
Component.text("/villageroptimizer version").color(Util.PL_COLOR),
Component.text("Show the plugin version.").color(NamedTextColor.GRAY)
);
} }
@Override @Override
public @Nullable List<String> onTabComplete( public TextComponent getDescription() {
@NotNull CommandSender sender, @NotNull Command command, @NotNull String commandLabel, @NotNull String[] args return Component.text("Show the plugin version.").color(NamedTextColor.GRAY);
) {
return Collections.emptyList();
} }
@Override @Override
public boolean onCommand( public TextComponent getSyntax() {
@NotNull CommandSender sender, @NotNull Command command, @NotNull String commandLabel, @NotNull String[] args return Component.text("/villageroptimizer version").color(GenericUtil.COLOR);
) { }
@Override
@SuppressWarnings({"deprecation", "UnstableApiUsage"})
public void perform(CommandSender sender, String[] args) {
if (!sender.hasPermission(Permissions.Commands.VERSION.get())) { if (!sender.hasPermission(Permissions.Commands.VERSION.get())) {
KyoriUtil.sendMessage(sender, VillagerOptimizer.getLang(sender).no_permission); KyoriUtil.sendMessage(sender, VillagerOptimizer.getLang(sender).no_permission);
return true; return;
} }
String name, version, website, author; String name, version, website, author;
@ -63,7 +57,7 @@ public class VersionSubCmd extends SubCommand {
KyoriUtil.sendMessage(sender, Component.newline() KyoriUtil.sendMessage(sender, Component.newline()
.append( .append(
Component.text(name + " " + version) Component.text(name + " " + version)
.style(Util.PL_STYLE) .style(GenericUtil.STYLE)
.clickEvent(ClickEvent.openUrl(website)) .clickEvent(ClickEvent.openUrl(website))
) )
.append(Component.text(" by ").color(NamedTextColor.GRAY)) .append(Component.text(" by ").color(NamedTextColor.GRAY))
@ -74,7 +68,5 @@ public class VersionSubCmd extends SubCommand {
) )
.append(Component.newline()) .append(Component.newline())
); );
return true;
} }
} }

View File

@ -5,7 +5,6 @@ import me.xginko.villageroptimizer.VillagerOptimizer;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.io.File; import java.io.File;
import java.time.Duration;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
@ -13,36 +12,37 @@ public class Config {
private final @NotNull ConfigFile config; private final @NotNull ConfigFile config;
public final @NotNull Locale default_lang; public final @NotNull Locale default_lang;
public final @NotNull Duration cache_keep_time;
public final boolean auto_lang, support_other_plugins; public final boolean auto_lang, support_other_plugins;
public final long cache_keep_time_seconds;
public Config() throws Exception { public Config() throws Exception {
// Create plugin folder first if it does not exist yet
File pluginFolder = VillagerOptimizer.getInstance().getDataFolder();
if (!pluginFolder.exists() && !pluginFolder.mkdir())
VillagerOptimizer.getLog().error("Failed to create plugin directory.");
// Load config.yml with ConfigMaster // Load config.yml with ConfigMaster
this.config = ConfigFile.loadConfig(new File(VillagerOptimizer.getInstance().getDataFolder(), "config.yml")); this.config = ConfigFile.loadConfig(new File(pluginFolder, "config.yml"));
structureConfig(); structureConfig();
this.default_lang = Locale.forLanguageTag( this.default_lang = Locale.forLanguageTag(
getString("general.default-language", "en_us", getString("general.default-language", "en_us",
"The default language that will be used if auto-language is false\n" + "The default language that will be used if auto-language is false or no matching language file was found.")
"or no matching language file was found.")
.replace("_", "-")); .replace("_", "-"));
this.auto_lang = getBoolean("general.auto-language", true, this.auto_lang = getBoolean("general.auto-language", true,
"If set to true, will display messages based on client language"); "If set to true, will display messages based on client language");
this.cache_keep_time = Duration.ofSeconds(Math.max(1, getInt("general.cache-keep-time-seconds", 30, this.cache_keep_time_seconds = getInt("general.cache-keep-time-seconds", 30,
"The amount of time in seconds a villager will be kept in the plugin's cache."))); "The amount of time in seconds a villager will be kept in the plugin's cache.");
this.support_other_plugins = getBoolean("general.support-avl-villagers", false, this.support_other_plugins = getBoolean("general.support-avl-villagers", false,
"Enable if you have previously used AntiVillagerLag\n" + "Enable if you have previously used AntiVillagerLag (https://www.spigotmc.org/resources/antivillagerlag.102949/).\n" +
"(https://www.spigotmc.org/resources/antivillagerlag.102949/).\n" + "Tries to read pre-existing info like optimization state so players don't need to reoptimize their villagers.");
"Tries to read pre-existing info like optimization state so players\n" +
"don't need to reoptimize their villagers.");
} }
public void saveConfig() { public void saveConfig() {
try { try {
this.config.save(); this.config.save();
} catch (Throwable throwable) { } catch (Throwable throwable) {
VillagerOptimizer.logger().error("Failed to save config file!", throwable); VillagerOptimizer.getLog().error("Failed to save config file!", throwable);
} }
} }
@ -118,13 +118,13 @@ public class Config {
return this.config.getInteger(path, def); return this.config.getInteger(path, def);
} }
public @NotNull <T> List<T> getList(@NotNull String path, @NotNull List<T> def, @NotNull String comment) { public @NotNull List<String> getList(@NotNull String path, @NotNull List<String> def, @NotNull String comment) {
this.config.addDefault(path, def, comment); this.config.addDefault(path, def, comment);
return this.config.getList(path); return this.config.getStringList(path);
} }
public @NotNull <T> List<T> getList(@NotNull String path, @NotNull List<T> def) { public @NotNull List<String> getList(@NotNull String path, @NotNull List<String> def) {
this.config.addDefault(path, def); this.config.addDefault(path, def);
return this.config.getList(path); return this.config.getStringList(path);
} }
} }

View File

@ -20,7 +20,6 @@ public class LanguageCache {
public final @NotNull List<Component> nametag_optimize_success, nametag_on_optimize_cooldown, nametag_unoptimize_success, public final @NotNull List<Component> nametag_optimize_success, nametag_on_optimize_cooldown, nametag_unoptimize_success,
block_optimize_success, block_on_optimize_cooldown, block_unoptimize_success, block_optimize_success, block_on_optimize_cooldown, block_unoptimize_success,
workstation_optimize_success, workstation_on_optimize_cooldown, workstation_unoptimize_success, workstation_optimize_success, workstation_on_optimize_cooldown, workstation_unoptimize_success,
activity_optimize_success,
command_optimize_success, command_radius_limit_exceed, command_optimize_fail, command_unoptimize_success, command_optimize_success, command_radius_limit_exceed, command_optimize_fail, command_unoptimize_success,
command_specify_radius, command_radius_invalid, command_no_villagers_nearby, command_specify_radius, command_radius_invalid, command_no_villagers_nearby,
trades_restocked, optimize_for_trading, villager_leveling_up; trades_restocked, optimize_for_trading, villager_leveling_up;
@ -31,7 +30,7 @@ public class LanguageCache {
// Check if the lang folder has already been created // Check if the lang folder has already been created
File parent = langYML.getParentFile(); File parent = langYML.getParentFile();
if (!parent.exists() && !parent.mkdir()) if (!parent.exists() && !parent.mkdir())
VillagerOptimizer.logger().error("Failed to create lang directory."); VillagerOptimizer.getLog().error("Failed to create lang directory.");
// Check if the file already exists and save the one from the plugin's resources folder if it does not // Check if the file already exists and save the one from the plugin's resources folder if it does not
if (!langYML.exists()) if (!langYML.exists())
plugin.saveResource("lang/" + locale + ".yml", false); plugin.saveResource("lang/" + locale + ".yml", false);
@ -68,10 +67,6 @@ public class LanguageCache {
"<gray>You need to wait %time% until you can optimize this villager again."); "<gray>You need to wait %time% until you can optimize this villager again.");
this.workstation_unoptimize_success = getListTranslation("messages.workstation.unoptimize-success", this.workstation_unoptimize_success = getListTranslation("messages.workstation.unoptimize-success",
"<green>Successfully unoptimized %villagertype% villager by removing workstation block %blocktype%."); "<green>Successfully unoptimized %villagertype% villager by removing workstation block %blocktype%.");
// Activity
this.activity_optimize_success = getListTranslation("messages.activity.optimized-near-you",
"<gray>%amount% villagers close to you were automatically optimized due to high activity.");
// Command // Command
this.command_optimize_success = getListTranslation("messages.command.optimize-success", this.command_optimize_success = getListTranslation("messages.command.optimize-success",
"<green>Successfully optimized %amount% villager(s) in a radius of %radius% blocks."); "<green>Successfully optimized %amount% villager(s) in a radius of %radius% blocks.");
@ -91,7 +86,7 @@ public class LanguageCache {
try { try {
this.lang.save(); this.lang.save();
} catch (Throwable throwable) { } catch (Throwable throwable) {
VillagerOptimizer.logger().error("Failed to save language file: " + langYML.getName(), throwable); VillagerOptimizer.getLog().error("Failed to save language file: "+ langYML.getName(), throwable);
} }
} }

View File

@ -1,4 +1,4 @@
package me.xginko.villageroptimizer.struct.enums; package me.xginko.villageroptimizer.enums;
import net.kyori.adventure.key.Namespaced; import net.kyori.adventure.key.Namespaced;
import org.bukkit.Keyed; import org.bukkit.Keyed;
@ -12,7 +12,7 @@ import java.util.Locale;
public final class Keyring { public final class Keyring {
public enum Space implements Namespaced { public enum Spaces implements Namespaced {
VillagerOptimizer("VillagerOptimizer"), VillagerOptimizer("VillagerOptimizer"),
AntiVillagerLag("AntiVillagerLag"); AntiVillagerLag("AntiVillagerLag");
@ -20,7 +20,7 @@ public final class Keyring {
@Pattern("[a-z0-9_\\-.]+") @Pattern("[a-z0-9_\\-.]+")
private final @NotNull String namespace; private final @NotNull String namespace;
Space(@NotNull @Pattern("[a-z0-9_\\-.]+") String pluginName) { Spaces(@NotNull @Pattern("[a-z0-9_\\-.]+") String pluginName) {
this.namespace = pluginName.toLowerCase(Locale.ROOT); this.namespace = pluginName.toLowerCase(Locale.ROOT);
} }
@ -44,20 +44,20 @@ public final class Keyring {
* from a {@link PersistentDataContainer} * from a {@link PersistentDataContainer}
*/ */
public static NamespacedKey getKey(@NotNull String pluginName, @NotNull String key) { public static NamespacedKey getKey(@NotNull String pluginName, @NotNull String key) {
return new NamespacedKey(pluginName.toLowerCase(Locale.ROOT), key.toLowerCase(Locale.ROOT)); return new NamespacedKey(pluginName.toLowerCase(Locale.ROOT), key);
} }
public enum VillagerOptimizer implements Keyed { public enum VillagerOptimizer implements Keyed {
OPTIMIZATION_TYPE("optimization-type"), OPTIMIZATION_TYPE("optimization-type"),
LAST_OPTIMIZE_SYSTIME_MILLIS("last-optimize"), LAST_OPTIMIZE("last-optimize"),
LAST_LEVELUP_SYSTIME_MILLIS("last-levelup"), LAST_LEVELUP("last-levelup"),
LAST_RESTOCK_WORLD_FULLTIME("last-restock-full-time"); LAST_RESTOCK("last-restock");
private final @NotNull NamespacedKey key; private final @NotNull NamespacedKey key;
VillagerOptimizer(@NotNull String key) { VillagerOptimizer(@NotNull String key) {
this.key = Keyring.getKey(Space.VillagerOptimizer.namespace(), key); this.key = new NamespacedKey(Spaces.VillagerOptimizer.namespace(), key);
} }
@Override @Override
@ -69,7 +69,7 @@ public final class Keyring {
public enum AntiVillagerLag implements Keyed { public enum AntiVillagerLag implements Keyed {
NEXT_OPTIMIZATION_SYSTIME_SECONDS("cooldown"), // Returns LONG -> (System.currentTimeMillis() / 1000) + cooldown seconds NEXT_OPTIMIZATION_SYSTIME_SECONDS("cooldown"), // Returns LONG -> (System.currentTimeMillis() / 1000) + cooldown seconds
LAST_RESTOCK_WORLD_FULLTIME("time"), // Returns LONG -> villager.getWorld().getFullTime() LAST_RESTOCK_WORLDFULLTIME("time"), // Returns LONG -> villager.getWorld().getFullTime()
NEXT_LEVELUP_SYSTIME_SECONDS("levelCooldown"), // Returns LONG -> (System.currentTimeMillis() / 1000) + cooldown seconds NEXT_LEVELUP_SYSTIME_SECONDS("levelCooldown"), // Returns LONG -> (System.currentTimeMillis() / 1000) + cooldown seconds
OPTIMIZED_ANY("Marker"), // Returns STRING -> "AVL" OPTIMIZED_ANY("Marker"), // Returns STRING -> "AVL"
OPTIMIZED_BLOCK("disabledByBlock"), // Returns STRING -> key().toString() OPTIMIZED_BLOCK("disabledByBlock"), // Returns STRING -> key().toString()
@ -78,7 +78,7 @@ public final class Keyring {
private final @NotNull NamespacedKey key; private final @NotNull NamespacedKey key;
AntiVillagerLag(@NotNull String avlKey) { AntiVillagerLag(@NotNull String avlKey) {
this.key = Keyring.getKey(Space.AntiVillagerLag.namespace(), avlKey); this.key = new NamespacedKey(Spaces.AntiVillagerLag.namespace(), avlKey);
} }
@Override @Override

View File

@ -1,8 +1,6 @@
package me.xginko.villageroptimizer.struct.enums; package me.xginko.villageroptimizer.enums;
public enum OptimizationType { public enum OptimizationType {
CHUNK_LIMIT,
REGIONAL_ACTIVITY,
COMMAND, COMMAND,
NAMETAG, NAMETAG,
WORKSTATION, WORKSTATION,

View File

@ -1,6 +1,5 @@
package me.xginko.villageroptimizer.struct.enums; package me.xginko.villageroptimizer.enums;
import org.bukkit.Bukkit;
import org.bukkit.permissions.Permission; import org.bukkit.permissions.Permission;
import org.bukkit.permissions.PermissionDefault; import org.bukkit.permissions.PermissionDefault;
@ -72,24 +71,4 @@ public final class Permissions {
return permission; return permission;
} }
} }
public static void registerAll() {
for (Bypass perm : Bypass.values()) {
try {
Bukkit.getPluginManager().addPermission(perm.get());
} catch (IllegalArgumentException ignored) {}
}
for (Commands perm : Commands.values()) {
try {
Bukkit.getPluginManager().addPermission(perm.get());
} catch (IllegalArgumentException ignored) {}
}
for (Optimize perm : Optimize.values()) {
try {
Bukkit.getPluginManager().addPermission(perm.get());
} catch (IllegalArgumentException ignored) {}
}
}
} }

View File

@ -1,7 +1,7 @@
package me.xginko.villageroptimizer.events; package me.xginko.villageroptimizer.events;
import me.xginko.villageroptimizer.wrapper.WrappedVillager; import me.xginko.villageroptimizer.WrappedVillager;
import me.xginko.villageroptimizer.struct.enums.OptimizationType; import me.xginko.villageroptimizer.enums.OptimizationType;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable; import org.bukkit.event.Cancellable;
import org.bukkit.event.Event; import org.bukkit.event.Event;

View File

@ -1,7 +1,7 @@
package me.xginko.villageroptimizer.events; package me.xginko.villageroptimizer.events;
import me.xginko.villageroptimizer.wrapper.WrappedVillager; import me.xginko.villageroptimizer.WrappedVillager;
import me.xginko.villageroptimizer.struct.enums.OptimizationType; import me.xginko.villageroptimizer.enums.OptimizationType;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable; import org.bukkit.event.Cancellable;
import org.bukkit.event.Event; import org.bukkit.event.Event;

View File

@ -1,13 +1,17 @@
package me.xginko.villageroptimizer.modules; package me.xginko.villageroptimizer.modules;
import com.cryptomorin.xseries.XEntityType; import com.tcoded.folialib.impl.ServerImplementation;
import me.xginko.villageroptimizer.struct.models.ExpiringSet; import com.tcoded.folialib.wrapper.task.WrappedTask;
import me.xginko.villageroptimizer.utils.LocationUtil; import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.utils.Util; import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.wrapper.WrappedVillager; import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.utils.GenericUtil;
import net.kyori.adventure.text.Component;
import org.bukkit.Chunk; import org.bukkit.Chunk;
import org.bukkit.Server;
import org.bukkit.World; import org.bukkit.World;
import org.bukkit.entity.Entity; import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Villager; import org.bukkit.entity.Villager;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority; import org.bukkit.event.EventPriority;
@ -16,118 +20,89 @@ import org.bukkit.event.Listener;
import org.bukkit.event.entity.CreatureSpawnEvent; import org.bukkit.event.entity.CreatureSpawnEvent;
import org.bukkit.event.player.PlayerInteractEntityEvent; import org.bukkit.event.player.PlayerInteractEntityEvent;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import space.arim.morepaperlib.scheduling.ScheduledTask;
import java.time.Duration; import java.util.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream;
public class VillagerChunkLimit extends VillagerOptimizerModule implements Runnable, Listener { public class VillagerChunkLimit implements VillagerOptimizerModule, Listener {
private ScheduledTask periodic_chunk_check; private final ServerImplementation scheduler;
private final VillagerCache villagerCache;
private WrappedTask periodic_chunk_check;
private final List<Villager.Profession> non_optimized_removal_priority, optimized_removal_priority; private final List<Villager.Profession> non_optimized_removal_priority, optimized_removal_priority;
private final Set<Villager.Profession> profession_whitelist;
private final ExpiringSet<Chunk> checked_chunks;
private final long check_period; private final long check_period;
private final int non_optimized_max_per_chunk, optimized_max_per_chunk; private final int non_optimized_max_per_chunk, optimized_max_per_chunk;
private final boolean log_enabled, skip_unloaded_chunks, use_whitelist; private final boolean log_enabled, skip_unloaded_entity_chunks;
protected VillagerChunkLimit() { protected VillagerChunkLimit() {
super("villager-chunk-limit"); shouldEnable();
config.master().addComment(configPath + ".enable", this.scheduler = VillagerOptimizer.getFoliaLib().getImpl();
this.villagerCache = VillagerOptimizer.getCache();
Config config = VillagerOptimizer.getConfiguration();
config.master().addComment("villager-chunk-limit.enable",
"Checks chunks for too many villagers and removes excess villagers based on priority."); "Checks chunks for too many villagers and removes excess villagers based on priority.");
this.check_period = config.getInt(configPath + ".check-period-in-ticks", 600, this.check_period = config.getInt("villager-chunk-limit.check-period-in-ticks", 600,
"Check all loaded chunks every X ticks. 1 second = 20 ticks\n" + "Check all loaded chunks every X ticks. 1 second = 20 ticks\n" +
"A shorter delay in between checks is more efficient but is also more resource intense.\n" + "A shorter delay in between checks is more efficient but is also more resource intense.\n" +
"A larger delay is less resource intense but could become inefficient."); "A larger delay is less resource intense but could become inefficient.");
this.skip_unloaded_chunks = config.getBoolean(configPath + ".skip-not-fully-loaded-chunks", true, this.skip_unloaded_entity_chunks = config.getBoolean("villager-chunk-limit.skip-if-chunk-has-not-loaded-entities", true,
"Does not check chunks that don't have their entities loaded."); "Does not check chunks that don't have their entities loaded.");
this.checked_chunks = new ExpiringSet<>(Duration.ofSeconds( this.log_enabled = config.getBoolean("villager-chunk-limit.log-removals", true);
Math.max(1, config.getInt(configPath + ".chunk-check-cooldown-seconds", 5, this.non_optimized_max_per_chunk = config.getInt("villager-chunk-limit.unoptimized.max-per-chunk", 20,
"The delay in seconds a chunk will not be checked again after the first time.\n" +
"Reduces chances to lag the server due to overchecking."))));
this.log_enabled = config.getBoolean(configPath + ".log-removals", true);
List<String> defaults = Stream.of(
"NONE", "NITWIT", "SHEPHERD", "FISHERMAN", "BUTCHER", "CARTOGRAPHER", "LEATHERWORKER",
"FLETCHER", "MASON", "FARMER", "ARMORER", "TOOLSMITH", "WEAPONSMITH", "CLERIC", "LIBRARIAN")
.filter(profession -> {
try {
// Make sure no scary warnings appear when creating config defaults
Villager.Profession.valueOf(profession);
return true;
} catch (IllegalArgumentException e) {
return false;
}
}).collect(Collectors.toList());
this.use_whitelist = config.getBoolean(configPath + ".whitelist.enable", false,
"Enable if you only want to manage villager counts for certain profession types.");
this.profession_whitelist = config.getList(configPath + ".whitelist.professions", Arrays.asList("NONE", "NITWIT"),
"Professions in this list will not be touched by the chunk limit.")
.stream()
.map(configuredProfession -> {
try {
return Villager.Profession.valueOf(configuredProfession);
} catch (IllegalArgumentException e) {
warn("(whitelist) Villager profession '" + configuredProfession +
"' not recognized. Make sure you're using the correct profession enums from " +
"https://jd.papermc.io/paper/1.20/org/bukkit/entity/Villager.Profession.html.");
return null;
}
})
.filter(Objects::nonNull)
.collect(Collectors.toCollection(HashSet::new));
this.non_optimized_max_per_chunk = config.getInt(configPath + ".unoptimized.max-per-chunk", 20,
"The maximum amount of unoptimized villagers per chunk."); "The maximum amount of unoptimized villagers per chunk.");
this.non_optimized_removal_priority = config.getList(configPath + ".unoptimized.removal-priority", new ArrayList<>(defaults), this.non_optimized_removal_priority = config.getList("villager-chunk-limit.unoptimized.removal-priority", Arrays.asList(
"NONE", "NITWIT", "SHEPHERD", "FISHERMAN", "BUTCHER", "CARTOGRAPHER", "LEATHERWORKER",
"FLETCHER", "MASON", "FARMER", "ARMORER", "TOOLSMITH", "WEAPONSMITH", "CLERIC", "LIBRARIAN"
),
"Professions that are in the top of the list are going to be scheduled for removal first.\n" + "Professions that are in the top of the list are going to be scheduled for removal first.\n" +
"Use enums from https://jd.papermc.io/paper/1.20/org/bukkit/entity/Villager.Profession.html") "Use enums from https://jd.papermc.io/paper/1.20/org/bukkit/entity/Villager.Profession.html"
.stream() ).stream().map(configuredProfession -> {
.map(configuredProfession -> {
try { try {
return Villager.Profession.valueOf(configuredProfession); return Villager.Profession.valueOf(configuredProfession);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
warn("(unoptimized) Villager profession '" + configuredProfession + VillagerOptimizer.getLog().warn("(villager-chunk-limit.unoptimized) Villager profession '"+configuredProfession +
"' not recognized. Make sure you're using the correct profession enums from " + "' not recognized. Make sure you're using the correct profession enums from " +
"https://jd.papermc.io/paper/1.20/org/bukkit/entity/Villager.Profession.html."); "https://jd.papermc.io/paper/1.20/org/bukkit/entity/Villager.Profession.html.");
return null; return null;
} }
}) }).filter(Objects::nonNull).collect(Collectors.toList());
.filter(Objects::nonNull) this.optimized_max_per_chunk = config.getInt("villager-chunk-limit.optimized.max-per-chunk", 60,
.collect(Collectors.toList());
this.optimized_max_per_chunk = config.getInt(configPath + ".optimized.max-per-chunk", 60,
"The maximum amount of optimized villagers per chunk."); "The maximum amount of optimized villagers per chunk.");
this.optimized_removal_priority = config.getList(configPath + ".optimized.removal-priority", new ArrayList<>(defaults)) this.optimized_removal_priority = config.getList("villager-chunk-limit.optimized.removal-priority", Arrays.asList(
.stream() "NONE", "NITWIT", "SHEPHERD", "FISHERMAN", "BUTCHER", "CARTOGRAPHER", "LEATHERWORKER",
.map(configuredProfession -> { "FLETCHER", "MASON", "FARMER", "ARMORER", "TOOLSMITH", "WEAPONSMITH", "CLERIC", "LIBRARIAN"
)).stream().map(configuredProfession -> {
try { try {
return Villager.Profession.valueOf(configuredProfession); return Villager.Profession.valueOf(configuredProfession);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
warn("(optimized) Villager profession '" + configuredProfession + "' not recognized. " + VillagerOptimizer.getLog().warn("(villager-chunk-limit.optimized) Villager profession '"+configuredProfession +
"Make sure you're using the correct profession enums from " + "' not recognized. Make sure you're using the correct profession enums from " +
"https://jd.papermc.io/paper/1.20/org/bukkit/entity/Villager.Profession.html."); "https://jd.papermc.io/paper/1.20/org/bukkit/entity/Villager.Profession.html.");
return null; return null;
} }
}) }).filter(Objects::nonNull).collect(Collectors.toList());
.filter(Objects::nonNull)
.collect(Collectors.toList());
} }
@Override @Override
public void enable() { public void enable() {
plugin.getServer().getPluginManager().registerEvents(this, plugin); final VillagerOptimizer plugin = VillagerOptimizer.getInstance();
periodic_chunk_check = scheduling.globalRegionalScheduler().runAtFixedRate(this, check_period, check_period); final Server server = plugin.getServer();
server.getPluginManager().registerEvents(this, plugin);
this.periodic_chunk_check = scheduler.runTimer(() -> {
for (World world : server.getWorlds()) {
for (Chunk chunk : world.getLoadedChunks()) {
if (!skip_unloaded_entity_chunks || GenericUtil.isEntitiesLoaded(chunk)) {
this.manageVillagerCount(chunk);
}
}
}
}, check_period, check_period);
} }
@Override @Override
public boolean shouldEnable() { public boolean shouldEnable() {
return config.getBoolean(configPath + ".enable", false); return VillagerOptimizer.getConfiguration().getBoolean("villager-chunk-limit.enable", false);
} }
@Override @Override
@ -136,60 +111,35 @@ public class VillagerChunkLimit extends VillagerOptimizerModule implements Runna
if (periodic_chunk_check != null) periodic_chunk_check.cancel(); if (periodic_chunk_check != null) periodic_chunk_check.cancel();
} }
@Override
public void run() {
for (World world : plugin.getServer().getWorlds()) {
for (Chunk chunk : world.getLoadedChunks()) {
scheduling.regionSpecificScheduler(chunk.getWorld(), chunk.getX(), chunk.getZ()).run(() -> {
if (!skip_unloaded_chunks || Util.isChunkLoaded(chunk)) {
manageVillagerCount(chunk);
}
});
}
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onCreatureSpawn(CreatureSpawnEvent event) { private void onCreatureSpawn(CreatureSpawnEvent event) {
if (event.getEntityType() == XEntityType.VILLAGER.get()) { if (event.getEntityType() == EntityType.VILLAGER) {
scheduling.regionSpecificScheduler(event.getLocation()).run(() -> { this.manageVillagerCount(event.getEntity().getChunk());
manageVillagerCount(event.getEntity().getChunk());
});
} }
} }
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true) @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
private void onInteract(PlayerInteractEntityEvent event) { private void onInteract(PlayerInteractEntityEvent event) {
if (event.getRightClicked().getType() == XEntityType.VILLAGER.get()) { if (event.getRightClicked().getType() == EntityType.VILLAGER) {
scheduling.regionSpecificScheduler(event.getRightClicked().getLocation()).run(() -> { this.manageVillagerCount(event.getRightClicked().getChunk());
manageVillagerCount(event.getRightClicked().getChunk());
});
} }
} }
private void manageVillagerCount(@NotNull Chunk chunk) { private void manageVillagerCount(@NotNull Chunk chunk) {
// Remember which chunk we have already checked
if (checked_chunks.contains(chunk)) return;
else checked_chunks.add(chunk);
// Collect all optimized and unoptimized villagers in that chunk // Collect all optimized and unoptimized villagers in that chunk
List<Villager> optimized_villagers = new ArrayList<>(); List<Villager> optimized_villagers = new ArrayList<>();
List<Villager> not_optimized_villagers = new ArrayList<>(); List<Villager> not_optimized_villagers = new ArrayList<>();
for (Entity entity : chunk.getEntities()) { for (Entity entity : chunk.getEntities()) {
if (entity.getType() != XEntityType.VILLAGER.get()) continue; if (entity.getType().equals(EntityType.VILLAGER)) {
Villager villager = (Villager) entity; Villager villager = (Villager) entity;
if (villagerCache.getOrAdd(villager).isOptimized()) {
// Ignore villager if profession is not in the whitelist
if (use_whitelist && profession_whitelist.contains(villager.getProfession())) continue;
if (wrapperCache.get(villager, WrappedVillager::new).isOptimized()) {
optimized_villagers.add(villager); optimized_villagers.add(villager);
} else { } else {
not_optimized_villagers.add(villager); not_optimized_villagers.add(villager);
} }
} }
}
// Check if there are more unoptimized villagers in that chunk than allowed // Check if there are more unoptimized villagers in that chunk than allowed
final int not_optimized_villagers_too_many = not_optimized_villagers.size() - non_optimized_max_per_chunk; final int not_optimized_villagers_too_many = not_optimized_villagers.size() - non_optimized_max_per_chunk;
@ -202,11 +152,14 @@ public class VillagerChunkLimit extends VillagerOptimizerModule implements Runna
// Remove prioritized villagers that are too many // Remove prioritized villagers that are too many
for (int i = 0; i < not_optimized_villagers_too_many; i++) { for (int i = 0; i < not_optimized_villagers_too_many; i++) {
Villager villager = not_optimized_villagers.get(i); Villager villager = not_optimized_villagers.get(i);
scheduling.entitySpecificScheduler(villager).run(kill -> { scheduler.runAtEntity(villager, kill -> {
villager.remove(); villager.remove();
if (log_enabled) info("Removed unoptimized villager with profession '" + if (log_enabled) {
Util.toNiceString(villager.getProfession()) + "' at " + LocationUtil.toString(villager.getLocation())); VillagerOptimizer.getLog().info(Component.text(
}, null); "Removed unoptimized villager with profession '" + villager.getProfession().name() + "' at " +
GenericUtil.formatLocation(villager.getLocation())).color(GenericUtil.COLOR));
}
});
} }
} }
@ -221,11 +174,15 @@ public class VillagerChunkLimit extends VillagerOptimizerModule implements Runna
// Remove prioritized villagers that are too many // Remove prioritized villagers that are too many
for (int i = 0; i < optimized_villagers_too_many; i++) { for (int i = 0; i < optimized_villagers_too_many; i++) {
Villager villager = optimized_villagers.get(i); Villager villager = optimized_villagers.get(i);
scheduling.entitySpecificScheduler(villager).run(kill -> { scheduler.runAtEntity(villager, kill -> {
villager.remove(); villager.remove();
if (log_enabled) info("Removed unoptimized villager with profession '" +
Util.toNiceString(villager.getProfession()) + "' at " + LocationUtil.toString(villager.getLocation())); if (log_enabled) {
}, null); VillagerOptimizer.getLog().info(Component.text("Removed optimized villager with profession '" +
villager.getProfession().name() + "' at " +
GenericUtil.formatLocation(villager.getLocation())).color(GenericUtil.COLOR));
}
});
} }
} }
} }

View File

@ -1,87 +1,43 @@
package me.xginko.villageroptimizer.modules; package me.xginko.villageroptimizer.modules;
import com.github.benmanes.caffeine.cache.Cache; import me.xginko.villageroptimizer.modules.gameplay.*;
import me.xginko.villageroptimizer.VillagerOptimizer; import me.xginko.villageroptimizer.modules.optimization.OptimizeByBlock;
import me.xginko.villageroptimizer.config.Config; import me.xginko.villageroptimizer.modules.optimization.OptimizeByNametag;
import me.xginko.villageroptimizer.struct.Disableable; import me.xginko.villageroptimizer.modules.optimization.OptimizeByWorkstation;
import me.xginko.villageroptimizer.struct.Enableable;
import me.xginko.villageroptimizer.wrapper.WrappedVillager;
import org.bukkit.entity.Villager;
import org.reflections.Reflections;
import org.reflections.scanners.Scanners;
import space.arim.morepaperlib.scheduling.GracefulScheduling;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set;
public abstract class VillagerOptimizerModule implements Enableable, Disableable { public interface VillagerOptimizerModule {
private static final Reflections MODULES_PACKAGE = new Reflections(VillagerOptimizerModule.class.getPackage().getName()); void enable();
public static final Set<VillagerOptimizerModule> ENABLED_MODULES = new HashSet<>(); void disable();
boolean shouldEnable();
public abstract boolean shouldEnable(); HashSet<VillagerOptimizerModule> modules = new HashSet<>();
protected final VillagerOptimizer plugin; static void reloadModules() {
protected final Config config; modules.forEach(VillagerOptimizerModule::disable);
protected final Cache<Villager, WrappedVillager> wrapperCache; modules.clear();
protected final GracefulScheduling scheduling;
public final String configPath;
private final String logFormat;
public VillagerOptimizerModule(String configPath) { modules.add(new OptimizeByNametag());
this.plugin = VillagerOptimizer.getInstance(); modules.add(new OptimizeByBlock());
this.config = VillagerOptimizer.config(); modules.add(new OptimizeByWorkstation());
this.wrapperCache = VillagerOptimizer.wrappers();
this.scheduling = VillagerOptimizer.scheduling(); modules.add(new EnableLeashingVillagers());
this.configPath = configPath; modules.add(new FixOptimisationAfterCure());
shouldEnable(); // Ensure enable option is always first modules.add(new RestockOptimizedTrades());
String[] paths = configPath.split("\\."); modules.add(new LevelOptimizedProfession());
if (paths.length <= 2) { modules.add(new VisuallyHighlightOptimized());
this.logFormat = "<" + configPath + "> {}"; modules.add(new MakeVillagersSpawnAdult());
} else { modules.add(new PreventUnoptimizedTrading());
this.logFormat = "<" + paths[paths.length - 2] + "." + paths[paths.length - 1] + "> {}"; modules.add(new PreventOptimizedTargeting());
} modules.add(new PreventOptimizedDamage());
} modules.add(new UnoptimizeOnJobLoose());
public static void reloadModules() { modules.add(new VillagerChunkLimit());
ENABLED_MODULES.forEach(VillagerOptimizerModule::disable);
ENABLED_MODULES.clear(); modules.forEach(module -> {
if (module.shouldEnable()) module.enable();
for (Class<?> clazz : MODULES_PACKAGE.get(Scanners.SubTypes.of(VillagerOptimizerModule.class).asClass())) { });
if (clazz.isInterface() || Modifier.isAbstract(clazz.getModifiers())) continue;
try {
VillagerOptimizerModule module = (VillagerOptimizerModule) clazz.getDeclaredConstructor().newInstance();
if (module.shouldEnable()) {
ENABLED_MODULES.add(module);
}
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
VillagerOptimizer.logger().error("Failed initialising module class '{}'.", clazz.getSimpleName(), e);
}
}
ENABLED_MODULES.forEach(VillagerOptimizerModule::enable);
}
protected void error(String message, Throwable throwable) {
VillagerOptimizer.logger().error(logFormat, message, throwable);
}
protected void error(String message) {
VillagerOptimizer.logger().error(logFormat, message);
}
protected void warn(String message) {
VillagerOptimizer.logger().warn(logFormat, message);
}
protected void info(String message) {
VillagerOptimizer.logger().info(logFormat, message);
}
protected void notRecognized(Class<?> clazz, String unrecognized) {
warn("Unable to parse " + clazz.getSimpleName() + " at '" + unrecognized + "'. Please check your configuration.");
} }
} }

View File

@ -1,11 +1,15 @@
package me.xginko.villageroptimizer.modules.gameplay; package me.xginko.villageroptimizer.modules.gameplay;
import com.cryptomorin.xseries.XEntityType; import com.tcoded.folialib.impl.ServerImplementation;
import com.cryptomorin.xseries.XMaterial; import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule; import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import me.xginko.villageroptimizer.utils.LocationUtil; import me.xginko.villageroptimizer.utils.GenericUtil;
import me.xginko.villageroptimizer.wrapper.WrappedVillager; import net.kyori.adventure.text.Component;
import org.bukkit.GameMode; import org.bukkit.GameMode;
import org.bukkit.Material;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.entity.Villager; import org.bukkit.entity.Villager;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
@ -16,21 +20,27 @@ import org.bukkit.event.entity.PlayerLeashEntityEvent;
import org.bukkit.event.player.PlayerInteractEntityEvent; import org.bukkit.event.player.PlayerInteractEntityEvent;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
public class EnableLeashingVillagers extends VillagerOptimizerModule implements Listener { public class EnableLeashingVillagers implements VillagerOptimizerModule, Listener {
private final ServerImplementation scheduler;
private final VillagerCache villagerCache;
private final boolean only_optimized, log_enabled; private final boolean only_optimized, log_enabled;
public EnableLeashingVillagers() { public EnableLeashingVillagers() {
super("gameplay.villagers-can-be-leashed"); shouldEnable();
config.master().addComment(configPath + ".enable", this.scheduler = VillagerOptimizer.getFoliaLib().getImpl();
this.villagerCache = VillagerOptimizer.getCache();
Config config = VillagerOptimizer.getConfiguration();
config.master().addComment("gameplay.villagers-can-be-leashed.enable",
"Enable leashing of villagers, enabling players to easily move villagers to where they want them to be."); "Enable leashing of villagers, enabling players to easily move villagers to where they want them to be.");
this.only_optimized = config.getBoolean(configPath + ".only-optimized", false, this.only_optimized = config.getBoolean("gameplay.villagers-can-be-leashed.only-optimized", false,
"If set to true, only optimized villagers can be leashed."); "If set to true, only optimized villagers can be leashed.");
this.log_enabled = config.getBoolean(configPath + ".log", false); this.log_enabled = config.getBoolean("gameplay.villagers-can-be-leashed.log", false);
} }
@Override @Override
public void enable() { public void enable() {
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
plugin.getServer().getPluginManager().registerEvents(this, plugin); plugin.getServer().getPluginManager().registerEvents(this, plugin);
} }
@ -41,20 +51,20 @@ public class EnableLeashingVillagers extends VillagerOptimizerModule implements
@Override @Override
public boolean shouldEnable() { public boolean shouldEnable() {
return config.getBoolean(configPath + ".enable", false); return VillagerOptimizer.getConfiguration().getBoolean("gameplay.villagers-can-be-leashed.enable", false);
} }
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onLeash(PlayerInteractEntityEvent event) { private void onLeash(PlayerInteractEntityEvent event) {
if (event.getRightClicked().getType() != XEntityType.VILLAGER.get()) return; if (!event.getRightClicked().getType().equals(EntityType.VILLAGER)) return;
final Player player = event.getPlayer(); final Player player = event.getPlayer();
final ItemStack handItem = player.getInventory().getItem(event.getHand()); final ItemStack handItem = player.getInventory().getItem(event.getHand());
if (handItem == null || handItem.getType() != XMaterial.LEAD.parseMaterial()) return; if (handItem == null || !handItem.getType().equals(Material.LEAD)) return;
final Villager villager = (Villager) event.getRightClicked(); final Villager villager = (Villager) event.getRightClicked();
if (villager.isLeashed()) return; if (villager.isLeashed()) return;
if (only_optimized && !wrapperCache.get(villager, WrappedVillager::new).isOptimized()) return; if (only_optimized && !villagerCache.getOrAdd(villager).isOptimized()) return;
event.setCancelled(true); // Cancel the event, so we don't interact with the villager event.setCancelled(true); // Cancel the event, so we don't interact with the villager
@ -69,15 +79,16 @@ public class EnableLeashingVillagers extends VillagerOptimizerModule implements
// If canceled by any plugin, do nothing // If canceled by any plugin, do nothing
if (!leashEvent.callEvent()) return; if (!leashEvent.callEvent()) return;
scheduling.entitySpecificScheduler(villager).run(leash -> { scheduler.runAtEntity(villager, leash -> {
// Legitimate to not use entities from the event object since they are final in PlayerLeashEntityEvent // Legitimate to not use entities from the event object since they are final in PlayerLeashEntityEvent
if (!villager.setLeashHolder(player)) return; if (!villager.setLeashHolder(player)) return;
if (player.getGameMode().equals(GameMode.SURVIVAL)) if (player.getGameMode().equals(GameMode.SURVIVAL))
handItem.subtract(1); // Manually consume for survival players handItem.subtract(1); // Manually consume for survival players
if (log_enabled) { if (log_enabled) {
info(player.getName() + " leashed a villager at " + LocationUtil.toString(villager.getLocation())); VillagerOptimizer.getLog().info(Component.text(player.getName() + " leashed a villager at " +
GenericUtil.formatLocation(villager.getLocation())).color(GenericUtil.COLOR));
} }
}, null); });
} }
} }

View File

@ -1,8 +1,9 @@
package me.xginko.villageroptimizer.modules.gameplay; package me.xginko.villageroptimizer.modules.gameplay;
import com.cryptomorin.xseries.XEntityType; import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.WrappedVillager;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule; import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import me.xginko.villageroptimizer.wrapper.WrappedVillager; import org.bukkit.entity.EntityType;
import org.bukkit.entity.Villager; import org.bukkit.entity.Villager;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority; import org.bukkit.event.EventPriority;
@ -10,14 +11,15 @@ import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityTransformEvent; import org.bukkit.event.entity.EntityTransformEvent;
public class FixOptimisationAfterCure extends VillagerOptimizerModule implements Listener { import java.util.concurrent.TimeUnit;
public FixOptimisationAfterCure() { public class FixOptimisationAfterCure implements VillagerOptimizerModule, Listener {
super("post-cure-optimization-fix");
} public FixOptimisationAfterCure() {}
@Override @Override
public void enable() { public void enable() {
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
plugin.getServer().getPluginManager().registerEvents(this, plugin); plugin.getServer().getPluginManager().registerEvents(this, plugin);
} }
@ -34,14 +36,14 @@ public class FixOptimisationAfterCure extends VillagerOptimizerModule implements
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onTransform(EntityTransformEvent event) { private void onTransform(EntityTransformEvent event) {
if ( if (
event.getTransformReason() == EntityTransformEvent.TransformReason.CURED event.getTransformReason().equals(EntityTransformEvent.TransformReason.CURED)
&& event.getTransformedEntity().getType() == XEntityType.VILLAGER.get() && event.getTransformedEntity().getType().equals(EntityType.VILLAGER)
) { ) {
Villager villager = (Villager) event.getTransformedEntity(); Villager villager = (Villager) event.getTransformedEntity();
scheduling.entitySpecificScheduler(villager).runDelayed(() -> { VillagerOptimizer.getFoliaLib().getImpl().runAtEntityLater(villager, () -> {
WrappedVillager wVillager = wrapperCache.get(villager, WrappedVillager::new); WrappedVillager wVillager = VillagerOptimizer.getCache().getOrAdd(villager);
wVillager.setOptimizationType(wVillager.getOptimizationType()); wVillager.setOptimizationType(wVillager.getOptimizationType());
}, null, 40L); }, 2, TimeUnit.SECONDS);
} }
} }
} }

View File

@ -1,12 +1,13 @@
package me.xginko.villageroptimizer.modules.gameplay; package me.xginko.villageroptimizer.modules.gameplay;
import com.cryptomorin.xseries.XPotion; import com.tcoded.folialib.impl.ServerImplementation;
import me.xginko.villageroptimizer.VillagerOptimizer; import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.config.Config; import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.WrappedVillager;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule; import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import me.xginko.villageroptimizer.utils.GenericUtil;
import me.xginko.villageroptimizer.utils.KyoriUtil; import me.xginko.villageroptimizer.utils.KyoriUtil;
import me.xginko.villageroptimizer.utils.Util;
import me.xginko.villageroptimizer.wrapper.WrappedVillager;
import net.kyori.adventure.text.TextReplacementConfig; import net.kyori.adventure.text.TextReplacementConfig;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.entity.Villager; import org.bukkit.entity.Villager;
@ -17,34 +18,37 @@ import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryCloseEvent; import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.event.inventory.InventoryType; import org.bukkit.event.inventory.InventoryType;
import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
import java.time.Duration; import java.time.Duration;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
public class LevelOptimizedProfession extends VillagerOptimizerModule implements Listener { public class LevelOptimizedProfession implements VillagerOptimizerModule, Listener {
private static final PotionEffect SUPER_SLOWNESS = new PotionEffect(
XPotion.SLOWNESS.getPotionEffectType(), 120, 120, false, false);
private final ServerImplementation scheduler;
private final VillagerCache villagerCache;
private final boolean notify_player; private final boolean notify_player;
private final long cooldown_millis; private final long cooldown_millis;
public LevelOptimizedProfession() { public LevelOptimizedProfession() {
super("gameplay.level-optimized-profession"); shouldEnable();
Config config = VillagerOptimizer.config(); this.scheduler = VillagerOptimizer.getFoliaLib().getImpl();
config.master().addComment(configPath, this.villagerCache = VillagerOptimizer.getCache();
Config config = VillagerOptimizer.getConfiguration();
config.master().addComment("gameplay.level-optimized-profession",
"This is needed to allow optimized villagers to level up.\n" + "This is needed to allow optimized villagers to level up.\n" +
"Temporarily enables the villagers AI to allow it to level up and then disables it again."); "Temporarily enables the villagers AI to allow it to level up and then disables it again.");
this.cooldown_millis = TimeUnit.SECONDS.toMillis( this.cooldown_millis = TimeUnit.SECONDS.toMillis(
config.getInt(configPath + ".level-check-cooldown-seconds", 5, config.getInt("gameplay.level-optimized-profession.level-check-cooldown-seconds", 5,
"Cooldown in seconds until the level of a villager will be checked and updated again.\n" + "Cooldown in seconds until the level of a villager will be checked and updated again.\n" +
"Recommended to leave as is.")); "Recommended to leave as is."));
this.notify_player = config.getBoolean(configPath + ".notify-player", true, this.notify_player = config.getBoolean("gameplay.level-optimized-profession.notify-player", true,
"Tell players to wait when a villager is leveling up."); "Tell players to wait when a villager is leveling up.");
} }
@Override @Override
public void enable() { public void enable() {
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
plugin.getServer().getPluginManager().registerEvents(this, plugin); plugin.getServer().getPluginManager().registerEvents(this, plugin);
} }
@ -61,30 +65,30 @@ public class LevelOptimizedProfession extends VillagerOptimizerModule implements
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onTradeScreenClose(InventoryCloseEvent event) { private void onTradeScreenClose(InventoryCloseEvent event) {
if ( if (
event.getInventory().getType() == InventoryType.MERCHANT event.getInventory().getType().equals(InventoryType.MERCHANT)
&& event.getInventory().getHolder() instanceof Villager && event.getInventory().getHolder() instanceof Villager
) { ) {
final Villager villager = (Villager) event.getInventory().getHolder(); final Villager villager = (Villager) event.getInventory().getHolder();
final WrappedVillager wVillager = wrapperCache.get(villager, WrappedVillager::new); final WrappedVillager wVillager = villagerCache.getOrAdd(villager);
if (!wVillager.isOptimized()) return; if (!wVillager.isOptimized()) return;
if (wVillager.canLevelUp(cooldown_millis)) { if (wVillager.canLevelUp(cooldown_millis)) {
if (wVillager.calculateLevel() <= villager.getVillagerLevel()) return; if (wVillager.calculateLevel() <= villager.getVillagerLevel()) return;
scheduling.entitySpecificScheduler(villager).run(enableAI -> { scheduler.runAtEntity(villager, enableAI -> {
villager.addPotionEffect(SUPER_SLOWNESS); villager.addPotionEffect(new PotionEffect(PotionEffectType.SLOW, 120, 120, false, false));
villager.setAware(true); villager.setAware(true);
scheduling.entitySpecificScheduler(villager).runDelayed(disableAI -> { scheduler.runAtEntityLater(villager, disableAI -> {
villager.setAware(false); villager.setAware(false);
wVillager.saveLastLevelUp(); wVillager.saveLastLevelUp();
}, null, 100L); }, 5, TimeUnit.SECONDS);
}, null); });
} else { } else {
if (notify_player) { if (notify_player) {
Player player = (Player) event.getPlayer(); Player player = (Player) event.getPlayer();
final TextReplacementConfig timeLeft = TextReplacementConfig.builder() final TextReplacementConfig timeLeft = TextReplacementConfig.builder()
.matchLiteral("%time%") .matchLiteral("%time%")
.replacement(Util.formatDuration(Duration.ofMillis(wVillager.getLevelCooldownMillis(cooldown_millis)))) .replacement(GenericUtil.formatDuration(Duration.ofMillis(wVillager.getLevelCooldownMillis(cooldown_millis))))
.build(); .build();
VillagerOptimizer.getLang(player.locale()).villager_leveling_up VillagerOptimizer.getLang(player.locale()).villager_leveling_up
.forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(timeLeft))); .forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(timeLeft)));

View File

@ -1,7 +1,8 @@
package me.xginko.villageroptimizer.modules.gameplay; package me.xginko.villageroptimizer.modules.gameplay;
import com.cryptomorin.xseries.XEntityType; import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule; import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Villager; import org.bukkit.entity.Villager;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority; import org.bukkit.event.EventPriority;
@ -9,19 +10,13 @@ import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.event.entity.CreatureSpawnEvent; import org.bukkit.event.entity.CreatureSpawnEvent;
public class MakeVillagersSpawnAdult extends VillagerOptimizerModule implements Listener { public class MakeVillagersSpawnAdult implements VillagerOptimizerModule, Listener {
public MakeVillagersSpawnAdult() { public MakeVillagersSpawnAdult() {}
super("gameplay.villagers-spawn-as-adults");
config.master().addComment(configPath + ".enable",
"Spawned villagers will immediately be adults.\n" +
"This is to save some more resources as players don't have to keep unoptimized\n" +
"villagers loaded because they have to wait for them to turn into adults before they can\n" +
"optimize them.");
}
@Override @Override
public void enable() { public void enable() {
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
plugin.getServer().getPluginManager().registerEvents(this, plugin); plugin.getServer().getPluginManager().registerEvents(this, plugin);
} }
@ -32,12 +27,16 @@ public class MakeVillagersSpawnAdult extends VillagerOptimizerModule implements
@Override @Override
public boolean shouldEnable() { public boolean shouldEnable() {
return config.getBoolean(configPath + ".enable", false); return VillagerOptimizer.getConfiguration().getBoolean("gameplay.villagers-spawn-as-adults.enable", false,
"Spawned villagers will immediately be adults.\n" +
"This is to save some more resources as players don't have to keep unoptimized\n" +
"villagers loaded because they have to wait for them to turn into adults before they can\n" +
"optimize them.");
} }
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onVillagerSpawn(CreatureSpawnEvent event) { private void onVillagerSpawn(CreatureSpawnEvent event) {
if (event.getEntityType() == XEntityType.VILLAGER.get()) { if (event.getEntityType().equals(EntityType.VILLAGER)) {
final Villager villager = (Villager) event.getEntity(); final Villager villager = (Villager) event.getEntity();
if (!villager.isAdult()) villager.setAdult(); if (!villager.isAdult()) villager.setAdult();
} }

View File

@ -1,8 +1,11 @@
package me.xginko.villageroptimizer.modules.gameplay; package me.xginko.villageroptimizer.modules.gameplay;
import com.cryptomorin.xseries.XEntityType; import com.destroystokyo.paper.event.entity.EntityKnockbackByEntityEvent;
import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule; import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import me.xginko.villageroptimizer.wrapper.WrappedVillager; import org.bukkit.entity.EntityType;
import org.bukkit.entity.Villager; import org.bukkit.entity.Villager;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority; import org.bukkit.event.EventPriority;
@ -11,43 +14,45 @@ import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageEvent; import org.bukkit.event.entity.EntityDamageEvent;
import java.util.Arrays; import java.util.Arrays;
import java.util.EnumSet; import java.util.HashSet;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class PreventOptimizedDamage extends VillagerOptimizerModule implements Listener { public class PreventOptimizedDamage implements VillagerOptimizerModule, Listener {
private final VillagerCache villagerCache;
private final Set<EntityDamageEvent.DamageCause> damage_causes_to_cancel; private final Set<EntityDamageEvent.DamageCause> damage_causes_to_cancel;
private final boolean cancel_knockback; private final boolean cancel_knockback;
public PreventOptimizedDamage() { public PreventOptimizedDamage() {
super("gameplay.prevent-damage-to-optimized"); shouldEnable();
config.master().addComment(configPath + ".enable", this.villagerCache = VillagerOptimizer.getCache();
Config config = VillagerOptimizer.getConfiguration();
config.master().addComment("gameplay.prevent-damage-to-optimized.enable",
"Configure what kind of damage you want to cancel for optimized villagers here."); "Configure what kind of damage you want to cancel for optimized villagers here.");
this.cancel_knockback = config.getBoolean(configPath + ".prevent-knockback-from-entity", true, this.cancel_knockback = config.getBoolean("gameplay.prevent-damage-to-optimized.prevent-knockback-from-entity", true,
"Prevents optimized villagers from getting knocked back by an attacking entity"); "Prevents optimized villagers from getting knocked back by an attacking entity");
this.damage_causes_to_cancel = config.getList(configPath + ".damage-causes-to-cancel", this.damage_causes_to_cancel = config.getList("gameplay.prevent-damage-to-optimized.damage-causes-to-cancel",
Arrays.stream(EntityDamageEvent.DamageCause.values()).map(Enum::name).sorted().collect(Collectors.toList()), Arrays.stream(EntityDamageEvent.DamageCause.values()).map(Enum::name).sorted().collect(Collectors.toList()),
"These are all current entries in the game. Remove what you do not need blocked.\n" + "These are all current entries in the game. Remove what you do not need blocked.\n" +
"If you want a description or need to add a previously removed type, refer to:\n" + "If you want a description or need to add a previously removed type, refer to:\n" +
"https://jd.papermc.io/paper/1.20/org/bukkit/event/entity/EntityDamageEvent.DamageCause.html") "https://jd.papermc.io/paper/1.20/org/bukkit/event/entity/EntityDamageEvent.DamageCause.html"
.stream() ).stream().map(configuredDamageCause -> {
.map(configuredDamageCause -> {
try { try {
return EntityDamageEvent.DamageCause.valueOf(configuredDamageCause); return EntityDamageEvent.DamageCause.valueOf(configuredDamageCause);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
warn("DamageCause '" + configuredDamageCause + "' not recognized. Please use correct DamageCause enums from: " + VillagerOptimizer.getLog().warn("(prevent-damage-to-optimized) DamageCause '"+configuredDamageCause +
"' not recognized. Please use correct DamageCause enums from: " +
"https://jd.papermc.io/paper/1.20/org/bukkit/event/entity/EntityDamageEvent.DamageCause.html"); "https://jd.papermc.io/paper/1.20/org/bukkit/event/entity/EntityDamageEvent.DamageCause.html");
return null; return null;
} }
}) }).filter(Objects::nonNull).collect(Collectors.toCollection(HashSet::new));
.filter(Objects::nonNull)
.collect(Collectors.toCollection(() -> EnumSet.noneOf(EntityDamageEvent.DamageCause.class)));
} }
@Override @Override
public void enable() { public void enable() {
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
plugin.getServer().getPluginManager().registerEvents(this, plugin); plugin.getServer().getPluginManager().registerEvents(this, plugin);
} }
@ -58,26 +63,26 @@ public class PreventOptimizedDamage extends VillagerOptimizerModule implements L
@Override @Override
public boolean shouldEnable() { public boolean shouldEnable() {
return config.getBoolean(configPath + ".enable", true); return VillagerOptimizer.getConfiguration().getBoolean("gameplay.prevent-damage-to-optimized.enable", true);
} }
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onDamageByEntity(EntityDamageEvent event) { private void onDamageByEntity(EntityDamageEvent event) {
if ( if (
event.getEntityType() == XEntityType.VILLAGER.get() event.getEntityType().equals(EntityType.VILLAGER)
&& damage_causes_to_cancel.contains(event.getCause()) && damage_causes_to_cancel.contains(event.getCause())
&& wrapperCache.get((Villager) event.getEntity(), WrappedVillager::new).isOptimized() && villagerCache.getOrAdd((Villager) event.getEntity()).isOptimized()
) { ) {
event.setCancelled(true); event.setCancelled(true);
} }
} }
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onKnockbackByEntity(com.destroystokyo.paper.event.entity.EntityKnockbackByEntityEvent event) { private void onKnockbackByEntity(EntityKnockbackByEntityEvent event) {
if ( if (
cancel_knockback cancel_knockback
&& event.getEntityType() == XEntityType.VILLAGER.get() && event.getEntityType().equals(EntityType.VILLAGER)
&& wrapperCache.get((Villager) event.getEntity(), WrappedVillager::new).isOptimized() && villagerCache.getOrAdd((Villager) event.getEntity()).isOptimized()
) { ) {
event.setCancelled(true); event.setCancelled(true);
} }

View File

@ -1,9 +1,11 @@
package me.xginko.villageroptimizer.modules.gameplay; package me.xginko.villageroptimizer.modules.gameplay;
import com.cryptomorin.xseries.XEntityType; import com.destroystokyo.paper.event.entity.EntityPathfindEvent;
import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule; import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import me.xginko.villageroptimizer.wrapper.WrappedVillager;
import org.bukkit.entity.Entity; import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Mob; import org.bukkit.entity.Mob;
import org.bukkit.entity.Villager; import org.bukkit.entity.Villager;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
@ -13,16 +15,17 @@ import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageByEntityEvent; import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.entity.EntityTargetEvent; import org.bukkit.event.entity.EntityTargetEvent;
public class PreventOptimizedTargeting extends VillagerOptimizerModule implements Listener { public class PreventOptimizedTargeting implements VillagerOptimizerModule, Listener {
private final VillagerCache villagerCache;
public PreventOptimizedTargeting() { public PreventOptimizedTargeting() {
super("gameplay.prevent-entities-from-targeting-optimized"); this.villagerCache = VillagerOptimizer.getCache();
config.master().addComment(configPath + ".enable",
"Prevents hostile entities from targeting optimized villagers.");
} }
@Override @Override
public void enable() { public void enable() {
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
plugin.getServer().getPluginManager().registerEvents(this, plugin); plugin.getServer().getPluginManager().registerEvents(this, plugin);
} }
@ -33,7 +36,8 @@ public class PreventOptimizedTargeting extends VillagerOptimizerModule implement
@Override @Override
public boolean shouldEnable() { public boolean shouldEnable() {
return config.getBoolean(configPath + ".enable", true); return VillagerOptimizer.getConfiguration().getBoolean("gameplay.prevent-entities-from-targeting-optimized.enable", true,
"Prevents hostile entities from targeting optimized villagers.");
} }
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
@ -41,8 +45,8 @@ public class PreventOptimizedTargeting extends VillagerOptimizerModule implement
final Entity target = event.getTarget(); final Entity target = event.getTarget();
if ( if (
target != null target != null
&& target.getType() == XEntityType.VILLAGER.get() && target.getType().equals(EntityType.VILLAGER)
&& wrapperCache.get((Villager) target, WrappedVillager::new).isOptimized() && villagerCache.getOrAdd((Villager) target).isOptimized()
) { ) {
event.setTarget(null); event.setTarget(null);
event.setCancelled(true); event.setCancelled(true);
@ -50,12 +54,12 @@ public class PreventOptimizedTargeting extends VillagerOptimizerModule implement
} }
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onEntityTargetVillager(com.destroystokyo.paper.event.entity.EntityPathfindEvent event) { private void onEntityTargetVillager(EntityPathfindEvent event) {
final Entity target = event.getTargetEntity(); final Entity target = event.getTargetEntity();
if ( if (
target != null target != null
&& target.getType() == XEntityType.VILLAGER.get() && target.getType().equals(EntityType.VILLAGER)
&& wrapperCache.get((Villager) target, WrappedVillager::new).isOptimized() && villagerCache.getOrAdd((Villager) target).isOptimized()
) { ) {
event.setCancelled(true); event.setCancelled(true);
} }
@ -64,9 +68,9 @@ public class PreventOptimizedTargeting extends VillagerOptimizerModule implement
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onEntityAttackVillager(EntityDamageByEntityEvent event) { private void onEntityAttackVillager(EntityDamageByEntityEvent event) {
if ( if (
event.getEntityType() == XEntityType.VILLAGER.get() event.getEntityType().equals(EntityType.VILLAGER)
&& event.getDamager() instanceof Mob && event.getDamager() instanceof Mob
&& wrapperCache.get((Villager) event.getEntity(), WrappedVillager::new).isOptimized() && villagerCache.getOrAdd((Villager) event.getEntity()).isOptimized()
) { ) {
((Mob) event.getDamager()).setTarget(null); ((Mob) event.getDamager()).setTarget(null);
} }

View File

@ -1,10 +1,11 @@
package me.xginko.villageroptimizer.modules.gameplay; package me.xginko.villageroptimizer.modules.gameplay;
import me.xginko.villageroptimizer.VillagerOptimizer; import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.struct.enums.Permissions; import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.enums.Permissions;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule; import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import me.xginko.villageroptimizer.utils.KyoriUtil; import me.xginko.villageroptimizer.utils.KyoriUtil;
import me.xginko.villageroptimizer.wrapper.WrappedVillager;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.entity.Villager; import org.bukkit.entity.Villager;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
@ -15,22 +16,26 @@ import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryType; import org.bukkit.event.inventory.InventoryType;
import org.bukkit.event.inventory.TradeSelectEvent; import org.bukkit.event.inventory.TradeSelectEvent;
public class PreventUnoptimizedTrading extends VillagerOptimizerModule implements Listener { public class PreventUnoptimizedTrading implements VillagerOptimizerModule, Listener {
private final VillagerCache villagerCache;
private final boolean notify_player; private final boolean notify_player;
public PreventUnoptimizedTrading() { public PreventUnoptimizedTrading() {
super("gameplay.prevent-trading-with-unoptimized"); shouldEnable();
config.master().addComment(configPath + ".enable", this.villagerCache = VillagerOptimizer.getCache();
Config config = VillagerOptimizer.getConfiguration();
config.master().addComment("gameplay.prevent-trading-with-unoptimized.enable",
"Will prevent players from selecting and using trades of unoptimized villagers.\n" + "Will prevent players from selecting and using trades of unoptimized villagers.\n" +
"Use this if you have a lot of villagers and therefore want to force your players to optimize them.\n" + "Use this if you have a lot of villagers and therefore want to force your players to optimize them.\n" +
"Inventories can still be opened so players can move villagers around."); "Inventories can still be opened so players can move villagers around.");
this.notify_player = config.getBoolean(configPath + ".notify-player", true, this.notify_player = config.getBoolean("gameplay.prevent-trading-with-unoptimized.notify-player", true,
"Sends players a message when they try to trade with an unoptimized villager."); "Sends players a message when they try to trade with an unoptimized villager.");
} }
@Override @Override
public void enable() { public void enable() {
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
plugin.getServer().getPluginManager().registerEvents(this, plugin); plugin.getServer().getPluginManager().registerEvents(this, plugin);
} }
@ -41,36 +46,38 @@ public class PreventUnoptimizedTrading extends VillagerOptimizerModule implement
@Override @Override
public boolean shouldEnable() { public boolean shouldEnable() {
return config.getBoolean(configPath + ".enable", false); return VillagerOptimizer.getConfiguration().getBoolean("gameplay.prevent-trading-with-unoptimized.enable", false);
} }
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onTradeOpen(TradeSelectEvent event) { private void onTradeOpen(TradeSelectEvent event) {
if (event.getInventory().getType() != InventoryType.MERCHANT) return; if (!event.getInventory().getType().equals(InventoryType.MERCHANT)) return;
if (event.getWhoClicked().hasPermission(Permissions.Bypass.TRADE_PREVENTION.get())) return; if (event.getWhoClicked().hasPermission(Permissions.Bypass.TRADE_PREVENTION.get())) return;
if (!(event.getInventory().getHolder() instanceof Villager)) return; if (!(event.getInventory().getHolder() instanceof Villager)) return;
if (wrapperCache.get((Villager) event.getInventory().getHolder(), WrappedVillager::new).isOptimized()) return; if (villagerCache.getOrAdd((Villager) event.getInventory().getHolder()).isOptimized()) return;
event.setCancelled(true); event.setCancelled(true);
if (notify_player) { if (notify_player) {
Player player = (Player) event.getWhoClicked(); Player player = (Player) event.getWhoClicked();
VillagerOptimizer.getLang(player.locale()).optimize_for_trading.forEach(line -> KyoriUtil.sendMessage(player, line)); VillagerOptimizer.getLang(player.locale()).optimize_for_trading
.forEach(line -> KyoriUtil.sendMessage(player, line));
} }
} }
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onInventoryClick(InventoryClickEvent event) { private void onInventoryClick(InventoryClickEvent event) {
if (event.getInventory().getType() != InventoryType.MERCHANT) return; if (!event.getInventory().getType().equals(InventoryType.MERCHANT)) return;
if (event.getWhoClicked().hasPermission(Permissions.Bypass.TRADE_PREVENTION.get())) return; if (event.getWhoClicked().hasPermission(Permissions.Bypass.TRADE_PREVENTION.get())) return;
if (!(event.getInventory().getHolder() instanceof Villager)) return; if (!(event.getInventory().getHolder() instanceof Villager)) return;
if (wrapperCache.get((Villager) event.getInventory().getHolder(), WrappedVillager::new).isOptimized()) return; if (villagerCache.getOrAdd((Villager) event.getInventory().getHolder()).isOptimized()) return;
event.setCancelled(true); event.setCancelled(true);
if (notify_player) { if (notify_player) {
Player player = (Player) event.getWhoClicked(); Player player = (Player) event.getWhoClicked();
VillagerOptimizer.getLang(player.locale()).optimize_for_trading.forEach(line -> KyoriUtil.sendMessage(player, line)); VillagerOptimizer.getLang(player.locale()).optimize_for_trading
.forEach(line -> KyoriUtil.sendMessage(player, line));
} }
} }
} }

View File

@ -1,14 +1,17 @@
package me.xginko.villageroptimizer.modules.gameplay; package me.xginko.villageroptimizer.modules.gameplay;
import com.cryptomorin.xseries.XEntityType; import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.VillagerOptimizer; import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.WrappedVillager;
import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.enums.Permissions;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule; import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import me.xginko.villageroptimizer.struct.enums.Permissions; import me.xginko.villageroptimizer.utils.GenericUtil;
import me.xginko.villageroptimizer.utils.KyoriUtil; import me.xginko.villageroptimizer.utils.KyoriUtil;
import me.xginko.villageroptimizer.utils.LocationUtil; import net.kyori.adventure.text.Component;
import me.xginko.villageroptimizer.utils.Util;
import me.xginko.villageroptimizer.wrapper.WrappedVillager;
import net.kyori.adventure.text.TextReplacementConfig; import net.kyori.adventure.text.TextReplacementConfig;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.entity.Villager; import org.bukkit.entity.Villager;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority; import org.bukkit.event.EventPriority;
@ -17,32 +20,30 @@ import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerInteractEntityEvent; import org.bukkit.event.player.PlayerInteractEntityEvent;
import java.time.Duration; import java.time.Duration;
import java.util.Arrays;
import java.util.Comparator;
import java.util.SortedSet;
import java.util.TreeSet;
public class RestockOptimizedTrades extends VillagerOptimizerModule implements Listener { public class RestockOptimizedTrades implements VillagerOptimizerModule, Listener {
private final SortedSet<Long> restockDayTimes; private final VillagerCache villagerCache;
private final long restock_delay_millis;
private final boolean log_enabled, notify_player; private final boolean log_enabled, notify_player;
public RestockOptimizedTrades() { public RestockOptimizedTrades() {
super("gameplay.restock-optimized-trades"); shouldEnable();
config.master().addComment(configPath, this.villagerCache = VillagerOptimizer.getCache();
Config config = VillagerOptimizer.getConfiguration();
config.master().addComment("gameplay.restock-optimized-trades",
"This is for automatic restocking of trades for optimized villagers. Optimized Villagers\n" + "This is for automatic restocking of trades for optimized villagers. Optimized Villagers\n" +
"don't have enough AI to restock their trades naturally, so this is here as a workaround."); "don't have enough AI to restock their trades naturally, so this is here as a workaround.");
this.restockDayTimes = new TreeSet<>(Comparator.reverseOrder()); this.restock_delay_millis = config.getInt("gameplay.restock-optimized-trades.delay-in-ticks", 1000,
this.restockDayTimes.addAll(config.getList(configPath + ".restock-times", Arrays.asList(1000L, 13000L), "1 second = 20 ticks. There are 24.000 ticks in a single minecraft day.") * 50L;
"At which (tick-)times during the day villagers will restock.\n" + this.notify_player = config.getBoolean("gameplay.restock-optimized-trades.notify-player", true,
"There are 24.000 ticks in a single minecraft day."));
this.notify_player = config.getBoolean(configPath + ".notify-player", true,
"Sends the player a message when the trades were restocked on a clicked villager."); "Sends the player a message when the trades were restocked on a clicked villager.");
this.log_enabled = config.getBoolean(configPath + ".log", false); this.log_enabled = config.getBoolean("gameplay.restock-optimized-trades.log", false);
} }
@Override @Override
public void enable() { public void enable() {
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
plugin.getServer().getPluginManager().registerEvents(this, plugin); plugin.getServer().getPluginManager().registerEvents(this, plugin);
} }
@ -57,54 +58,32 @@ public class RestockOptimizedTrades extends VillagerOptimizerModule implements L
} }
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onPlayerInteractEntity(PlayerInteractEntityEvent event) { private void onInteract(PlayerInteractEntityEvent event) {
if (event.getRightClicked().getType() != XEntityType.VILLAGER.get()) return; if (!event.getRightClicked().getType().equals(EntityType.VILLAGER)) return;
WrappedVillager wrapped = wrapperCache.get((Villager) event.getRightClicked(), WrappedVillager::new); final WrappedVillager wVillager = villagerCache.getOrAdd((Villager) event.getRightClicked());
if (!wrapped.isOptimized()) return; if (!wVillager.isOptimized()) return;
if (event.getPlayer().hasPermission(Permissions.Bypass.RESTOCK_COOLDOWN.get())) { final Player player = event.getPlayer();
wrapped.restock(); final boolean player_bypassing = player.hasPermission(Permissions.Bypass.RESTOCK_COOLDOWN.get());
return;
}
long lastRestockFullTimeTicks = wrapped.getLastRestockFullTime(); if (wVillager.canRestock(restock_delay_millis) || player_bypassing) {
long currentFullTimeTicks = wrapped.currentFullTimeTicks(); wVillager.restock();
long currentDayTimeTicks = wrapped.currentDayTimeTicks(); wVillager.saveRestockTime();
long currentDay = currentFullTimeTicks - currentDayTimeTicks; if (notify_player && !player_bypassing) {
long ticksTillRestock = (24000 + currentDay + restockDayTimes.first()) - currentFullTimeTicks;
boolean restocked = false;
for (Long restockDayTime : restockDayTimes) {
long restockTimeToday = currentDay + restockDayTime;
if (currentFullTimeTicks < restockTimeToday || lastRestockFullTimeTicks >= restockTimeToday) {
ticksTillRestock = Math.min(ticksTillRestock, restockTimeToday - currentFullTimeTicks);
continue;
}
if (!restocked) {
wrapped.restock();
wrapped.saveRestockTime();
restocked = true;
}
}
if (!restocked) return;
if (notify_player) {
final TextReplacementConfig timeLeft = TextReplacementConfig.builder() final TextReplacementConfig timeLeft = TextReplacementConfig.builder()
.matchLiteral("%time%") .matchLiteral("%time%")
.replacement(Util.formatDuration(Duration.ofMillis(ticksTillRestock * 50L))) .replacement(GenericUtil.formatDuration(Duration.ofMillis(wVillager.getRestockCooldownMillis(restock_delay_millis))))
.build(); .build();
VillagerOptimizer.getLang(event.getPlayer().locale()).trades_restocked VillagerOptimizer.getLang(player.locale()).trades_restocked
.forEach(line -> KyoriUtil.sendMessage(event.getPlayer(), line.replaceText(timeLeft))); .forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(timeLeft)));
} }
if (log_enabled) { if (log_enabled) {
info("Restocked optimized villager at " + LocationUtil.toString(wrapped.villager.getLocation())); VillagerOptimizer.getLog().info(Component.text("Restocked optimized villager at " +
GenericUtil.formatLocation(wVillager.villager().getLocation())).color(GenericUtil.COLOR));
}
} }
} }
} }

View File

@ -1,24 +1,27 @@
package me.xginko.villageroptimizer.modules.gameplay; package me.xginko.villageroptimizer.modules.gameplay;
import me.xginko.villageroptimizer.struct.enums.OptimizationType; import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.WrappedVillager;
import me.xginko.villageroptimizer.enums.OptimizationType;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule; import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import me.xginko.villageroptimizer.wrapper.WrappedVillager;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority; import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList; import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.event.entity.VillagerCareerChangeEvent; import org.bukkit.event.entity.VillagerCareerChangeEvent;
public class UnoptimizeOnJobLoose extends VillagerOptimizerModule implements Listener { public class UnoptimizeOnJobLoose implements VillagerOptimizerModule, Listener {
private final VillagerCache villagerCache;
public UnoptimizeOnJobLoose() { public UnoptimizeOnJobLoose() {
super("gameplay.unoptimize-on-job-loose"); this.villagerCache = VillagerOptimizer.getCache();
config.master().addComment(configPath + ".enable",
"Villagers that get their jobs reset will become unoptimized again.");
} }
@Override @Override
public void enable() { public void enable() {
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
plugin.getServer().getPluginManager().registerEvents(this, plugin); plugin.getServer().getPluginManager().registerEvents(this, plugin);
} }
@ -29,15 +32,16 @@ public class UnoptimizeOnJobLoose extends VillagerOptimizerModule implements Lis
@Override @Override
public boolean shouldEnable() { public boolean shouldEnable() {
return config.getBoolean(configPath + ".enable", true); return VillagerOptimizer.getConfiguration().getBoolean("gameplay.unoptimize-on-job-loose.enable", true,
"Villagers that get their jobs reset will become unoptimized again.");
} }
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onJobReset(VillagerCareerChangeEvent event) { private void onJobReset(VillagerCareerChangeEvent event) {
if (event.getReason() != VillagerCareerChangeEvent.ChangeReason.LOSING_JOB) return; if (!event.getReason().equals(VillagerCareerChangeEvent.ChangeReason.LOSING_JOB)) return;
final WrappedVillager wrapped = wrapperCache.get(event.getEntity(), WrappedVillager::new); final WrappedVillager wrappedVillager = villagerCache.getOrAdd(event.getEntity());
if (wrapped.isOptimized()) { if (wrappedVillager.isOptimized()) {
wrapped.setOptimizationType(OptimizationType.NONE); wrappedVillager.setOptimizationType(OptimizationType.NONE);
} }
} }
} }

View File

@ -1,5 +1,8 @@
package me.xginko.villageroptimizer.modules.gameplay; package me.xginko.villageroptimizer.modules.gameplay;
import com.tcoded.folialib.impl.ServerImplementation;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.events.VillagerOptimizeEvent; import me.xginko.villageroptimizer.events.VillagerOptimizeEvent;
import me.xginko.villageroptimizer.events.VillagerUnoptimizeEvent; import me.xginko.villageroptimizer.events.VillagerUnoptimizeEvent;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule; import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
@ -9,16 +12,21 @@ import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList; import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
public class VisuallyHighlightOptimized extends VillagerOptimizerModule implements Listener { public class VisuallyHighlightOptimized implements VillagerOptimizerModule, Listener {
private final ServerImplementation scheduler;
public VisuallyHighlightOptimized() { public VisuallyHighlightOptimized() {
super("gameplay.outline-optimized-villagers"); shouldEnable();
this.scheduler = VillagerOptimizer.getFoliaLib().getImpl();
Config config = VillagerOptimizer.getConfiguration();
config.master().addComment("gameplay.outline-optimized-villagers.enable", config.master().addComment("gameplay.outline-optimized-villagers.enable",
"Will make optimized villagers glow."); "Will make optimized villagers glow.");
} }
@Override @Override
public void enable() { public void enable() {
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
plugin.getServer().getPluginManager().registerEvents(this, plugin); plugin.getServer().getPluginManager().registerEvents(this, plugin);
} }
@ -29,22 +37,22 @@ public class VisuallyHighlightOptimized extends VillagerOptimizerModule implemen
@Override @Override
public boolean shouldEnable() { public boolean shouldEnable() {
return config.getBoolean("gameplay.outline-optimized-villagers.enable", false); return VillagerOptimizer.getConfiguration().getBoolean("gameplay.outline-optimized-villagers.enable", false);
} }
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onOptimize(VillagerOptimizeEvent event) { private void onOptimize(VillagerOptimizeEvent event) {
Villager villager = event.getWrappedVillager().villager; Villager villager = event.getWrappedVillager().villager();
scheduling.entitySpecificScheduler(villager).run(glow -> { scheduler.runAtEntity(villager, glow -> {
if (!villager.isGlowing()) villager.setGlowing(true); if (!villager.isGlowing()) villager.setGlowing(true);
}, null); });
} }
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onUnOptimize(VillagerUnoptimizeEvent event) { private void onUnOptimize(VillagerUnoptimizeEvent event) {
Villager villager = event.getWrappedVillager().villager; Villager villager = event.getWrappedVillager().villager();
scheduling.entitySpecificScheduler(villager).run(unGlow -> { scheduler.runAtEntity(villager, unGlow -> {
if (villager.isGlowing()) villager.setGlowing(false); if (villager.isGlowing()) villager.setGlowing(false);
}, null); });
} }
} }

View File

@ -1,240 +0,0 @@
package me.xginko.villageroptimizer.modules.optimization;
import com.cryptomorin.xseries.XEntityType;
import com.destroystokyo.paper.event.entity.EntityPathfindEvent;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import me.xginko.villageroptimizer.struct.enums.OptimizationType;
import me.xginko.villageroptimizer.struct.models.BlockRegion2D;
import me.xginko.villageroptimizer.wrapper.WrappedVillager;
import net.kyori.adventure.text.TextReplacementConfig;
import org.bukkit.Location;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.entity.Villager;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityInteractEvent;
import org.jetbrains.annotations.NotNull;
import java.time.Duration;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
public class OptimizeByActivity extends VillagerOptimizerModule implements Listener {
protected static class RegionData {
public final BlockRegion2D region;
public final AtomicInteger pathfindCount, entityInteractCount;
public final AtomicBoolean regionBusy;
public RegionData(BlockRegion2D region) {
this.region = region;
this.pathfindCount = new AtomicInteger();
this.entityInteractCount = new AtomicInteger();
this.regionBusy = new AtomicBoolean(false);
}
}
private final Cache<BlockRegion2D, RegionData> regionDataCache;
private final double checkRadius;
private final int pathfindLimit, entityInteractLimit;
private final boolean notifyPlayers, doLogging;
public OptimizeByActivity() {
super("optimization-methods.regional-activity");
config.master().addComment(configPath + ".enable",
"Enable optimization by naming villagers to one of the names configured below.\n" +
"Nametag optimized villagers will be unoptimized again when they are renamed to something else.");
this.checkRadius = config.getDouble(configPath + ".check-radius-blocks", 500.0,
"The radius in blocks in which activity will be grouped together and measured.");
this.regionDataCache = Caffeine.newBuilder().expireAfterWrite(Duration.ofMillis(
config.getInt(configPath + ".data-keep-time-millis", 10000,
"The time in milliseconds before a region and its data will be expired\n" +
"if no activity has been detected.\n" +
"For proper functionality, needs to be at least as long as your pause time."))).build();
this.pathfindLimit = config.getInt(configPath + ".limits.pathfind-event", 150);
this.entityInteractLimit = config.getInt(configPath + ".limits.interact-event", 50);
this.notifyPlayers = config.getBoolean(configPath + ".notify-players", true,
"Sends players a message to any player near an auto-optimized villager.");
this.doLogging = config.getBoolean(configPath + ".log", false);
}
@Override
public void enable() {
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@Override
public void disable() {
HandlerList.unregisterAll(this);
}
@Override
public boolean shouldEnable() {
return config.getBoolean(configPath + ".enable", false);
}
private @NotNull RegionData getRegionData(Location location) {
return regionDataCache.get(getRegion(location), RegionData::new);
}
private @NotNull BlockRegion2D getRegion(Location location) {
// Find and return region containing this location
for (Map.Entry<BlockRegion2D, RegionData> regionDataEntry : regionDataCache.asMap().entrySet()) {
if (regionDataEntry.getKey().contains(location)) {
return regionDataEntry.getKey();
}
}
// Create and cache region if none exists
BlockRegion2D region = BlockRegion2D.of(location.getWorld(), location.getX(), location.getZ(), checkRadius);
regionDataCache.put(region, new RegionData(region));
return region;
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onEntityPathfind(EntityPathfindEvent event) {
if (event.getEntityType() != XEntityType.VILLAGER.get()) return;
Location location = event.getEntity().getLocation();
BlockRegion2D region2D = getRegion(location);
RegionData regionData = getRegionData(location);
if (regionData.regionBusy.get() || regionData.pathfindCount.incrementAndGet() <= pathfindLimit) {
return;
}
regionData.regionBusy.set(true);
AtomicInteger optimizeCount = new AtomicInteger();
Set<Player> playersWithinArea = new CopyOnWriteArraySet<>();
region2D.getEntities()
.thenAccept(entities -> {
for (Entity entity : entities) {
scheduling.entitySpecificScheduler(entity).run(() -> {
if (entity.getType() == XEntityType.VILLAGER.get()) {
WrappedVillager wrappedVillager = wrapperCache.get((Villager) entity, WrappedVillager::new);
if (wrappedVillager.isOptimized()) {
return;
}
wrappedVillager.setOptimizationType(OptimizationType.REGIONAL_ACTIVITY);
optimizeCount.incrementAndGet();
}
if (notifyPlayers && entity.getType() == XEntityType.PLAYER.get()) {
playersWithinArea.add((Player) entity);
}
}, null);
}
})
.thenRun(() -> {
if (notifyPlayers) {
TextReplacementConfig amount = TextReplacementConfig.builder()
.matchLiteral("%amount%")
.replacement(optimizeCount.toString())
.build();
for (Player player : playersWithinArea) {
VillagerOptimizer.scheduling().entitySpecificScheduler(player).run(() ->
VillagerOptimizer.getLang(player.locale()).activity_optimize_success
.forEach(line -> player.sendMessage(line.replaceText(amount))),
null);
}
playersWithinArea.clear();
}
if (doLogging) {
info( "Optimized " + optimizeCount.get() + " villagers in a radius of " + checkRadius +
" blocks from center at x=" + regionData.region.getCenterX() + ", z=" + regionData.region.getCenterZ() +
" in world " + location.getWorld().getName() +
"because of too high pathfinding activity within the configured timeframe: " +
regionData.pathfindCount + " (limit: " + pathfindLimit + ")");
}
regionDataCache.invalidate(region2D);
});
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onEntityInteract(EntityInteractEvent event) {
if (event.getEntityType() != XEntityType.VILLAGER.get()) return;
Location location = event.getEntity().getLocation();
BlockRegion2D region2D = getRegion(location);
RegionData regionData = getRegionData(location);
if (regionData.regionBusy.get() || regionData.entityInteractCount.incrementAndGet() <= entityInteractLimit) {
return;
}
regionData.regionBusy.set(true);
AtomicInteger optimizeCount = new AtomicInteger();
Set<Player> playersWithinArea = new CopyOnWriteArraySet<>();
region2D.getEntities()
.thenAccept(entities -> {
for (Entity entity : entities) {
scheduling.entitySpecificScheduler(entity).run(() -> {
if (entity.getType() == XEntityType.VILLAGER.get()) {
WrappedVillager wrappedVillager = wrapperCache.get((Villager) entity, WrappedVillager::new);
if (wrappedVillager.isOptimized()) {
return;
}
wrappedVillager.setOptimizationType(OptimizationType.REGIONAL_ACTIVITY);
optimizeCount.incrementAndGet();
}
if (notifyPlayers && entity.getType() == XEntityType.PLAYER.get()) {
playersWithinArea.add((Player) entity);
}
}, null);
}
})
.thenRun(() -> {
if (notifyPlayers) {
TextReplacementConfig amount = TextReplacementConfig.builder()
.matchLiteral("%amount%")
.replacement(optimizeCount.toString())
.build();
for (Player player : playersWithinArea) {
VillagerOptimizer.scheduling().entitySpecificScheduler(player).run(() ->
VillagerOptimizer.getLang(player.locale()).activity_optimize_success
.forEach(line -> player.sendMessage(line.replaceText(amount))),
null);
}
playersWithinArea.clear();
}
if (doLogging) {
info( "Optimized " + optimizeCount.get() + " villagers in a radius of " + checkRadius +
" blocks from center at x=" + regionData.region.getCenterX() + ", z=" + regionData.region.getCenterZ() +
" in world " + location.getWorld().getName() +
"because of too many villagers interacting with objects within the configured timeframe: " +
regionData.pathfindCount + " (limit: " + pathfindLimit + ")");
}
regionDataCache.invalidate(region2D);
});
}
}

View File

@ -1,16 +1,17 @@
package me.xginko.villageroptimizer.modules.optimization; package me.xginko.villageroptimizer.modules.optimization;
import com.cryptomorin.xseries.XMaterial; import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.VillagerOptimizer; import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.struct.enums.OptimizationType; import me.xginko.villageroptimizer.WrappedVillager;
import me.xginko.villageroptimizer.struct.enums.Permissions; import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.enums.OptimizationType;
import me.xginko.villageroptimizer.enums.Permissions;
import me.xginko.villageroptimizer.events.VillagerOptimizeEvent; import me.xginko.villageroptimizer.events.VillagerOptimizeEvent;
import me.xginko.villageroptimizer.events.VillagerUnoptimizeEvent; import me.xginko.villageroptimizer.events.VillagerUnoptimizeEvent;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule; import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import me.xginko.villageroptimizer.utils.GenericUtil;
import me.xginko.villageroptimizer.utils.KyoriUtil; import me.xginko.villageroptimizer.utils.KyoriUtil;
import me.xginko.villageroptimizer.utils.LocationUtil; import net.kyori.adventure.text.Component;
import me.xginko.villageroptimizer.utils.Util;
import me.xginko.villageroptimizer.wrapper.WrappedVillager;
import net.kyori.adventure.text.TextReplacementConfig; import net.kyori.adventure.text.TextReplacementConfig;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.Material; import org.bukkit.Material;
@ -25,60 +26,58 @@ import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent; import org.bukkit.event.block.BlockPlaceEvent;
import java.time.Duration; import java.time.Duration;
import java.util.EnumSet; import java.util.Arrays;
import java.util.List; import java.util.HashSet;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream;
public class OptimizeByBlock extends VillagerOptimizerModule implements Listener { public class OptimizeByBlock implements VillagerOptimizerModule, Listener {
private final VillagerCache villagerCache;
private final Set<Material> blocks_that_disable; private final Set<Material> blocks_that_disable;
private final long cooldown_millis; private final long cooldown_millis;
private final double search_radius; private final double search_radius;
private final boolean only_while_sneaking, notify_player, log_enabled; private final boolean only_while_sneaking, notify_player, log_enabled;
public OptimizeByBlock() { public OptimizeByBlock() {
super("optimization-methods.block-optimization"); shouldEnable();
config.master().addComment(configPath + ".enable", this.villagerCache = VillagerOptimizer.getCache();
Config config = VillagerOptimizer.getConfiguration();
config.master().addComment("optimization-methods.block-optimization.enable",
"When enabled, the closest villager standing near a configured block being placed will be optimized.\n" + "When enabled, the closest villager standing near a configured block being placed will be optimized.\n" +
"If a configured block is broken nearby, the closest villager will become unoptimized again."); "If a configured block is broken nearby, the closest villager will become unoptimized again.");
List<String> defaults = Stream.of(XMaterial.LAPIS_BLOCK, XMaterial.GLOWSTONE, XMaterial.IRON_BLOCK) this.blocks_that_disable = config.getList("optimization-methods.block-optimization.materials", Arrays.asList(
.filter(XMaterial::isSupported) "LAPIS_BLOCK", "GLOWSTONE", "IRON_BLOCK"
.map(Enum::name) ), "Values here need to be valid bukkit Material enums for your server version."
.collect(Collectors.toList()); ).stream().map(configuredMaterial -> {
this.blocks_that_disable = config.getList(configPath + ".materials", defaults,
"Values here need to be valid bukkit Material enums for your server version.")
.stream()
.map(configuredMaterial -> {
try { try {
return Material.valueOf(configuredMaterial); return Material.valueOf(configuredMaterial);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
warn("Material '" + configuredMaterial + "' not recognized. Please use correct Material enums from: " + VillagerOptimizer.getLog().warn("(block-optimization) Material '" + configuredMaterial +
"' not recognized. Please use correct Material enums from: " +
"https://jd.papermc.io/paper/1.20/org/bukkit/Material.html"); "https://jd.papermc.io/paper/1.20/org/bukkit/Material.html");
return null; return null;
} }
}) }).filter(Objects::nonNull).collect(Collectors.toCollection(HashSet::new));
.filter(Objects::nonNull)
.collect(Collectors.toCollection(() -> EnumSet.noneOf(Material.class)));
this.cooldown_millis = TimeUnit.SECONDS.toMillis( this.cooldown_millis = TimeUnit.SECONDS.toMillis(
config.getInt(configPath + ".optimize-cooldown-seconds", 600, config.getInt("optimization-methods.block-optimization.optimize-cooldown-seconds", 600,
"Cooldown in seconds until a villager can be optimized again by using specific blocks.\n" + "Cooldown in seconds until a villager can be optimized again by using specific blocks.\n" +
"Here for configuration freedom. Recommended to leave as is to not enable any exploitable behavior.")); "Here for configuration freedom. Recommended to leave as is to not enable any exploitable behavior."));
this.search_radius = config.getDouble(configPath + ".search-radius-in-blocks", 2.0, this.search_radius = config.getDouble("optimization-methods.block-optimization.search-radius-in-blocks", 2.0,
"The radius in blocks a villager can be away from the player when he places an optimize block.\n" + "The radius in blocks a villager can be away from the player when he places an optimize block.\n" +
"The closest unoptimized villager to the player will be optimized.") / 2; "The closest unoptimized villager to the player will be optimized.") / 2;
this.only_while_sneaking = config.getBoolean(configPath + ".only-when-sneaking", true, this.only_while_sneaking = config.getBoolean("optimization-methods.block-optimization.only-when-sneaking", true,
"Only optimize/unoptimize by block when player is sneaking during place or break."); "Only optimize/unoptimize by block when player is sneaking during place or break.");
this.notify_player = config.getBoolean(configPath + ".notify-player", true, this.notify_player = config.getBoolean("optimization-methods.block-optimization.notify-player", true,
"Sends players a message when they successfully optimized or unoptimized a villager."); "Sends players a message when they successfully optimized or unoptimized a villager.");
this.log_enabled = config.getBoolean(configPath + ".log", false); this.log_enabled = config.getBoolean("optimization-methods.block-optimization.log", false);
} }
@Override @Override
public void enable() { public void enable() {
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
plugin.getServer().getPluginManager().registerEvents(this, plugin); plugin.getServer().getPluginManager().registerEvents(this, plugin);
} }
@ -89,7 +88,7 @@ public class OptimizeByBlock extends VillagerOptimizerModule implements Listener
@Override @Override
public boolean shouldEnable() { public boolean shouldEnable() {
return config.getBoolean(configPath + ".enable", false); return VillagerOptimizer.getConfiguration().getBoolean("optimization-methods.block-optimization.enable", false);
} }
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
@ -107,11 +106,10 @@ public class OptimizeByBlock extends VillagerOptimizerModule implements Listener
for (Villager villager : blockLoc.getNearbyEntitiesByType(Villager.class, search_radius)) { for (Villager villager : blockLoc.getNearbyEntitiesByType(Villager.class, search_radius)) {
final Villager.Profession profession = villager.getProfession(); final Villager.Profession profession = villager.getProfession();
if (profession.equals(Villager.Profession.NONE) || profession.equals(Villager.Profession.NITWIT)) continue; if (profession.equals(Villager.Profession.NONE) || profession.equals(Villager.Profession.NITWIT)) continue;
final double distance = villager.getLocation().distanceSquared(blockLoc);
final double distance = LocationUtil.relDistance3DSquared(villager.getLocation(), blockLoc);
if (distance >= closestDistance) continue; if (distance >= closestDistance) continue;
final WrappedVillager wVillager = wrapperCache.get(villager, WrappedVillager::new); final WrappedVillager wVillager = villagerCache.getOrAdd(villager);
if (wVillager.canOptimize(cooldown_millis)) { if (wVillager.canOptimize(cooldown_millis)) {
closestOptimizableVillager = wVillager; closestOptimizableVillager = wVillager;
closestDistance = distance; closestDistance = distance;
@ -135,26 +133,26 @@ public class OptimizeByBlock extends VillagerOptimizerModule implements Listener
if (notify_player) { if (notify_player) {
final TextReplacementConfig vilProfession = TextReplacementConfig.builder() final TextReplacementConfig vilProfession = TextReplacementConfig.builder()
.matchLiteral("%vil_profession%") .matchLiteral("%vil_profession%")
.replacement(Util.toNiceString(closestOptimizableVillager.villager.getProfession())) .replacement(closestOptimizableVillager.villager().getProfession().toString().toLowerCase())
.build(); .build();
final TextReplacementConfig placedMaterial = TextReplacementConfig.builder() final TextReplacementConfig placedMaterial = TextReplacementConfig.builder()
.matchLiteral("%blocktype%") .matchLiteral("%blocktype%")
.replacement(Util.toNiceString(placed.getType())) .replacement(placed.getType().toString().toLowerCase())
.build(); .build();
VillagerOptimizer.getLang(player.locale()).block_optimize_success VillagerOptimizer.getLang(player.locale()).block_optimize_success
.forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(vilProfession).replaceText(placedMaterial))); .forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(vilProfession).replaceText(placedMaterial)));
} }
if (log_enabled) { if (log_enabled) {
info(player.getName() + " optimized villager at " + VillagerOptimizer.getLog().info(Component.text(player.getName() + " optimized villager by block at " +
LocationUtil.toString(closestOptimizableVillager.villager.getLocation())); GenericUtil.formatLocation(closestOptimizableVillager.villager().getLocation())).color(GenericUtil.COLOR));
} }
} else { } else {
closestOptimizableVillager.sayNo(); closestOptimizableVillager.sayNo();
if (notify_player) { if (notify_player) {
final TextReplacementConfig timeLeft = TextReplacementConfig.builder() final TextReplacementConfig timeLeft = TextReplacementConfig.builder()
.matchLiteral("%time%") .matchLiteral("%time%")
.replacement(Util.formatDuration(Duration.ofMillis(closestOptimizableVillager.getOptimizeCooldownMillis(cooldown_millis)))) .replacement(GenericUtil.formatDuration(Duration.ofMillis(closestOptimizableVillager.getOptimizeCooldownMillis(cooldown_millis))))
.build(); .build();
VillagerOptimizer.getLang(player.locale()).block_on_optimize_cooldown VillagerOptimizer.getLang(player.locale()).block_on_optimize_cooldown
.forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(timeLeft))); .forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(timeLeft)));
@ -175,10 +173,10 @@ public class OptimizeByBlock extends VillagerOptimizerModule implements Listener
double closestDistance = Double.MAX_VALUE; double closestDistance = Double.MAX_VALUE;
for (Villager villager : blockLoc.getNearbyEntitiesByType(Villager.class, search_radius)) { for (Villager villager : blockLoc.getNearbyEntitiesByType(Villager.class, search_radius)) {
final double distance = LocationUtil.relDistance3DSquared(villager.getLocation(), blockLoc); final double distance = villager.getLocation().distanceSquared(blockLoc);
if (distance >= closestDistance) continue; if (distance >= closestDistance) continue;
final WrappedVillager wVillager = wrapperCache.get(villager, WrappedVillager::new); final WrappedVillager wVillager = villagerCache.getOrAdd(villager);
if (wVillager.isOptimized()) { if (wVillager.isOptimized()) {
closestOptimizedVillager = wVillager; closestOptimizedVillager = wVillager;
closestDistance = distance; closestDistance = distance;
@ -200,19 +198,19 @@ public class OptimizeByBlock extends VillagerOptimizerModule implements Listener
if (notify_player) { if (notify_player) {
final TextReplacementConfig vilProfession = TextReplacementConfig.builder() final TextReplacementConfig vilProfession = TextReplacementConfig.builder()
.matchLiteral("%vil_profession%") .matchLiteral("%vil_profession%")
.replacement(Util.toNiceString(closestOptimizedVillager.villager.getProfession())) .replacement(closestOptimizedVillager.villager().getProfession().toString().toLowerCase())
.build(); .build();
final TextReplacementConfig brokenMaterial = TextReplacementConfig.builder() final TextReplacementConfig brokenMaterial = TextReplacementConfig.builder()
.matchLiteral("%blocktype%") .matchLiteral("%blocktype%")
.replacement(Util.toNiceString(broken.getType())) .replacement(broken.getType().toString().toLowerCase())
.build(); .build();
VillagerOptimizer.getLang(player.locale()).block_unoptimize_success VillagerOptimizer.getLang(player.locale()).block_unoptimize_success
.forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(vilProfession).replaceText(brokenMaterial))); .forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(vilProfession).replaceText(brokenMaterial)));
} }
if (log_enabled) { if (log_enabled) {
info(player.getName() + " unoptimized villager using " + Util.toNiceString(broken.getType()) + VillagerOptimizer.getLog().info(Component.text(player.getName() + " unoptimized villager by block at " +
LocationUtil.toString(closestOptimizedVillager.villager.getLocation())); GenericUtil.formatLocation(closestOptimizedVillager.villager().getLocation())).color(GenericUtil.COLOR));
} }
} }
} }

View File

@ -1,20 +1,20 @@
package me.xginko.villageroptimizer.modules.optimization; package me.xginko.villageroptimizer.modules.optimization;
import com.cryptomorin.xseries.XEntityType; import me.xginko.villageroptimizer.VillagerCache;
import com.cryptomorin.xseries.XMaterial;
import me.xginko.villageroptimizer.VillagerOptimizer; import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.struct.enums.OptimizationType; import me.xginko.villageroptimizer.WrappedVillager;
import me.xginko.villageroptimizer.struct.enums.Permissions; import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.enums.OptimizationType;
import me.xginko.villageroptimizer.enums.Permissions;
import me.xginko.villageroptimizer.events.VillagerOptimizeEvent; import me.xginko.villageroptimizer.events.VillagerOptimizeEvent;
import me.xginko.villageroptimizer.events.VillagerUnoptimizeEvent; import me.xginko.villageroptimizer.events.VillagerUnoptimizeEvent;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule; import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import me.xginko.villageroptimizer.utils.GenericUtil;
import me.xginko.villageroptimizer.utils.KyoriUtil; import me.xginko.villageroptimizer.utils.KyoriUtil;
import me.xginko.villageroptimizer.utils.LocationUtil; import net.kyori.adventure.text.Component;
import me.xginko.villageroptimizer.utils.Util;
import me.xginko.villageroptimizer.wrapper.WrappedVillager;
import net.kyori.adventure.text.TextReplacementConfig; import net.kyori.adventure.text.TextReplacementConfig;
import org.bukkit.ChatColor; import org.bukkit.Material;
import org.bukkit.GameMode; import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.entity.Villager; import org.bukkit.entity.Villager;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
@ -32,33 +32,37 @@ import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class OptimizeByNametag extends VillagerOptimizerModule implements Listener { public class OptimizeByNametag implements VillagerOptimizerModule, Listener {
private final VillagerCache villagerCache;
private final Set<String> nametags; private final Set<String> nametags;
private final long cooldown; private final long cooldown;
private final boolean consume_nametag, notify_player, log_enabled; private final boolean consume_nametag, notify_player, log_enabled;
public OptimizeByNametag() { public OptimizeByNametag() {
super("optimization-methods.nametag-optimization"); shouldEnable();
config.master().addComment(configPath + ".enable", this.villagerCache = VillagerOptimizer.getCache();
Config config = VillagerOptimizer.getConfiguration();
config.master().addComment("optimization-methods.nametag-optimization.enable",
"Enable optimization by naming villagers to one of the names configured below.\n" + "Enable optimization by naming villagers to one of the names configured below.\n" +
"Nametag optimized villagers will be unoptimized again when they are renamed to something else."); "Nametag optimized villagers will be unoptimized again when they are renamed to something else.");
this.nametags = config.getList(configPath + ".names", Arrays.asList("Optimize", "DisableAI"), this.nametags = config.getList("optimization-methods.nametag-optimization.names", Arrays.asList("Optimize", "DisableAI"),
"Names are case insensitive, capital letters won't matter.") "Names are case insensitive, capital letters won't matter.")
.stream().map(String::toLowerCase).collect(Collectors.toCollection(HashSet::new)); .stream().map(String::toLowerCase).collect(Collectors.toCollection(HashSet::new));
this.consume_nametag = config.getBoolean(configPath + ".nametags-get-consumed", true, this.consume_nametag = config.getBoolean("optimization-methods.nametag-optimization.nametags-get-consumed", true,
"Enable or disable consumption of the used nametag item."); "Enable or disable consumption of the used nametag item.");
this.cooldown = TimeUnit.SECONDS.toMillis( this.cooldown = TimeUnit.SECONDS.toMillis(
config.getInt(configPath + ".optimize-cooldown-seconds", 600, config.getInt("optimization-methods.nametag-optimization.optimize-cooldown-seconds", 600,
"Cooldown in seconds until a villager can be optimized again using a nametag.\n" + "Cooldown in seconds until a villager can be optimized again using a nametag.\n" +
"Here for configuration freedom. Recommended to leave as is to not enable any exploitable behavior.")); "Here for configuration freedom. Recommended to leave as is to not enable any exploitable behavior."));
this.notify_player = config.getBoolean(configPath + ".notify-player", true, this.notify_player = config.getBoolean("optimization-methods.nametag-optimization.notify-player", true,
"Sends players a message when they successfully optimized a villager."); "Sends players a message when they successfully optimized a villager.");
this.log_enabled = config.getBoolean(configPath + ".log", false); this.log_enabled = config.getBoolean("optimization-methods.nametag-optimization.log", false);
} }
@Override @Override
public void enable() { public void enable() {
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
plugin.getServer().getPluginManager().registerEvents(this, plugin); plugin.getServer().getPluginManager().registerEvents(this, plugin);
} }
@ -69,29 +73,32 @@ public class OptimizeByNametag extends VillagerOptimizerModule implements Listen
@Override @Override
public boolean shouldEnable() { public boolean shouldEnable() {
return config.getBoolean(configPath + ".enable", true); return VillagerOptimizer.getConfiguration().getBoolean("optimization-methods.nametag-optimization.enable", true);
} }
@SuppressWarnings("deprecation")
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onPlayerInteractEntity(PlayerInteractEntityEvent event) { private void onPlayerInteractEntity(PlayerInteractEntityEvent event) {
if (event.getRightClicked().getType() != XEntityType.VILLAGER.get()) return; if (!event.getRightClicked().getType().equals(EntityType.VILLAGER)) return;
final Player player = event.getPlayer(); final Player player = event.getPlayer();
if (!player.hasPermission(Permissions.Optimize.NAMETAG.get())) return; if (!player.hasPermission(Permissions.Optimize.NAMETAG.get())) return;
final ItemStack usedItem = player.getInventory().getItem(event.getHand()); final ItemStack usedItem = player.getInventory().getItem(event.getHand());
if (usedItem != null && usedItem.getType() != XMaterial.NAME_TAG.parseMaterial()) return; if (usedItem != null && !usedItem.getType().equals(Material.NAME_TAG)) return;
if (!usedItem.hasItemMeta()) return; if (!usedItem.hasItemMeta()) return;
final ItemMeta meta = usedItem.getItemMeta(); final ItemMeta meta = usedItem.getItemMeta();
if (!meta.hasDisplayName()) return; if (!meta.hasDisplayName()) return;
final String nameTagPlainText = ChatColor.stripColor(meta.getDisplayName()); // Get component name first, so we can manually name the villager when canceling the event to avoid item consumption.
final WrappedVillager wrapped = wrapperCache.get((Villager) event.getRightClicked(), WrappedVillager::new); final Component newVillagerName = meta.displayName();
assert newVillagerName != null; // Legitimate since we checked for hasDisplayName()
final String nameTagPlainText = GenericUtil.plainTextSerializer.serialize(newVillagerName);
final Villager villager = (Villager) event.getRightClicked();
final WrappedVillager wVillager = villagerCache.getOrAdd(villager);
if (nametags.contains(nameTagPlainText.toLowerCase())) { if (nametags.contains(nameTagPlainText.toLowerCase())) {
if (wrapped.canOptimize(cooldown) || player.hasPermission(Permissions.Bypass.NAMETAG_COOLDOWN.get())) { if (wVillager.canOptimize(cooldown) || player.hasPermission(Permissions.Bypass.NAMETAG_COOLDOWN.get())) {
VillagerOptimizeEvent optimizeEvent = new VillagerOptimizeEvent( VillagerOptimizeEvent optimizeEvent = new VillagerOptimizeEvent(
wrapped, wVillager,
OptimizationType.NAMETAG, OptimizationType.NAMETAG,
player, player,
event.isAsynchronous() event.isAsynchronous()
@ -99,49 +106,47 @@ public class OptimizeByNametag extends VillagerOptimizerModule implements Listen
if (!optimizeEvent.callEvent()) return; if (!optimizeEvent.callEvent()) return;
wrapped.setOptimizationType(optimizeEvent.getOptimizationType()); if (!consume_nametag) {
wrapped.saveOptimizeTime(); event.setCancelled(true);
villager.customName(newVillagerName);
if (!consume_nametag && player.getGameMode() == GameMode.SURVIVAL) {
player.getInventory().addItem(usedItem.asOne());
} }
wVillager.setOptimizationType(optimizeEvent.getOptimizationType());
wVillager.saveOptimizeTime();
if (notify_player) { if (notify_player) {
VillagerOptimizer.getLang(player.locale()).nametag_optimize_success VillagerOptimizer.getLang(player.locale()).nametag_optimize_success
.forEach(line -> KyoriUtil.sendMessage(player, line)); .forEach(line -> KyoriUtil.sendMessage(player, line));
} }
if (log_enabled) { if (log_enabled) {
info(player.getName() + " optimized villager using nametag '" + nameTagPlainText + "' at " + VillagerOptimizer.getLog().info(Component.text(player.getName() +
LocationUtil.toString(wrapped.villager.getLocation())); " optimized villager by nametag '" + nameTagPlainText + "' at " +
GenericUtil.formatLocation(wVillager.villager().getLocation())).color(GenericUtil.COLOR));
} }
} else { } else {
event.setCancelled(true); event.setCancelled(true);
wrapped.sayNo(); wVillager.sayNo();
if (notify_player) { if (notify_player) {
final TextReplacementConfig timeLeft = TextReplacementConfig.builder() final TextReplacementConfig timeLeft = TextReplacementConfig.builder()
.matchLiteral("%time%") .matchLiteral("%time%")
.replacement(Util.formatDuration(Duration.ofMillis(wrapped.getOptimizeCooldownMillis(cooldown)))) .replacement(GenericUtil.formatDuration(Duration.ofMillis(wVillager.getOptimizeCooldownMillis(cooldown))))
.build(); .build();
VillagerOptimizer.getLang(player.locale()).nametag_on_optimize_cooldown VillagerOptimizer.getLang(player.locale()).nametag_on_optimize_cooldown
.forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(timeLeft))); .forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(timeLeft)));
} }
} }
} else { } else {
if (wrapped.isOptimized()) { if (wVillager.isOptimized()) {
VillagerUnoptimizeEvent unOptimizeEvent = new VillagerUnoptimizeEvent( VillagerUnoptimizeEvent unOptimizeEvent = new VillagerUnoptimizeEvent(
wrapped, wVillager,
player, player,
OptimizationType.NAMETAG, OptimizationType.NAMETAG,
event.isAsynchronous() event.isAsynchronous()
); );
if (!unOptimizeEvent.callEvent()) return; if (!unOptimizeEvent.callEvent()) return;
wrapped.setOptimizationType(OptimizationType.NONE); wVillager.setOptimizationType(OptimizationType.NONE);
if (!consume_nametag && player.getGameMode() == GameMode.SURVIVAL) {
player.getInventory().addItem(usedItem.asOne());
}
if (notify_player) { if (notify_player) {
VillagerOptimizer.getLang(player.locale()).nametag_unoptimize_success VillagerOptimizer.getLang(player.locale()).nametag_unoptimize_success
@ -149,8 +154,9 @@ public class OptimizeByNametag extends VillagerOptimizerModule implements Listen
} }
if (log_enabled) { if (log_enabled) {
info(player.getName() + " unoptimized villager using nametag '" + nameTagPlainText + "' at " + VillagerOptimizer.getLog().info(Component.text(player.getName() +
LocationUtil.toString(wrapped.villager.getLocation())); " unoptimized villager by nametag '" + nameTagPlainText + "' at " +
GenericUtil.formatLocation(wVillager.villager().getLocation())).color(GenericUtil.COLOR));
} }
} }
} }

View File

@ -1,15 +1,18 @@
package me.xginko.villageroptimizer.modules.optimization; package me.xginko.villageroptimizer.modules.optimization;
import com.tcoded.folialib.impl.ServerImplementation;
import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.VillagerOptimizer; import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.struct.enums.OptimizationType; import me.xginko.villageroptimizer.WrappedVillager;
import me.xginko.villageroptimizer.struct.enums.Permissions; import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.enums.OptimizationType;
import me.xginko.villageroptimizer.enums.Permissions;
import me.xginko.villageroptimizer.events.VillagerOptimizeEvent; import me.xginko.villageroptimizer.events.VillagerOptimizeEvent;
import me.xginko.villageroptimizer.events.VillagerUnoptimizeEvent; import me.xginko.villageroptimizer.events.VillagerUnoptimizeEvent;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule; import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import me.xginko.villageroptimizer.utils.GenericUtil;
import me.xginko.villageroptimizer.utils.KyoriUtil; import me.xginko.villageroptimizer.utils.KyoriUtil;
import me.xginko.villageroptimizer.utils.LocationUtil; import net.kyori.adventure.text.Component;
import me.xginko.villageroptimizer.utils.Util;
import me.xginko.villageroptimizer.wrapper.WrappedVillager;
import net.kyori.adventure.text.TextReplacementConfig; import net.kyori.adventure.text.TextReplacementConfig;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.block.Block; import org.bukkit.block.Block;
@ -27,37 +30,43 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
public class OptimizeByWorkstation extends VillagerOptimizerModule implements Listener { public class OptimizeByWorkstation implements VillagerOptimizerModule, Listener {
private final ServerImplementation scheduler;
private final VillagerCache villagerCache;
private final long cooldown_millis; private final long cooldown_millis;
private final double search_radius; private final double search_radius;
private final int check_duration_ticks; private final int check_duration_ticks;
private final boolean only_while_sneaking, log_enabled, notify_player; private final boolean only_while_sneaking, log_enabled, notify_player;
public OptimizeByWorkstation() { public OptimizeByWorkstation() {
super("optimization-methods.workstation-optimization"); shouldEnable();
config.master().addComment(configPath + ".enable", this.scheduler = VillagerOptimizer.getFoliaLib().getImpl();
this.villagerCache = VillagerOptimizer.getCache();
Config config = VillagerOptimizer.getConfiguration();
config.master().addComment("optimization-methods.workstation-optimization.enable",
"When enabled, villagers that have a job and have been traded with at least once will become optimized,\n" + "When enabled, villagers that have a job and have been traded with at least once will become optimized,\n" +
"if near their workstation. If the workstation is broken, the villager will become unoptimized again."); "if near their workstation. If the workstation is broken, the villager will become unoptimized again.");
this.check_duration_ticks = Math.max(config.getInt(configPath + ".check-linger-duration-ticks", 100, this.check_duration_ticks = Math.max(config.getInt("optimization-methods.workstation-optimization.check-linger-duration-ticks", 100,
"After a workstation has been placed, the plugin will wait for the configured amount of time in ticks\n" + "After a workstation has been placed, the plugin will wait for the configured amount of time in ticks\n" +
"for a villager to claim that workstation. Not recommended to go below 100 ticks."), 1); "for a villager to claim that workstation. Not recommended to go below 100 ticks."), 1);
this.search_radius = config.getDouble(configPath + ".search-radius-in-blocks", 2.0, this.search_radius = config.getDouble("optimization-methods.workstation-optimization.search-radius-in-blocks", 2.0,
"The radius in blocks a villager can be away from the player when he places a workstation.\n" + "The radius in blocks a villager can be away from the player when he places a workstation.\n" +
"The closest unoptimized villager to the player will be optimized."); "The closest unoptimized villager to the player will be optimized.");
this.cooldown_millis = TimeUnit.SECONDS.toMillis( this.cooldown_millis = TimeUnit.SECONDS.toMillis(
Math.max(1, config.getInt(configPath + ".optimize-cooldown-seconds", 600, Math.max(1, config.getInt("optimization-methods.workstation-optimization.optimize-cooldown-seconds", 600,
"Cooldown in seconds until a villager can be optimized again using a workstation.\n" + "Cooldown in seconds until a villager can be optimized again using a workstation.\n" +
"Here for configuration freedom. Recommended to leave as is to not enable any exploitable behavior."))); "Here for configuration freedom. Recommended to leave as is to not enable any exploitable behavior.")));
this.only_while_sneaking = config.getBoolean(configPath + ".only-when-sneaking", true, this.only_while_sneaking = config.getBoolean("optimization-methods.workstation-optimization.only-when-sneaking", true,
"Only optimize/unoptimize by workstation when player is sneaking during place or break. Useful for villager rolling."); "Only optimize/unoptimize by workstation when player is sneaking during place or break. Useful for villager rolling.");
this.notify_player = config.getBoolean(configPath + ".notify-player", true, this.notify_player = config.getBoolean("optimization-methods.workstation-optimization.notify-player", true,
"Sends players a message when they successfully optimized a villager."); "Sends players a message when they successfully optimized a villager.");
this.log_enabled = config.getBoolean(configPath + ".log", false); this.log_enabled = config.getBoolean("optimization-methods.workstation-optimization.log", false);
} }
@Override @Override
public void enable() { public void enable() {
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
plugin.getServer().getPluginManager().registerEvents(this, plugin); plugin.getServer().getPluginManager().registerEvents(this, plugin);
} }
@ -68,13 +77,13 @@ public class OptimizeByWorkstation extends VillagerOptimizerModule implements Li
@Override @Override
public boolean shouldEnable() { public boolean shouldEnable() {
return config.getBoolean(configPath + ".enable", false); return VillagerOptimizer.getConfiguration().getBoolean("optimization-methods.workstation-optimization.enable", false);
} }
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onBlockPlace(BlockPlaceEvent event) { private void onBlockPlace(BlockPlaceEvent event) {
final Block placed = event.getBlock(); final Block placed = event.getBlock();
final Villager.Profession workstationProfession = Util.getWorkstationProfession(placed.getType()); final Villager.Profession workstationProfession = GenericUtil.getWorkstationProfession(placed.getType());
if (workstationProfession == null) return; if (workstationProfession == null) return;
final Player player = event.getPlayer(); final Player player = event.getPlayer();
@ -85,33 +94,28 @@ public class OptimizeByWorkstation extends VillagerOptimizerModule implements Li
final AtomicBoolean taskComplete = new AtomicBoolean(); final AtomicBoolean taskComplete = new AtomicBoolean();
final AtomicInteger taskAliveTicks = new AtomicInteger(); final AtomicInteger taskAliveTicks = new AtomicInteger();
scheduling.regionSpecificScheduler(workstationLoc).runAtFixedRate(repeatingTask -> { scheduler.runAtLocationTimer(workstationLoc, repeatingTask -> {
if (taskComplete.get() || taskAliveTicks.getAndAdd(10) > check_duration_ticks) { if (taskComplete.get() || taskAliveTicks.getAndAdd(10) > check_duration_ticks) {
repeatingTask.cancel(); repeatingTask.cancel();
return; return;
} }
for (Villager villager : workstationLoc.getNearbyEntitiesByType(Villager.class, search_radius)) { for (Villager villager : workstationLoc.getNearbyEntitiesByType(Villager.class, search_radius)) {
scheduling.entitySpecificScheduler(villager).run(() -> { if (villager.getProfession() != workstationProfession) continue;
if (villager.getProfession() != workstationProfession) return; WrappedVillager wrapped = villagerCache.getOrAdd(villager);
WrappedVillager wrapped = wrapperCache.get(villager, WrappedVillager::new); if (wrapped.getJobSite() == null) continue;
if (wrapped.getJobSite().distanceSquared(workstationLoc) > 1) continue;
Location jobSite = wrapped.getJobSite();
if (jobSite == null || jobSite.getWorld().getUID() != workstationLoc.getWorld().getUID()) return;
if (LocationUtil.relDistance3DSquared(jobSite, workstationLoc) > 1) return;
if (!wrapped.canOptimize(cooldown_millis) && !player.hasPermission(Permissions.Bypass.WORKSTATION_COOLDOWN.get())) { if (!wrapped.canOptimize(cooldown_millis) && !player.hasPermission(Permissions.Bypass.WORKSTATION_COOLDOWN.get())) {
wrapped.sayNo(); wrapped.sayNo();
if (notify_player) { if (notify_player) {
final TextReplacementConfig timeLeft = TextReplacementConfig.builder() final TextReplacementConfig timeLeft = TextReplacementConfig.builder()
.matchLiteral("%time%") .matchLiteral("%time%")
.replacement(Util.formatDuration(Duration.ofMillis(wrapped.getOptimizeCooldownMillis(cooldown_millis)))) .replacement(GenericUtil.formatDuration(Duration.ofMillis(wrapped.getOptimizeCooldownMillis(cooldown_millis))))
.build(); .build();
VillagerOptimizer.getLang(player.locale()).nametag_on_optimize_cooldown VillagerOptimizer.getLang(player.locale()).nametag_on_optimize_cooldown
.forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(timeLeft))); .forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(timeLeft)));
} }
taskComplete.set(true); taskComplete.set(true);
return; return;
} }
@ -131,23 +135,24 @@ public class OptimizeByWorkstation extends VillagerOptimizerModule implements Li
if (notify_player) { if (notify_player) {
final TextReplacementConfig vilProfession = TextReplacementConfig.builder() final TextReplacementConfig vilProfession = TextReplacementConfig.builder()
.matchLiteral("%vil_profession%") .matchLiteral("%vil_profession%")
.replacement(Util.toNiceString(wrapped.villager.getProfession())) .replacement(wrapped.villager().getProfession().toString().toLowerCase())
.build(); .build();
final TextReplacementConfig placedWorkstation = TextReplacementConfig.builder() final TextReplacementConfig placedWorkstation = TextReplacementConfig.builder()
.matchLiteral("%blocktype%") .matchLiteral("%blocktype%")
.replacement(Util.toNiceString(placed.getType())) .replacement(placed.getType().toString().toLowerCase())
.build(); .build();
VillagerOptimizer.getLang(player.locale()).workstation_optimize_success VillagerOptimizer.getLang(player.locale()).workstation_optimize_success
.forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(vilProfession).replaceText(placedWorkstation))); .forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(vilProfession).replaceText(placedWorkstation)));
} }
if (log_enabled) { if (log_enabled) {
info(player.getName() + " optimized villager using workstation " + Util.toNiceString(placed.getType()) + " at " + VillagerOptimizer.getLog().info(Component.text(player.getName() +
LocationUtil.toString(wrapped.villager.getLocation())); " optimized villager by workstation (" + placed.getType().toString().toLowerCase() + ") at " +
GenericUtil.formatLocation(wrapped.villager().getLocation())).color(GenericUtil.COLOR));
} }
taskComplete.set(true); taskComplete.set(true);
}, null); return;
} }
}, 1L, 10L); }, 1L, 10L);
} }
@ -155,7 +160,7 @@ public class OptimizeByWorkstation extends VillagerOptimizerModule implements Li
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onBlockBreak(BlockBreakEvent event) { private void onBlockBreak(BlockBreakEvent event) {
final Block broken = event.getBlock(); final Block broken = event.getBlock();
final Villager.Profession workstationProfession = Util.getWorkstationProfession(broken.getType()); final Villager.Profession workstationProfession = GenericUtil.getWorkstationProfession(broken.getType());
if (workstationProfession == null) return; if (workstationProfession == null) return;
final Player player = event.getPlayer(); final Player player = event.getPlayer();
@ -168,10 +173,10 @@ public class OptimizeByWorkstation extends VillagerOptimizerModule implements Li
for (Villager villager : workstationLoc.getNearbyEntitiesByType(Villager.class, search_radius)) { for (Villager villager : workstationLoc.getNearbyEntitiesByType(Villager.class, search_radius)) {
if (!villager.getProfession().equals(workstationProfession)) continue; if (!villager.getProfession().equals(workstationProfession)) continue;
final double distance = LocationUtil.relDistance3DSquared(villager.getLocation(), workstationLoc); final double distance = villager.getLocation().distanceSquared(workstationLoc);
if (distance >= closestDistance) continue; if (distance >= closestDistance) continue;
WrappedVillager wrapped = wrapperCache.get(villager, WrappedVillager::new); WrappedVillager wrapped = villagerCache.getOrAdd(villager);
if (wrapped.isOptimized()) { if (wrapped.isOptimized()) {
closestOptimized = wrapped; closestOptimized = wrapped;
@ -195,19 +200,20 @@ public class OptimizeByWorkstation extends VillagerOptimizerModule implements Li
if (notify_player) { if (notify_player) {
final TextReplacementConfig vilProfession = TextReplacementConfig.builder() final TextReplacementConfig vilProfession = TextReplacementConfig.builder()
.matchLiteral("%vil_profession%") .matchLiteral("%vil_profession%")
.replacement(Util.toNiceString(closestOptimized.villager.getProfession())) .replacement(closestOptimized.villager().getProfession().toString().toLowerCase())
.build(); .build();
final TextReplacementConfig brokenWorkstation = TextReplacementConfig.builder() final TextReplacementConfig brokenWorkstation = TextReplacementConfig.builder()
.matchLiteral("%blocktype%") .matchLiteral("%blocktype%")
.replacement(Util.toNiceString(broken.getType())) .replacement(broken.getType().toString().toLowerCase())
.build(); .build();
VillagerOptimizer.getLang(player.locale()).workstation_unoptimize_success VillagerOptimizer.getLang(player.locale()).workstation_unoptimize_success
.forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(vilProfession).replaceText(brokenWorkstation))); .forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(vilProfession).replaceText(brokenWorkstation)));
} }
if (log_enabled) { if (log_enabled) {
info(player.getName() + " unoptimized villager using workstation " + Util.toNiceString(broken.getType()) + " at " + VillagerOptimizer.getLog().info(Component.text(player.getName() +
LocationUtil.toString(closestOptimized.villager.getLocation())); " unoptimized villager by workstation (" + broken.getType().toString().toLowerCase() + ") at " +
GenericUtil.formatLocation(closestOptimized.villager().getLocation())).color(GenericUtil.COLOR));
} }
} }
} }

View File

@ -1,5 +0,0 @@
package me.xginko.villageroptimizer.struct;
public interface Disableable {
void disable();
}

View File

@ -1,5 +0,0 @@
package me.xginko.villageroptimizer.struct;
public interface Enableable {
void enable();
}

View File

@ -1,131 +0,0 @@
package me.xginko.villageroptimizer.struct.models;
import me.xginko.villageroptimizer.VillagerOptimizer;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Entity;
import java.util.Collection;
import java.util.Collections;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
public class BlockRegion2D {
private final UUID worldUID;
private final double halfSideLength, centerX, centerZ;
/**
* A square region on a minecraft world map.
*
* @param worldUID The UUID of the world this region is in.
* @param centerX The X-axis of the center location on the map.
* @param centerZ The Z-axis of the center location on the map.
* @param halfSideLength Half the length of the square's side. Acts like a radius would on circular regions.
*/
public BlockRegion2D(UUID worldUID, double centerX, double centerZ, double halfSideLength) {
this.worldUID = worldUID;
this.centerX = centerX;
this.centerZ = centerZ;
this.halfSideLength = halfSideLength;
}
/**
* Creates a square region on a minecraft world map.
*
* @param worldUID The UUID of the world this region is in.
* @param centerX The X-axis of the center location on the map.
* @param centerZ The Z-axis of the center location on the map.
* @param halfSideLength Half the length of the square's side. Acts like a radius would on circular regions.
*/
public static BlockRegion2D of(UUID worldUID, double centerX, double centerZ, double halfSideLength) {
return new BlockRegion2D(worldUID, centerX, centerZ, halfSideLength);
}
/**
* Creates a square region on a minecraft world map.
*
* @param world The world this region is in.
* @param centerX The X-axis of the center location on the map.
* @param centerZ The Z-axis of the center location on the map.
* @param halfSideLength Half the length of the square's side. Acts like a radius would on circular regions.
*/
public static BlockRegion2D of(World world, double centerX, double centerZ, double halfSideLength) {
return BlockRegion2D.of(world.getUID(), centerX, centerZ, halfSideLength);
}
public UUID getWorldUID() {
return this.worldUID;
}
public double getHalfSideLength() {
return this.halfSideLength;
}
public double getCenterX() {
return this.centerX;
}
public double getCenterZ() {
return this.centerZ;
}
public boolean contains(Location location) {
if (!location.getWorld().getUID().equals(this.worldUID)) {
return false;
}
return location.getX() >= this.centerX - this.halfSideLength
&& location.getX() <= this.centerX + this.halfSideLength
&& location.getZ() >= this.centerZ - this.halfSideLength
&& location.getZ() <= this.centerZ + this.halfSideLength;
}
public CompletableFuture<Collection<Entity>> getEntities() {
World world = Bukkit.getWorld(worldUID);
if (world == null) {
// Only way I can imagine this happening would be if the server is using a world manager plugin and unloads
// the world during an operation.
// Since these plugins are rather common though, we will silently complete with an empty set instead of exceptionally.
return CompletableFuture.completedFuture(Collections.emptySet());
}
CompletableFuture<Collection<Entity>> future = new CompletableFuture<>();
Location centerLoc = new Location(world, centerX, world.getMinHeight(), centerZ);
VillagerOptimizer.scheduling().regionSpecificScheduler(centerLoc).run(() -> future.complete(
centerLoc.getNearbyEntities(
halfSideLength,
Math.abs(world.getMaxHeight()) + Math.abs(world.getMinHeight()), // World y can be between -64 and 320, we want everything from top to bottom
halfSideLength
)));
return future;
}
@Override
public boolean equals(Object obj) {
if (null == obj || obj.getClass() != BlockRegion2D.class)
return false;
BlockRegion2D blockRegion2D = (BlockRegion2D)obj;
return blockRegion2D.worldUID.equals(this.worldUID) && blockRegion2D.centerX == this.centerX && blockRegion2D.centerZ == this.centerZ;
}
@Override
public int hashCode() {
return Objects.hash(this.worldUID, this.centerX, this.centerZ, this.halfSideLength);
}
@Override
public String toString() {
return "BlockRegion2D{" +
" radius(half side length)=" + halfSideLength +
", centerX=" + centerX +
", centerZ=" + centerZ +
", worldUID=" + worldUID +
"}";
}
}

View File

@ -1,349 +0,0 @@
package me.xginko.villageroptimizer.struct.models;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.jetbrains.annotations.NotNull;
import java.time.Duration;
import java.util.AbstractSet;
import java.util.Collection;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.TimeUnit;
public final class ExpiringSet<E> extends AbstractSet<E> implements Set<E> {
private final Cache<E, Object> cache;
private static final Object PRESENT = new Object(); // Dummy value to associate with an Object in the backing Cache
public ExpiringSet(long duration, TimeUnit unit) {
this.cache = Caffeine.newBuilder().expireAfterWrite(duration, unit).build();
}
public ExpiringSet(Duration duration) {
this.cache = Caffeine.newBuilder().expireAfterWrite(duration).build();
}
/**
* Returns the number of elements in this set (its cardinality). If this
* set contains more than {@code Integer.MAX_VALUE} elements, returns
* {@code Integer.MAX_VALUE}.
*
* @return the number of elements in this set (its cardinality)
*/
@Override
public int size() {
return this.cache.asMap().size();
}
/**
* Returns {@code true} if this set contains no elements.
*
* @return {@code true} if this set contains no elements
*/
@Override
public boolean isEmpty() {
return this.cache.asMap().isEmpty();
}
/**
* Returns {@code true} if this set contains the specified element.
* More formally, returns {@code true} if and only if this set
* contains an element {@code e} such that
* {@code Objects.equals(o, e)}.
*
* @param item element whose presence in this set is to be tested
* @return {@code true} if this set contains the specified element
* @throws ClassCastException if the type of the specified element
* is incompatible with this set
* (<a href="Collection.html#optional-restrictions">optional</a>)
* @throws NullPointerException if the specified element is null and this
* set does not permit null elements
* (<a href="Collection.html#optional-restrictions">optional</a>)
*/
@Override
public boolean contains(Object item) {
return this.cache.getIfPresent(item) != null;
}
/**
* Returns an iterator over the elements in this set. The elements are
* returned in no particular order (unless this set is an instance of some
* class that provides a guarantee).
*
* @return an iterator over the elements in this set
*/
@Override
public @NotNull Iterator<E> iterator() {
return this.cache.asMap().keySet().iterator();
}
/**
* Returns an array containing all of the elements in this set.
* If this set makes any guarantees as to what order its elements
* are returned by its iterator, this method must return the
* elements in the same order.
*
* <p>The returned array will be "safe" in that no references to it
* are maintained by this set. (In other words, this method must
* allocate a new array even if this set is backed by an array).
* The caller is thus free to modify the returned array.
*
* <p>This method acts as bridge between array-based and collection-based
* APIs.
*
* @return an array containing all the elements in this set
*/
@Override
public @NotNull Object @NotNull [] toArray() {
return this.cache.asMap().keySet().toArray();
}
/**
* Returns an array containing all of the elements in this set; the
* runtime type of the returned array is that of the specified array.
* If the set fits in the specified array, it is returned therein.
* Otherwise, a new array is allocated with the runtime type of the
* specified array and the size of this set.
*
* <p>If this set fits in the specified array with room to spare
* (i.e., the array has more elements than this set), the element in
* the array immediately following the end of the set is set to
* {@code null}. (This is useful in determining the length of this
* set <i>only</i> if the caller knows that this set does not contain
* any null elements.)
*
* <p>If this set makes any guarantees as to what order its elements
* are returned by its iterator, this method must return the elements
* in the same order.
*
* <p>Like the {@link #toArray()} method, this method acts as bridge between
* array-based and collection-based APIs. Further, this method allows
* precise control over the runtime type of the output array, and may,
* under certain circumstances, be used to save allocation costs.
*
* <p>Suppose {@code x} is a set known to contain only strings.
* The following code can be used to dump the set into a newly allocated
* array of {@code String}:
*
* <pre>
* String[] y = x.toArray(new String[0]);</pre>
* <p>
* Note that {@code toArray(new Object[0])} is identical in function to
* {@code toArray()}.
*
* @param a the array into which the elements of this set are to be
* stored, if it is big enough; otherwise, a new array of the same
* runtime type is allocated for this purpose.
* @return an array containing all the elements in this set
* @throws ArrayStoreException if the runtime type of the specified array
* is not a supertype of the runtime type of every element in this
* set
* @throws NullPointerException if the specified array is null
*/
@Override
public @NotNull <T> T @NotNull [] toArray(@NotNull T @NotNull [] a) {
return this.cache.asMap().keySet().toArray(a);
}
/**
* Adds the specified element to this set if it is not already present
* (optional operation). More formally, adds the specified element
* {@code e} to this set if the set contains no element {@code e2}
* such that
* {@code Objects.equals(e, e2)}.
* If this set already contains the element, the call leaves the set
* unchanged and returns {@code false}. In combination with the
* restriction on constructors, this ensures that sets never contain
* duplicate elements.
*
* <p>The stipulation above does not imply that sets must accept all
* elements; sets may refuse to add any particular element, including
* {@code null}, and throw an exception, as described in the
* specification for {@link Collection#add Collection.add}.
* Individual set implementations should clearly document any
* restrictions on the elements that they may contain.
*
* @param item element to be added to this set
* @return {@code true} if this set did not already contain the specified
* element
* @throws UnsupportedOperationException if the {@code add} operation
* is not supported by this set
* @throws ClassCastException if the class of the specified element
* prevents it from being added to this set
* @throws NullPointerException if the specified element is null and this
* set does not permit null elements
* @throws IllegalArgumentException if some property of the specified element
* prevents it from being added to this set
*/
public boolean add(E item) {
boolean containedBefore = contains(item);
this.cache.put(item, PRESENT);
return !containedBefore;
}
/**
* Removes the specified element from this set if it is present
* (optional operation). More formally, removes an element {@code e}
* such that
* {@code Objects.equals(o, e)}, if
* this set contains such an element. Returns {@code true} if this set
* contained the element (or equivalently, if this set changed as a
* result of the call). (This set will not contain the element once the
* call returns.)
*
* @param o object to be removed from this set, if present
* @return {@code true} if this set contained the specified element
* @throws ClassCastException if the type of the specified element
* is incompatible with this set
* (<a href="Collection.html#optional-restrictions">optional</a>)
* @throws NullPointerException if the specified element is null and this
* set does not permit null elements
* (<a href="Collection.html#optional-restrictions">optional</a>)
* @throws UnsupportedOperationException if the {@code remove} operation
* is not supported by this set
*/
@Override
public boolean remove(Object o) {
boolean present = contains(o);
this.cache.invalidate(o);
return present;
}
/**
* Returns {@code true} if this set contains all of the elements of the
* specified collection. If the specified collection is also a set, this
* method returns {@code true} if it is a <i>subset</i> of this set.
*
* @param c collection to be checked for containment in this set
* @return {@code true} if this set contains all of the elements of the
* specified collection
* @throws ClassCastException if the types of one or more elements
* in the specified collection are incompatible with this
* set
* (<a href="Collection.html#optional-restrictions">optional</a>)
* @throws NullPointerException if the specified collection contains one
* or more null elements and this set does not permit null
* elements
* (<a href="Collection.html#optional-restrictions">optional</a>),
* or if the specified collection is null
* @see #contains(Object)
*/
@Override
public boolean containsAll(@NotNull Collection<?> c) {
for (Object o : c) {
if (!contains(o)) {
return false;
}
}
return true;
}
/**
* Adds all of the elements in the specified collection to this set if
* they're not already present (optional operation). If the specified
* collection is also a set, the {@code addAll} operation effectively
* modifies this set so that its value is the <i>union</i> of the two
* sets. The behavior of this operation is undefined if the specified
* collection is modified while the operation is in progress.
*
* @param c collection containing elements to be added to this set
* @return {@code true} if this set changed as a result of the call
* @throws UnsupportedOperationException if the {@code addAll} operation
* is not supported by this set
* @throws ClassCastException if the class of an element of the
* specified collection prevents it from being added to this set
* @throws NullPointerException if the specified collection contains one
* or more null elements and this set does not permit null
* elements, or if the specified collection is null
* @throws IllegalArgumentException if some property of an element of the
* specified collection prevents it from being added to this set
* @see #add(Object)
*/
@Override
public boolean addAll(@NotNull Collection<? extends E> c) {
boolean changed = false;
for (E o : c) {
if (add(o)) {
changed = true;
}
}
return changed;
}
/**
* Retains only the elements in this set that are contained in the
* specified collection (optional operation). In other words, removes
* from this set all of its elements that are not contained in the
* specified collection. If the specified collection is also a set, this
* operation effectively modifies this set so that its value is the
* <i>intersection</i> of the two sets.
*
* @param c collection containing elements to be retained in this set
* @return {@code true} if this set changed as a result of the call
* @throws UnsupportedOperationException if the {@code retainAll} operation
* is not supported by this set
* @throws ClassCastException if the class of an element of this set
* is incompatible with the specified collection
* (<a href="Collection.html#optional-restrictions">optional</a>)
* @throws NullPointerException if this set contains a null element and the
* specified collection does not permit null elements
* (<a href="Collection.html#optional-restrictions">optional</a>),
* or if the specified collection is null
* @see #remove(Object)
*/
@Override
public boolean retainAll(@NotNull Collection<?> c) {
boolean changed = false;
for (E e : this.cache.asMap().keySet()) {
if (!c.contains(e) && remove(e)) {
changed = true;
}
}
return changed;
}
/**
* Removes from this set all of its elements that are contained in the
* specified collection (optional operation). If the specified
* collection is also a set, this operation effectively modifies this
* set so that its value is the <i>asymmetric set difference</i> of
* the two sets.
*
* @param c collection containing elements to be removed from this set
* @return {@code true} if this set changed as a result of the call
* @throws UnsupportedOperationException if the {@code removeAll} operation
* is not supported by this set
* @throws ClassCastException if the class of an element of this set
* is incompatible with the specified collection
* (<a href="Collection.html#optional-restrictions">optional</a>)
* @throws NullPointerException if this set contains a null element and the
* specified collection does not permit null elements
* (<a href="Collection.html#optional-restrictions">optional</a>),
* or if the specified collection is null
* @see #remove(Object)
* @see #contains(Object)
*/
@Override
public boolean removeAll(@NotNull Collection<?> c) {
boolean changed = false;
for (E e : this.cache.asMap().keySet()) {
if (remove(e)) {
changed = true;
}
}
return changed;
}
/**
* Removes all of the elements from this set (optional operation).
* The set will be empty after this call returns.
*
* @throws UnsupportedOperationException if the {@code clear} method
* is not supported by this set
*/
@Override
public void clear() {
this.cache.invalidateAll();
}
}

View File

@ -1,11 +1,13 @@
package me.xginko.villageroptimizer.logging; package me.xginko.villageroptimizer.utils;
import com.google.auto.service.AutoService;
import net.kyori.adventure.text.logger.slf4j.ComponentLogger; import net.kyori.adventure.text.logger.slf4j.ComponentLogger;
import net.kyori.adventure.text.logger.slf4j.ComponentLoggerProvider; import net.kyori.adventure.text.logger.slf4j.ComponentLoggerProvider;
import net.kyori.adventure.text.serializer.ansi.ANSIComponentSerializer; import net.kyori.adventure.text.serializer.ansi.ANSIComponentSerializer;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@AutoService(ComponentLoggerProvider.class)
@SuppressWarnings("UnstableApiUsage") @SuppressWarnings("UnstableApiUsage")
public final class ComponentLoggerProviderImpl implements ComponentLoggerProvider { public final class ComponentLoggerProviderImpl implements ComponentLoggerProvider {
private static final @NotNull ANSIComponentSerializer SERIALIZER = ANSIComponentSerializer.builder() private static final @NotNull ANSIComponentSerializer SERIALIZER = ANSIComponentSerializer.builder()

View File

@ -0,0 +1,87 @@
package me.xginko.villageroptimizer.utils;
import net.kyori.adventure.text.format.Style;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.format.TextDecoration;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.bukkit.Chunk;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.entity.Villager;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.time.Duration;
public class GenericUtil {
public static final @NotNull TextColor COLOR = TextColor.color(102,255,230);
public static final @NotNull Style STYLE = Style.style(COLOR, TextDecoration.BOLD);
public static final @NotNull PlainTextComponentSerializer plainTextSerializer = PlainTextComponentSerializer.plainText();
public static @NotNull String formatDuration(@NotNull Duration duration) {
if (duration.isNegative()) duration = duration.negated();
final int seconds = (int) (duration.getSeconds() % 60);
final int minutes = (int) (duration.toMinutes() % 60);
final int hours = (int) (duration.toHours() % 24);
if (hours > 0) {
return String.format("%02dh %02dm %02ds", hours, minutes, seconds);
} else if (minutes > 0) {
return String.format("%02dm %02ds", minutes, seconds);
} else {
return String.format("%02ds", seconds);
}
}
public static @NotNull String formatLocation(@NotNull Location location) {
return "[" + location.getWorld().getName() + "] x=" + location.getBlockX() + ", y=" + location.getBlockY() + ", z=" + location.getBlockZ();
}
private static boolean specificChunkLoadedMethodAvailable = true;
public static boolean isEntitiesLoaded(@NotNull Chunk chunk) {
if (!specificChunkLoadedMethodAvailable) {
return chunk.isLoaded();
}
try {
return chunk.isEntitiesLoaded();
} catch (NoSuchMethodError e) {
specificChunkLoadedMethodAvailable = false;
return chunk.isLoaded();
}
}
public static @Nullable Villager.Profession getWorkstationProfession(@NotNull Material workstation) {
switch (workstation) {
case BARREL:
return Villager.Profession.FISHERMAN;
case CARTOGRAPHY_TABLE:
return Villager.Profession.CARTOGRAPHER;
case SMOKER:
return Villager.Profession.BUTCHER;
case SMITHING_TABLE:
return Villager.Profession.TOOLSMITH;
case GRINDSTONE:
return Villager.Profession.WEAPONSMITH;
case BLAST_FURNACE:
return Villager.Profession.ARMORER;
case CAULDRON:
return Villager.Profession.LEATHERWORKER;
case BREWING_STAND:
return Villager.Profession.CLERIC;
case COMPOSTER:
return Villager.Profession.FARMER;
case FLETCHING_TABLE:
return Villager.Profession.FLETCHER;
case LOOM:
return Villager.Profession.SHEPHERD;
case LECTERN:
return Villager.Profession.LIBRARIAN;
case STONECUTTER:
return Villager.Profession.MASON;
default:
return null;
}
}
}

View File

@ -11,11 +11,11 @@ import java.util.Locale;
public class KyoriUtil { public class KyoriUtil {
public static void sendMessage(@NotNull CommandSender sender, @NotNull Component message) { public static void sendMessage(@NotNull CommandSender sender, @NotNull Component message) {
VillagerOptimizer.audiences().sender(sender).sendMessage(message); VillagerOptimizer.getAudiences().sender(sender).sendMessage(message);
} }
public static void sendActionBar(@NotNull CommandSender sender, @NotNull Component message) { public static void sendActionBar(@NotNull CommandSender sender, @NotNull Component message) {
VillagerOptimizer.audiences().sender(sender).sendActionBar(message); VillagerOptimizer.getAudiences().sender(sender).sendActionBar(message);
} }
public static @NotNull Component toUpperCase(@NotNull Component input, @NotNull Locale locale) { public static @NotNull Component toUpperCase(@NotNull Component input, @NotNull Locale locale) {

View File

@ -1,59 +0,0 @@
package me.xginko.villageroptimizer.utils;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.util.NumberConversions;
import org.jetbrains.annotations.NotNull;
public class LocationUtil {
public static @NotNull String toString(@NotNull Location location) {
return "[" + location.getWorld().getName() + "] x=" + location.getBlockX() + ", y=" + location.getBlockY() + ", z=" + location.getBlockZ();
}
public static double relDistance2DSquared(@NotNull Location from, @NotNull Location to) {
double toX = to.getX();
double toZ = to.getZ();
double fromX = from.getX();
double fromZ = from.getZ();
// Make sure distance is relative since one block in the nether equates to 8 in the overworld/end
if (to.getWorld().getEnvironment() != from.getWorld().getEnvironment()) {
if (from.getWorld().getEnvironment() == World.Environment.NETHER) {
fromX *= 8;
fromZ *= 8;
}
if (to.getWorld().getEnvironment() == World.Environment.NETHER) {
toX *= 8;
toZ *= 8;
}
}
return NumberConversions.square(toX - fromX) + NumberConversions.square(toZ - fromZ);
}
public static double relDistance3DSquared(@NotNull Location from, @NotNull Location to) {
double toY = to.getY();
double fromY = from.getY();
// Clamp Y levels the same way minecraft would for portal creation logic
if (fromY < to.getWorld().getMinHeight())
fromY = to.getWorld().getMinHeight();
if (fromY > to.getWorld().getMaxHeight())
fromY = to.getWorld().getMaxHeight();
if (toY < from.getWorld().getMinHeight())
toY = from.getWorld().getMinHeight();
if (toY > from.getWorld().getMaxHeight())
toY = from.getWorld().getMaxHeight();
return relDistance2DSquared(from, to) + NumberConversions.square(toY - fromY);
}
public static double relDistance2D(@NotNull Location from, @NotNull Location to) {
return Math.sqrt(relDistance2DSquared(from, to));
}
public static double relDistance3D(@NotNull Location from, @NotNull Location to) {
return Math.sqrt(relDistance3DSquared(from, to));
}
}

View File

@ -1,4 +1,4 @@
package me.xginko.villageroptimizer.logging; package me.xginko.villageroptimizer.utils;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TranslatableComponent; import net.kyori.adventure.text.TranslatableComponent;

View File

@ -1,97 +0,0 @@
package me.xginko.villageroptimizer.utils;
import com.cryptomorin.xseries.XMaterial;
import net.kyori.adventure.text.format.Style;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.format.TextDecoration;
import org.bukkit.Chunk;
import org.bukkit.Material;
import org.bukkit.entity.Villager;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.time.Duration;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
public class Util {
public static final @NotNull TextColor PL_COLOR;
public static final @NotNull Style PL_STYLE;
private static final @NotNull Map<Material, Villager.Profession> PROFESSION_MAP;
private static boolean canUseIsEntitiesLoaded;
static {
PL_COLOR = TextColor.color(102,255,230);
PL_STYLE = Style.style(PL_COLOR, TextDecoration.BOLD);
PROFESSION_MAP = new HashMap<>();
PROFESSION_MAP.put(XMaterial.LOOM.parseMaterial(), Villager.Profession.SHEPHERD);
PROFESSION_MAP.put(XMaterial.BARREL.parseMaterial(), Villager.Profession.FISHERMAN);
PROFESSION_MAP.put(XMaterial.SMOKER.parseMaterial(), Villager.Profession.BUTCHER);
PROFESSION_MAP.put(XMaterial.LECTERN.parseMaterial(), Villager.Profession.LIBRARIAN);
PROFESSION_MAP.put(XMaterial.CAULDRON.parseMaterial(), Villager.Profession.LEATHERWORKER);
PROFESSION_MAP.put(XMaterial.COMPOSTER.parseMaterial(), Villager.Profession.FARMER);
PROFESSION_MAP.put(XMaterial.GRINDSTONE.parseMaterial(), Villager.Profession.WEAPONSMITH);
PROFESSION_MAP.put(XMaterial.STONECUTTER.parseMaterial(), Villager.Profession.MASON);
PROFESSION_MAP.put(XMaterial.BREWING_STAND.parseMaterial(), Villager.Profession.CLERIC);
PROFESSION_MAP.put(XMaterial.BLAST_FURNACE.parseMaterial(), Villager.Profession.ARMORER);
PROFESSION_MAP.put(XMaterial.SMITHING_TABLE.parseMaterial(), Villager.Profession.TOOLSMITH);
PROFESSION_MAP.put(XMaterial.FLETCHING_TABLE.parseMaterial(), Villager.Profession.FLETCHER);
PROFESSION_MAP.put(XMaterial.CARTOGRAPHY_TABLE.parseMaterial(), Villager.Profession.CARTOGRAPHER);
try {
Chunk.class.getMethod("isEntitiesLoaded");
canUseIsEntitiesLoaded = true;
} catch (NoSuchMethodException e) {
canUseIsEntitiesLoaded = false;
}
}
public static @Nullable Villager.Profession getWorkstationProfession(@NotNull Material workstation) {
return PROFESSION_MAP.getOrDefault(workstation, null);
}
public static boolean isChunkLoaded(@NotNull Chunk chunk) {
return canUseIsEntitiesLoaded ? chunk.isEntitiesLoaded() : chunk.isLoaded();
}
public static @NotNull String formatDuration(@NotNull Duration duration) {
if (duration.isNegative()) duration = duration.negated();
final int seconds = (int) (duration.getSeconds() % 60);
final int minutes = (int) (duration.toMinutes() % 60);
final int hours = (int) (duration.toHours() % 24);
if (hours > 0) {
return String.format("%02dh %02dm %02ds", hours, minutes, seconds);
} else if (minutes > 0) {
return String.format("%02dm %02ds", minutes, seconds);
} else {
return String.format("%02ds", seconds);
}
}
public static @NotNull String toNiceString(@NotNull Object input) {
// Get name
String name;
if (input instanceof Enum<?>) {
name = ((Enum<?>) input).name();
} else {
name = input.toString();
}
// Turn something like "REDSTONE_TORCH" into "redstone torch"
String[] lowercaseWords = name.toLowerCase(Locale.ROOT).split("_");
// Capitalize first letter for each word
for (int i = 0; i < lowercaseWords.length; i++) {
String word = lowercaseWords[i];
lowercaseWords[i] = word.substring(0, 1).toUpperCase() + word.substring(1);
}
// return as nice string
return String.join(" ", lowercaseWords);
}
}

View File

@ -1,100 +0,0 @@
package me.xginko.villageroptimizer.wrapper;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.struct.enums.Keyring;
import me.xginko.villageroptimizer.struct.enums.OptimizationType;
import org.bukkit.entity.Villager;
import org.bukkit.persistence.PersistentDataContainer;
import org.jetbrains.annotations.NotNull;
public abstract class PDCWrapper {
public final Villager villager;
public final PersistentDataContainer dataContainer;
public PDCWrapper(Villager villager) {
this.villager = villager;
this.dataContainer = villager.getPersistentDataContainer();
}
public static PDCWrapper[] forVillager(Villager villager) {
if (VillagerOptimizer.config().support_other_plugins) {
return new PDCWrapper[]{new PDCWrapperVO(villager), new PDCWrapperAVL(villager)};
} else {
return new PDCWrapper[]{new PDCWrapperVO(villager)};
}
}
/**
* @return The namespace of the handler
*/
public abstract Keyring.Space getSpace();
/**
* @return True if the villager is optimized by plugin, otherwise false.
*/
public abstract boolean isOptimized();
/**
* @param cooldown_millis The configured cooldown in millis until the next optimization is allowed to occur.
* @return True if villager can be optimized again, otherwise false.
*/
public abstract boolean canOptimize(long cooldown_millis);
/**
* @param type OptimizationType the villager should be set to.
*/
public abstract void setOptimizationType(OptimizationType type);
/**
* @return The current OptimizationType of the villager.
*/
@NotNull
public abstract OptimizationType getOptimizationType();
/**
* Saves the system time when the villager was last optimized.
*/
public abstract void saveOptimizeTime();
/**
* For convenience so the remaining millis since the last stored optimize time
* can be easily calculated.
* This enables new configured cooldowns to instantly apply instead of them being persistent.
*
* @param cooldown_millis The configured cooldown in milliseconds you want to check against.
* @return The time left in millis until the villager can be optimized again.
*/
public abstract long getOptimizeCooldownMillis(long cooldown_millis);
/**
* Gets the time of the day in ticks when the entity was last restocked.
* This value is affected by /time set
* @return The time of the minecraft day (in ticks) when the villager was last restocked
*/
public abstract long getLastRestockFullTime();
/**
* Saves the time of when the entity was last restocked.
*/
public abstract void saveRestockTime();
/**
* @param cooldown_millis The configured cooldown in milliseconds you want to check against.
* @return Whether the villager can be leveled up or not with the checked milliseconds
*/
public abstract boolean canLevelUp(long cooldown_millis);
/**
* Saves the time of the in-game world when the entity was last leveled up.
*/
public abstract void saveLastLevelUp();
/**
* Here for convenience so the remaining millis since the last stored level-up time
* can be easily calculated.
*
* @return The time of the in-game world when the entity was last leveled up.
*/
public abstract long getLevelCooldownMillis(long cooldown_millis);
}

View File

@ -1,131 +0,0 @@
package me.xginko.villageroptimizer.wrapper;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.struct.enums.Keyring;
import me.xginko.villageroptimizer.struct.enums.OptimizationType;
import org.bukkit.entity.Villager;
import org.bukkit.persistence.PersistentDataType;
import org.jetbrains.annotations.NotNull;
import java.util.concurrent.TimeUnit;
public final class PDCWrapperAVL extends PDCWrapper {
PDCWrapperAVL(@NotNull Villager villager) {
super(villager);
}
@Override
public Keyring.Space getSpace() {
return Keyring.Space.AntiVillagerLag;
}
@Override
public boolean isOptimized() {
return dataContainer.has(Keyring.AntiVillagerLag.OPTIMIZED_ANY.getKey(), PersistentDataType.STRING)
|| dataContainer.has(Keyring.AntiVillagerLag.OPTIMIZED_WORKSTATION.getKey(), PersistentDataType.STRING)
|| dataContainer.has(Keyring.AntiVillagerLag.OPTIMIZED_BLOCK.getKey(), PersistentDataType.STRING);
}
@Override
public boolean canOptimize(long cooldown_millis) {
return !dataContainer.has(Keyring.AntiVillagerLag.NEXT_OPTIMIZATION_SYSTIME_SECONDS.getKey(), PersistentDataType.LONG)
|| System.currentTimeMillis() > TimeUnit.SECONDS.toMillis(dataContainer.get(Keyring.AntiVillagerLag.NEXT_OPTIMIZATION_SYSTIME_SECONDS.getKey(), PersistentDataType.LONG));
}
@Override
public void setOptimizationType(OptimizationType type) {
VillagerOptimizer.scheduling().entitySpecificScheduler(villager).runAtFixedRate(setOptimization -> {
// Keep repeating task until villager is no longer trading with a player
if (villager.isTrading()) return;
if (type == OptimizationType.NONE) {
if (dataContainer.has(Keyring.AntiVillagerLag.OPTIMIZED_ANY.getKey(), PersistentDataType.STRING))
dataContainer.remove(Keyring.AntiVillagerLag.OPTIMIZED_ANY.getKey());
if (dataContainer.has(Keyring.AntiVillagerLag.OPTIMIZED_WORKSTATION.getKey(), PersistentDataType.STRING))
dataContainer.remove(Keyring.AntiVillagerLag.OPTIMIZED_WORKSTATION.getKey());
if (dataContainer.has(Keyring.AntiVillagerLag.OPTIMIZED_BLOCK.getKey(), PersistentDataType.STRING))
dataContainer.remove(Keyring.AntiVillagerLag.OPTIMIZED_BLOCK.getKey());
villager.setAware(true);
villager.setAI(true);
} else {
switch (type) {
case BLOCK:
dataContainer.set(Keyring.AntiVillagerLag.OPTIMIZED_BLOCK.getKey(), PersistentDataType.STRING, Keyring.AntiVillagerLag.OPTIMIZED_BLOCK.getKey().toString());
break;
case WORKSTATION:
dataContainer.set(Keyring.AntiVillagerLag.OPTIMIZED_WORKSTATION.getKey(), PersistentDataType.STRING, Keyring.AntiVillagerLag.OPTIMIZED_WORKSTATION.getKey().toString());
break;
case COMMAND:
case NAMETAG:
dataContainer.set(Keyring.AntiVillagerLag.OPTIMIZED_ANY.getKey(), PersistentDataType.STRING, "AVL");
break;
}
villager.setAware(false);
}
// End repeating task once logic is finished
setOptimization.cancel();
}, null, 1L, 20L);
}
@Override
public @NotNull OptimizationType getOptimizationType() {
if (dataContainer.has(Keyring.AntiVillagerLag.OPTIMIZED_BLOCK.getKey(), PersistentDataType.STRING)) {
return OptimizationType.BLOCK;
}
if (dataContainer.has(Keyring.AntiVillagerLag.OPTIMIZED_WORKSTATION.getKey(), PersistentDataType.STRING)) {
return OptimizationType.WORKSTATION;
}
if (dataContainer.has(Keyring.AntiVillagerLag.OPTIMIZED_ANY.getKey(), PersistentDataType.STRING)) {
return OptimizationType.COMMAND; // Best we can do
}
return OptimizationType.NONE;
}
@Override
public void saveOptimizeTime() {
// We do nothing here to not break stuff
}
@Override
public long getOptimizeCooldownMillis(long cooldown_millis) {
if (dataContainer.has(Keyring.AntiVillagerLag.NEXT_OPTIMIZATION_SYSTIME_SECONDS.getKey(), PersistentDataType.LONG)) {
return TimeUnit.SECONDS.toMillis(dataContainer.get(Keyring.AntiVillagerLag.NEXT_OPTIMIZATION_SYSTIME_SECONDS.getKey(), PersistentDataType.LONG) - System.currentTimeMillis());
}
return cooldown_millis;
}
@Override
public long getLastRestockFullTime() {
if (dataContainer.has(Keyring.AntiVillagerLag.LAST_RESTOCK_WORLD_FULLTIME.getKey(), PersistentDataType.LONG)) {
return dataContainer.get(Keyring.AntiVillagerLag.LAST_RESTOCK_WORLD_FULLTIME.getKey(), PersistentDataType.LONG);
}
return 0L;
}
@Override
public void saveRestockTime() {
dataContainer.set(Keyring.AntiVillagerLag.LAST_RESTOCK_WORLD_FULLTIME.getKey(), PersistentDataType.LONG, villager.getWorld().getFullTime());
}
@Override
public boolean canLevelUp(long cooldown_millis) {
return !dataContainer.has(Keyring.AntiVillagerLag.NEXT_LEVELUP_SYSTIME_SECONDS.getKey(), PersistentDataType.LONG)
|| System.currentTimeMillis() > TimeUnit.SECONDS.toMillis(dataContainer.get(Keyring.AntiVillagerLag.NEXT_LEVELUP_SYSTIME_SECONDS.getKey(), PersistentDataType.LONG));
}
@Override
public void saveLastLevelUp() {
// We do nothing here to not break stuff
}
@Override
public long getLevelCooldownMillis(long cooldown_millis) {
if (dataContainer.has(Keyring.AntiVillagerLag.NEXT_LEVELUP_SYSTIME_SECONDS.getKey(), PersistentDataType.LONG))
return System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(dataContainer.get(Keyring.AntiVillagerLag.NEXT_LEVELUP_SYSTIME_SECONDS.getKey(), PersistentDataType.LONG));
return cooldown_millis;
}
}

View File

@ -1,122 +0,0 @@
package me.xginko.villageroptimizer.wrapper;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.struct.enums.Keyring;
import me.xginko.villageroptimizer.struct.enums.OptimizationType;
import org.bukkit.entity.Villager;
import org.bukkit.persistence.PersistentDataType;
import org.jetbrains.annotations.NotNull;
public final class PDCWrapperVO extends PDCWrapper {
PDCWrapperVO(@NotNull Villager villager) {
super(villager);
}
@Override
public Keyring.Space getSpace() {
return Keyring.Space.VillagerOptimizer;
}
@Override
public boolean isOptimized() {
return dataContainer.has(Keyring.VillagerOptimizer.OPTIMIZATION_TYPE.getKey(), PersistentDataType.STRING);
}
@Override
public boolean canOptimize(long cooldown_millis) {
return System.currentTimeMillis() > getLastOptimize() + cooldown_millis;
}
@Override
public void setOptimizationType(OptimizationType type) {
VillagerOptimizer.scheduling().entitySpecificScheduler(villager).runAtFixedRate(setOptimization -> {
// Keep repeating task until villager is no longer trading with a player
if (villager.isTrading()) return;
if (type == OptimizationType.NONE) {
if (isOptimized())
dataContainer.remove(Keyring.VillagerOptimizer.OPTIMIZATION_TYPE.getKey());
villager.setAware(true);
villager.setAI(true);
} else {
dataContainer.set(Keyring.VillagerOptimizer.OPTIMIZATION_TYPE.getKey(), PersistentDataType.STRING, type.name());
villager.setAware(false);
}
// End repeating task once logic is finished
setOptimization.cancel();
}, null, 1L, 20L);
}
@Override
public @NotNull OptimizationType getOptimizationType() {
if (isOptimized()) {
return OptimizationType.valueOf(dataContainer.get(Keyring.VillagerOptimizer.OPTIMIZATION_TYPE.getKey(), PersistentDataType.STRING));
} else {
return OptimizationType.NONE;
}
}
@Override
public void saveOptimizeTime() {
dataContainer.set(Keyring.VillagerOptimizer.LAST_OPTIMIZE_SYSTIME_MILLIS.getKey(), PersistentDataType.LONG, System.currentTimeMillis());
}
/**
* @return The system time in millis when the villager was last optimized, 0L if the villager was never optimized.
*/
private long getLastOptimize() {
if (dataContainer.has(Keyring.VillagerOptimizer.LAST_OPTIMIZE_SYSTIME_MILLIS.getKey(), PersistentDataType.LONG)) {
return dataContainer.get(Keyring.VillagerOptimizer.LAST_OPTIMIZE_SYSTIME_MILLIS.getKey(), PersistentDataType.LONG);
}
return 0L;
}
@Override
public long getOptimizeCooldownMillis(long cooldown_millis) {
if (getLastOptimize() > 0L) {
return cooldown_millis - (System.currentTimeMillis() - getLastOptimize());
}
return cooldown_millis;
}
@Override
public long getLastRestockFullTime() {
if (dataContainer.has(Keyring.VillagerOptimizer.LAST_RESTOCK_WORLD_FULLTIME.getKey(), PersistentDataType.LONG)) {
return dataContainer.get(Keyring.VillagerOptimizer.LAST_RESTOCK_WORLD_FULLTIME.getKey(), PersistentDataType.LONG);
}
return 0L;
}
@Override
public void saveRestockTime() {
dataContainer.set(Keyring.VillagerOptimizer.LAST_RESTOCK_WORLD_FULLTIME.getKey(), PersistentDataType.LONG, villager.getWorld().getFullTime());
}
@Override
public boolean canLevelUp(long cooldown_millis) {
return System.currentTimeMillis() >= getLastLevelUpTime() + cooldown_millis;
}
@Override
public void saveLastLevelUp() {
dataContainer.set(Keyring.VillagerOptimizer.LAST_LEVELUP_SYSTIME_MILLIS.getKey(), PersistentDataType.LONG, System.currentTimeMillis());
}
/**
* @return The systime in millis when the entity was last leveled up.
*/
private long getLastLevelUpTime() {
if (dataContainer.has(Keyring.VillagerOptimizer.LAST_LEVELUP_SYSTIME_MILLIS.getKey(), PersistentDataType.LONG))
return dataContainer.get(Keyring.VillagerOptimizer.LAST_LEVELUP_SYSTIME_MILLIS.getKey(), PersistentDataType.LONG);
return 0L;
}
@Override
public long getLevelCooldownMillis(long cooldown_millis) {
if (dataContainer.has(Keyring.VillagerOptimizer.LAST_LEVELUP_SYSTIME_MILLIS.getKey(), PersistentDataType.LONG))
return System.currentTimeMillis() - (dataContainer.get(Keyring.VillagerOptimizer.LAST_LEVELUP_SYSTIME_MILLIS.getKey(), PersistentDataType.LONG) + cooldown_millis);
return cooldown_millis;
}
}

View File

@ -1,192 +0,0 @@
package me.xginko.villageroptimizer.wrapper;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.struct.enums.Keyring;
import me.xginko.villageroptimizer.struct.enums.OptimizationType;
import org.bukkit.Location;
import org.bukkit.Sound;
import org.bukkit.entity.Villager;
import org.bukkit.entity.memory.MemoryKey;
import org.bukkit.event.entity.VillagerReplenishTradeEvent;
import org.bukkit.inventory.MerchantRecipe;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class WrappedVillager extends PDCWrapper {
private final @NotNull PDCWrapper[] pdcWrappers;
public WrappedVillager(@NotNull Villager villager) {
super(villager);
this.pdcWrappers = PDCWrapper.forVillager(villager);
}
/**
* Returns a number between 0 and 24000
* is affected by /time set
*/
public long currentDayTimeTicks() {
return villager.getWorld().getTime();
}
/**
* Returns the tick time of the world
* is affected by /time set
*/
public long currentFullTimeTicks() {
return villager.getWorld().getFullTime();
}
/**
* Restock all trading recipes.
*/
public void restock() {
VillagerOptimizer.scheduling().entitySpecificScheduler(villager).run(() -> {
for (MerchantRecipe merchantRecipe : villager.getRecipes()) {
VillagerReplenishTradeEvent restockRecipeEvent = new VillagerReplenishTradeEvent(villager, merchantRecipe);
if (restockRecipeEvent.callEvent()) {
restockRecipeEvent.getRecipe().setUses(0);
}
}
}, null);
}
/**
* @return The level between 1-5 calculated from the villagers experience.
*/
public int calculateLevel() {
// https://minecraft.fandom.com/wiki/Trading#Mechanics
int vilEXP = villager.getVillagerExperience();
if (vilEXP >= 250) return 5;
if (vilEXP >= 150) return 4;
if (vilEXP >= 70) return 3;
if (vilEXP >= 10) return 2;
return 1;
}
/**
* @return true if the villager can lose its acquired profession by having its workstation destroyed.
*/
public boolean canLooseProfession() {
// A villager with a level of 1 and no trading experience is liable to lose its profession.
return villager.getVillagerLevel() <= 1 && villager.getVillagerExperience() <= 0;
}
public void sayNo() {
try {
villager.shakeHead();
} catch (NoSuchMethodError e) {
villager.getWorld().playSound(villager.getEyeLocation(), Sound.ENTITY_VILLAGER_NO, 1.0F, 1.0F);
}
}
public @Nullable Location getJobSite() {
return villager.getMemory(MemoryKey.JOB_SITE);
}
@Override
public Keyring.Space getSpace() {
return Keyring.Space.VillagerOptimizer;
}
@Override
public boolean isOptimized() {
for (PDCWrapper pdcWrapper : pdcWrappers) {
if (pdcWrapper.isOptimized()) {
return true;
}
}
return false;
}
@Override
public boolean canOptimize(long cooldown_millis) {
for (PDCWrapper pdcWrapper : pdcWrappers) {
if (!pdcWrapper.canOptimize(cooldown_millis)) {
return false;
}
}
return true;
}
@Override
public void setOptimizationType(OptimizationType type) {
for (PDCWrapper pdcWrapper : pdcWrappers) {
pdcWrapper.setOptimizationType(type);
}
}
@Override
public @NotNull OptimizationType getOptimizationType() {
OptimizationType result = OptimizationType.NONE;
for (PDCWrapper pdcWrapper : pdcWrappers) {
OptimizationType type = pdcWrapper.getOptimizationType();
if (type != OptimizationType.NONE) {
if (pdcWrapper.getSpace() == Keyring.Space.VillagerOptimizer) {
return type;
} else {
result = type;
}
}
}
return result;
}
@Override
public void saveOptimizeTime() {
for (PDCWrapper pdcWrapper : pdcWrappers) {
pdcWrapper.saveOptimizeTime();
}
}
@Override
public long getOptimizeCooldownMillis(long cooldown_millis) {
long cooldown = 0L;
for (PDCWrapper pdcWrapper : pdcWrappers) {
cooldown = Math.max(cooldown, pdcWrapper.getOptimizeCooldownMillis(cooldown_millis));
}
return cooldown;
}
@Override
public long getLastRestockFullTime() {
long cooldown = 0L;
for (PDCWrapper pdcWrapper : pdcWrappers) {
cooldown = Math.max(cooldown, pdcWrapper.getLastRestockFullTime());
}
return cooldown;
}
@Override
public void saveRestockTime() {
for (PDCWrapper pdcWrapper : pdcWrappers) {
pdcWrapper.saveRestockTime();
}
}
@Override
public boolean canLevelUp(long cooldown_millis) {
for (PDCWrapper pdcWrapper : pdcWrappers) {
if (!pdcWrapper.canLevelUp(cooldown_millis)) {
return false;
}
}
return true;
}
@Override
public void saveLastLevelUp() {
for (PDCWrapper pdcWrapper : pdcWrappers) {
pdcWrapper.saveLastLevelUp();
}
}
@Override
public long getLevelCooldownMillis(long cooldown_millis) {
long cooldown = cooldown_millis;
for (PDCWrapper pdcWrapper : pdcWrappers) {
cooldown = Math.max(cooldown, pdcWrapper.getLevelCooldownMillis(cooldown_millis));
}
return cooldown;
}
}

View File

@ -1 +1 @@
me.xginko.villageroptimizer.logging.ComponentLoggerProviderImpl me.xginko.villageroptimizer.utils.ComponentLoggerProviderImpl

View File

@ -1,44 +0,0 @@
messages:
no-permission: <red>이 명령을 사용할 권한이 없습니다.
trades-restocked:
- <green>모든 거래가 재입고되었습니다! 다음 재입고까지 %time% 남았습니다.
optimize-to-trade:
- <red>거래하기 전에 이 주민을 최적화해야 합니다.
villager-leveling-up:
- <yellow>주민이 현재 레벨 업 중입니다! %time% 후에 주민을 다시 사용할 수 있습니다.
nametag:
optimize-success:
- <green>네임태그을 사용하여 주민을 성공적으로 최적화했습니다.
optimize-on-cooldown:
- <gray>%time% 후에 이 주민을 다시 최적화할 수 있습니다.
unoptimize-success:
- <green>네임택을 제거하여 주민을 성공적으로 최적화 해제했습니다.
block:
optimize-success:
- <green>%blocktype% 블록을 사용하여 %vil_profession% 주민을 성공적으로 최적화했습니다.
optimize-on-cooldown:
- <gray>%time% 후에 이 주민을 다시 최적화할 수 있습니다.
unoptimize-success:
- <green>%blocktype% 블록을 제거하여 %vil_profession% 주민을 성공적으로 최적화 해제했습니다.
workstation:
optimize-success:
- <green>%blocktype% 작업대 블록을 사용하여 %vil_profession% 주민을 성공적으로 최적화했습니다.
optimize-on-cooldown:
- <gray>%time% 후에 이 주민을 다시 최적화할 수 있습니다.
unoptimize-success:
- <green>%blocktype% 작업대 블록을 제거하여 주민을 성공적으로 최적화 해제했습니다.
command:
optimize-success:
- <green>%radius% 블록 반경 내 %amount% 개의 주민을 성공적으로 최적화했습니다.
radius-limit-exceed:
- <red>입력한 반경이 %distance% 블록의 제한을 초과합니다.
optimize-fail:
- <gray>%amount% 개의 주민이 최근에 이미 최적화되어 최적화할 수 없습니다.
unoptimize-success:
- <green>%radius% 블록 반경 내 %amount% 개의 주민을 성공적으로 최적화 해제했습니다.
specify-radius:
- <red>반경을 지정하세요.
radius-invalid:
- <red>입력한 반경이 유효한 숫자가 아닙니다. 다시 시도하세요.
no-villagers-nearby:
- <gray>%radius% 블록 반경 내에서 사용 중인 주민을 찾을 수 없습니다.

View File

@ -1,44 +0,0 @@
messages:
no-permission: <red>您没有使用此命令的权限。
trades-restocked:
- <green>所有交易已经重新补充!下次补货时间为 %time%。
optimize-to-trade:
- <red>在与村民交易之前,您需要优化该村民。
villager-leveling-up:
- <yellow>村民正在升级中!您可以在 %time% 后再次与村民交互。
nametag:
optimize-success:
- <green>成功使用名牌优化了村民。
optimize-on-cooldown:
- <gray>您需要等待 %time% 才能再次优化该村民。
unoptimize-success:
- <green>成功使用名牌取消了村民的优化状态。
block:
optimize-success:
- <green>%vil_profession% 村民成功使用 %blocktype% 方块进行了优化。
optimize-on-cooldown:
- <gray>您需要等待 %time% 才能再次优化该村民。
unoptimize-success:
- <green>成功移除了 %blocktype% 方块,取消了 %vil_profession% 村民的优化状态。
workstation:
optimize-success:
- <green>%vil_profession% 村民成功使用工作站方块 %blocktype% 进行了优化。
optimize-on-cooldown:
- <gray>您需要等待 %time% 才能再次优化该村民。
unoptimize-success:
- <green>成功移除了工作站方块 %blocktype%,取消了村民的优化状态。
command:
optimize-success:
- <green>成功优化了 %amount% 名村民,半径为 %radius% 方块。
radius-limit-exceed:
- <red>您输入的半径超过了 %distance% 方块的限制。
optimize-fail:
- <gray>%amount% 名村民因为最近已经被优化过而无法再次优化。
unoptimize-success:
- <green>成功取消了 %amount% 名村民的优化状态,半径为 %radius% 方块。
specify-radius:
- <red>请指定一个半径。
radius-invalid:
- <red>您输入的半径不是有效的数字。请重试。
no-villagers-nearby:
- <gray>在 %radius% 方块范围内找不到任何就业的村民。

View File

@ -5,8 +5,6 @@ authors: [ xGinko ]
description: ${project.description} description: ${project.description}
website: ${project.url} website: ${project.url}
api-version: '1.16' api-version: '1.16'
softdepend:
- AntiVillagerLag
folia-supported: true folia-supported: true
commands: commands: