Compare commits

..

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

59 changed files with 1727 additions and 3158 deletions

2
.github/FUNDING.yml vendored
View File

@ -1 +1 @@
ko_fi: xginko
custom: https://ko-fi.com/xginko

100
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">
<modelVersion>4.0.0</modelVersion>
<groupId>me.xginko</groupId>
<groupId>me.xginko.VillagerOptimizer</groupId>
<artifactId>VillagerOptimizer</artifactId>
<version>1.7.0</version>
<version>1.4.0</version>
<packaging>jar</packaging>
<name>VillagerOptimizer</name>
@ -14,7 +14,7 @@
<url>https://github.com/xGinko/VillagerOptimizer</url>
<properties>
<java.version>1.8</java.version>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
@ -23,7 +23,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.12.1</version>
<version>3.11.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
@ -41,6 +41,10 @@
</goals>
<configuration>
<relocations>
<relocation>
<pattern>com.tcoded.folialib</pattern>
<shadedPattern>me.xginko.villageroptimizer.libs.folialib</shadedPattern>
</relocation>
<relocation>
<pattern>com.github.benmanes.caffeine</pattern>
<shadedPattern>me.xginko.villageroptimizer.libs.caffeine</shadedPattern>
@ -49,38 +53,13 @@
<pattern>org.bstats</pattern>
<shadedPattern>me.xginko.villageroptimizer.libs.bstats</shadedPattern>
</relocation>
<relocation>
<pattern>net.kyori</pattern>
<shadedPattern>me.xginko.villageroptimizer.libs.kyori</shadedPattern>
</relocation>
<relocation>
<pattern>io.github.thatsmusic99.configurationmaster</pattern>
<shadedPattern>me.xginko.villageroptimizer.libs.configmaster</shadedPattern>
</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>
<filters>
<filter>
<artifact>*:*</artifact>
<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>module-info.class</exclude>
<exclude>META-INF/MANIFEST.MF</exclude>
<exclude>META-INF/LICENSE</exclude>
<exclude>META-INF/LICENSE.txt</exclude>
</excludes>
</filter>
</filters>
@ -112,8 +91,8 @@
<url>https://ci.pluginwiki.us/plugin/repository/everything/</url>
</repository>
<repository>
<id>morepaperlib-repo</id>
<url>https://mvn-repo.arim.space/lesser-gpl3/</url>
<id>folialib-repo</id>
<url>https://nexuslite.gcnt.net/repos/other/</url>
</repository>
</repositories>
@ -124,70 +103,51 @@
<version>1.20.4-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.23.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.reflections</groupId>
<artifactId>reflections</artifactId>
<version>0.10.2</version>
</dependency>
<!-- Adventure API for easier cross-version compatibility -->
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-platform-bukkit</artifactId>
<version>4.3.3</version>
</dependency>
<!-- Adventure MiniMessage for parsing fancy tags in lang files -->
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-minimessage</artifactId>
<version>4.17.0</version>
<version>4.15.0</version>
<scope>compile</scope>
</dependency>
<!-- Needed to actually display colors in ComponentLogger -->
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-ansi</artifactId>
<version>4.17.0</version>
</dependency>
<!-- Adventure ComponentLogger for colorful slf4j logging -->
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-logger-slf4j</artifactId>
<version>4.17.0</version>
<version>4.15.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-plain</artifactId>
<version>4.15.0</version>
<scope>compile</scope>
</dependency>
<!-- Bukkit bStats -->
<dependency>
<groupId>org.bstats</groupId>
<artifactId>bstats-bukkit</artifactId>
<version>3.0.2</version>
<scope>compile</scope>
</dependency>
<!-- Enhanced config.yml manager -->
<dependency>
<groupId>com.github.thatsmusic99</groupId>
<artifactId>ConfigurationMaster-API</artifactId>
<version>v2.0.0-rc.1</version>
<scope>compile</scope>
</dependency>
<!-- Fast Caching (Needs to be 2.9.3 for java 8 support) -->
<!-- Fast Caching -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.9.3</version>
<version>3.1.8</version>
<scope>compile</scope>
</dependency>
<!-- Folia Support -->
<dependency>
<groupId>space.arim.morepaperlib</groupId>
<artifactId>morepaperlib</artifactId>
<version>0.4.3</version>
</dependency>
<!-- Cross-Version Support -->
<dependency>
<groupId>com.github.cryptomorin</groupId>
<artifactId>XSeries</artifactId>
<version>11.2.1</version>
<groupId>com.tcoded</groupId>
<artifactId>FoliaLib</artifactId>
<version>0.3.1</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

View File

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

View File

@ -0,0 +1,350 @@
package me.xginko.villageroptimizer;
import me.xginko.villageroptimizer.enums.Keyring;
import me.xginko.villageroptimizer.enums.OptimizationType;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.Location;
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 @Nullable CachedJobSite cachedJobSite;
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 namespaces) {
return switch (namespaces) {
case VillagerOptimizer -> dataContainer.has(Keyring.VillagerOptimizer.OPTIMIZATION_TYPE.getKey(), PersistentDataType.STRING);
case AntiVillagerLag -> 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;
switch (type) {
case NAMETAG, COMMAND, BLOCK, WORKSTATION -> {
dataContainer.set(Keyring.VillagerOptimizer.OPTIMIZATION_TYPE.getKey(), PersistentDataType.STRING, type.name());
villager.setAware(false);
}
case 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);
}
}
// 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) {
return switch (namespaces) {
case VillagerOptimizer -> {
if (isOptimized(Keyring.Spaces.VillagerOptimizer)) {
yield OptimizationType.valueOf(dataContainer.get(Keyring.VillagerOptimizer.OPTIMIZATION_TYPE.getKey(), PersistentDataType.STRING));
}
yield OptimizationType.NONE;
}
case AntiVillagerLag -> {
if (dataContainer.has(Keyring.AntiVillagerLag.OPTIMIZED_BLOCK.getKey(), PersistentDataType.STRING)) {
yield OptimizationType.BLOCK;
}
if (dataContainer.has(Keyring.AntiVillagerLag.OPTIMIZED_WORKSTATION.getKey(), PersistentDataType.STRING)) {
yield OptimizationType.WORKSTATION;
}
if (dataContainer.has(Keyring.AntiVillagerLag.OPTIMIZED_ANY.getKey(), PersistentDataType.STRING)) {
yield OptimizationType.COMMAND; // Best we can do
}
yield 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 <= villager.getWorld().getFullTime();
}
/**
* 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, villager.getWorld().getFullTime());
}
/**
* @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 villager.getWorld().getFullTime() - (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;
}
/**
* @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 (villager.getWorld().getFullTime() < 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, villager.getWorld().getFullTime());
}
/**
* 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 villager.getWorld().getFullTime() - (dataContainer.get(Keyring.VillagerOptimizer.LAST_LEVELUP.getKey(), PersistentDataType.LONG) + cooldown_millis);
return cooldown_millis;
}
public void memorizeName(final Component customName) {
dataContainer.set(Keyring.VillagerOptimizer.LAST_OPTIMIZE_NAME.getKey(), PersistentDataType.STRING, MiniMessage.miniMessage().serialize(customName));
}
public @Nullable Component getMemorizedName() {
if (dataContainer.has(Keyring.VillagerOptimizer.LAST_OPTIMIZE_NAME.getKey(), PersistentDataType.STRING))
return MiniMessage.miniMessage().deserialize(dataContainer.get(Keyring.VillagerOptimizer.LAST_OPTIMIZE_NAME.getKey(), PersistentDataType.STRING));
return null;
}
public void forgetName() {
dataContainer.remove(Keyring.VillagerOptimizer.LAST_OPTIMIZE_NAME.getKey());
}
private static class CachedJobSite {
private @Nullable Location jobSite;
private long lastRefresh;
private CachedJobSite(Villager villager) {
this.jobSite = villager.getMemory(MemoryKey.JOB_SITE);
this.lastRefresh = System.currentTimeMillis();
}
private @Nullable Location getJobSite(Villager villager) {
final long now = System.currentTimeMillis();
if (now - lastRefresh > 1000L) {
this.jobSite = villager.getMemory(MemoryKey.JOB_SITE);
this.lastRefresh = now;
}
return jobSite;
}
}
public @Nullable Location getJobSite() {
if (cachedJobSite == null)
cachedJobSite = new CachedJobSite(villager);
return cachedJobSite.getJobSite(villager);
}
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;
}
}

View File

@ -1,36 +1,11 @@
package me.xginko.villageroptimizer.commands;
import net.kyori.adventure.text.TextComponent;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.TabCompleter;
import org.jetbrains.annotations.NotNull;
import org.bukkit.command.CommandSender;
import java.util.Arrays;
public abstract class SubCommand implements CommandExecutor, TabCompleter {
private final String label;
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;
}
public abstract class SubCommand {
public abstract String getLabel();
public abstract TextComponent getDescription();
public abstract TextComponent getSyntax();
public abstract void perform(CommandSender sender, String[] args);
}

View File

@ -1,62 +1,36 @@
package me.xginko.villageroptimizer.commands;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.struct.Disableable;
import me.xginko.villageroptimizer.struct.Enableable;
import org.bukkit.command.CommandException;
import me.xginko.villageroptimizer.commands.optimizevillagers.OptVillagersRadius;
import me.xginko.villageroptimizer.commands.unoptimizevillagers.UnOptVillagersRadius;
import me.xginko.villageroptimizer.commands.villageroptimizer.VillagerOptimizerCmd;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.PluginCommand;
import org.bukkit.command.CommandMap;
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.Collections;
import java.util.HashSet;
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<>();
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());
String label();
public final PluginCommand pluginCommand;
List<String> NO_TABCOMPLETES = Collections.emptyList();
List<String> RADIUS_TABCOMPLETES = List.of("5", "10", "25", "50");
protected VillagerOptimizerCommand(@NotNull String name) throws CommandException {
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.");
}
HashSet<VillagerOptimizerCommand> commands = new HashSet<>();
public static void reloadCommands() {
COMMANDS.forEach(VillagerOptimizerCommand::disable);
COMMANDS.clear();
static void reloadCommands() {
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
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())) {
if (clazz.isInterface() || Modifier.isAbstract(clazz.getModifiers())) continue;
commands.add(new VillagerOptimizerCmd());
commands.add(new OptVillagersRadius());
commands.add(new UnOptVillagersRadius());
try {
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());
commands.forEach(command -> plugin.getCommand(command.label()).setExecutor(command));
}
}

View File

@ -1,13 +1,14 @@
package me.xginko.villageroptimizer.commands.optimizevillagers;
import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.WrappedVillager;
import me.xginko.villageroptimizer.commands.VillagerOptimizerCommand;
import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.struct.enums.OptimizationType;
import me.xginko.villageroptimizer.struct.enums.Permissions;
import me.xginko.villageroptimizer.enums.OptimizationType;
import me.xginko.villageroptimizer.enums.permissions.Bypass;
import me.xginko.villageroptimizer.enums.permissions.Commands;
import me.xginko.villageroptimizer.events.VillagerOptimizeEvent;
import me.xginko.villageroptimizer.utils.KyoriUtil;
import me.xginko.villageroptimizer.wrapper.WrappedVillager;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextReplacementConfig;
import net.kyori.adventure.text.format.NamedTextColor;
@ -21,50 +22,46 @@ import org.bukkit.entity.Villager;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collections;
import java.util.List;
public class OptVillagersRadius extends VillagerOptimizerCommand {
public class OptVillagersRadius implements VillagerOptimizerCommand {
private final long cooldown;
private final int max_radius;
public OptVillagersRadius() {
super("optimizevillagers");
Config config = VillagerOptimizer.config();
Config config = VillagerOptimizer.getConfiguration();
this.max_radius = config.getInt("optimization-methods.commands.optimizevillagers.max-block-radius", 100);
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" +
"Here for configuration freedom. Recommended to leave as is to not enable any exploitable behavior.") * 1000L;
this.cooldown = config.getInt("optimization-methods.commands.optimizevillagers.cooldown-seconds", 600, """
Cooldown in seconds until a villager can be optimized again using the command.\s
Here for configuration freedom. Recommended to leave as is to not enable any exploitable behavior.""") * 1000L;
}
@Override
public @Nullable List<String> onTabComplete(
@NotNull CommandSender sender, @NotNull Command command, @NotNull String commandLabel, @NotNull String[] args
) {
return args.length == 1 ? RADIUS_SUGGESTIONS : Collections.emptyList();
public String label() {
return "optimizevillagers";
}
@Override
public boolean onCommand(
@NotNull CommandSender sender, @NotNull Command command, @NotNull String commandLabel, @NotNull String[] args
) {
if (!sender.hasPermission(Permissions.Commands.OPTIMIZE_RADIUS.get())) {
KyoriUtil.sendMessage(sender, VillagerOptimizer.getLang(sender).no_permission);
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @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(Commands.OPTIMIZE_RADIUS.get())) {
sender.sendMessage(VillagerOptimizer.getLang(sender).no_permission);
return true;
}
if (!(sender instanceof Player)) {
KyoriUtil.sendMessage(sender, Component.text("This command can only be executed by a player.")
if (!(sender instanceof Player player)) {
sender.sendMessage(Component.text("This command can only be executed by a player.")
.color(NamedTextColor.RED).decorate(TextDecoration.BOLD));
return true;
}
Player player = (Player) sender;
if (args.length != 1) {
VillagerOptimizer.getLang(player.locale()).command_specify_radius
.forEach(line -> KyoriUtil.sendMessage(sender, line));
VillagerOptimizer.getLang(player.locale()).command_specify_radius.forEach(player::sendMessage);
return true;
}
@ -74,8 +71,7 @@ public class OptVillagersRadius extends VillagerOptimizerCommand {
final int safeRadius = (int) Math.sqrt(specifiedRadius * specifiedRadius);
if (safeRadius == 0) {
VillagerOptimizer.getLang(player.locale()).command_radius_invalid
.forEach(line -> KyoriUtil.sendMessage(sender, line));
VillagerOptimizer.getLang(player.locale()).command_radius_invalid.forEach(player::sendMessage);
return true;
}
@ -84,14 +80,14 @@ public class OptVillagersRadius extends VillagerOptimizerCommand {
.matchLiteral("%distance%")
.replacement(Integer.toString(max_radius))
.build();
VillagerOptimizer.getLang(player.locale()).command_radius_limit_exceed
.forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(limit)));
VillagerOptimizer.getLang(player.locale()).command_radius_limit_exceed.forEach(line -> player.sendMessage(line.replaceText(limit)));
return true;
}
VillagerCache villagerCache = VillagerOptimizer.getCache();
int successCount = 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(Bypass.COMMAND_COOLDOWN.get());
for (Entity entity : player.getNearbyEntities(safeRadius, safeRadius, safeRadius)) {
if (!entity.getType().equals(EntityType.VILLAGER)) continue;
@ -99,7 +95,7 @@ public class OptVillagersRadius extends VillagerOptimizerCommand {
Villager.Profession profession = villager.getProfession();
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)) {
VillagerOptimizeEvent optimizeEvent = new VillagerOptimizeEvent(wVillager, OptimizationType.COMMAND, player);
@ -118,8 +114,7 @@ public class OptVillagersRadius extends VillagerOptimizerCommand {
.matchLiteral("%radius%")
.replacement(Integer.toString(safeRadius))
.build();
VillagerOptimizer.getLang(player.locale()).command_no_villagers_nearby
.forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(radius)));
VillagerOptimizer.getLang(player.locale()).command_no_villagers_nearby.forEach(line -> player.sendMessage(line.replaceText(radius)));
return true;
}
@ -132,20 +127,20 @@ public class OptVillagersRadius extends VillagerOptimizerCommand {
.matchLiteral("%radius%")
.replacement(Integer.toString(safeRadius))
.build();
VillagerOptimizer.getLang(player.locale()).command_optimize_success
.forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(success_amount).replaceText(radius)));
VillagerOptimizer.getLang(player.locale()).command_optimize_success.forEach(line -> player.sendMessage(line
.replaceText(success_amount)
.replaceText(radius)
));
}
if (failCount > 0) {
final TextReplacementConfig alreadyOptimized = TextReplacementConfig.builder()
.matchLiteral("%amount%")
.replacement(Integer.toString(failCount))
.build();
VillagerOptimizer.getLang(player.locale()).command_optimize_fail
.forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(alreadyOptimized)));
VillagerOptimizer.getLang(player.locale()).command_optimize_fail.forEach(line -> player.sendMessage(line.replaceText(alreadyOptimized)));
}
} catch (NumberFormatException e) {
VillagerOptimizer.getLang(player.locale()).command_radius_invalid
.forEach(line -> KyoriUtil.sendMessage(player, line));
VillagerOptimizer.getLang(player.locale()).command_radius_invalid.forEach(player::sendMessage);
}
return true;

View File

@ -1,12 +1,12 @@
package me.xginko.villageroptimizer.commands.unoptimizevillagers;
import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.WrappedVillager;
import me.xginko.villageroptimizer.commands.VillagerOptimizerCommand;
import me.xginko.villageroptimizer.struct.enums.OptimizationType;
import me.xginko.villageroptimizer.struct.enums.Permissions;
import me.xginko.villageroptimizer.enums.OptimizationType;
import me.xginko.villageroptimizer.enums.permissions.Commands;
import me.xginko.villageroptimizer.events.VillagerUnoptimizeEvent;
import me.xginko.villageroptimizer.utils.KyoriUtil;
import me.xginko.villageroptimizer.wrapper.WrappedVillager;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextReplacementConfig;
import net.kyori.adventure.text.format.NamedTextColor;
@ -20,46 +20,41 @@ import org.bukkit.entity.Villager;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collections;
import java.util.List;
public class UnOptVillagersRadius extends VillagerOptimizerCommand {
public class UnOptVillagersRadius implements VillagerOptimizerCommand {
private final int max_radius;
public UnOptVillagersRadius() {
super("unoptimizevillagers");
this.max_radius = VillagerOptimizer.config()
.getInt("optimization-methods.commands.unoptimizevillagers.max-block-radius", 100);
this.max_radius = VillagerOptimizer.getConfiguration().getInt("optimization-methods.commands.unoptimizevillagers.max-block-radius", 100);
}
@Override
public @Nullable List<String> onTabComplete(
@NotNull CommandSender sender, @NotNull Command command, @NotNull String commandLabel, @NotNull String[] args
) {
return args.length == 1 ? RADIUS_SUGGESTIONS : Collections.emptyList();
public String label() {
return "unoptimizevillagers";
}
@Override
public boolean onCommand(
@NotNull CommandSender sender, @NotNull Command command, @NotNull String commandLabel, @NotNull String[] args
) {
if (!sender.hasPermission(Permissions.Commands.UNOPTIMIZE_RADIUS.get())) {
KyoriUtil.sendMessage(sender, VillagerOptimizer.getLang(sender).no_permission);
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @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(Commands.UNOPTIMIZE_RADIUS.get())) {
sender.sendMessage(VillagerOptimizer.getLang(sender).no_permission);
return true;
}
if (!(sender instanceof Player)) {
KyoriUtil.sendMessage(sender, Component.text("This command can only be executed by a player.")
if (!(sender instanceof Player player)) {
sender.sendMessage(Component.text("This command can only be executed by a player.")
.color(NamedTextColor.RED).decorate(TextDecoration.BOLD));
return true;
}
Player player = (Player) sender;
if (args.length != 1) {
VillagerOptimizer.getLang(player.locale()).command_specify_radius
.forEach(line -> KyoriUtil.sendMessage(sender, line));
VillagerOptimizer.getLang(player.locale()).command_specify_radius.forEach(player::sendMessage);
return true;
}
@ -69,8 +64,7 @@ public class UnOptVillagersRadius extends VillagerOptimizerCommand {
final int safeRadius = (int) Math.sqrt(specifiedRadius * specifiedRadius);
if (safeRadius == 0) {
VillagerOptimizer.getLang(player.locale()).command_radius_invalid
.forEach(line -> KyoriUtil.sendMessage(sender, line));
VillagerOptimizer.getLang(player.locale()).command_radius_invalid.forEach(player::sendMessage);
return true;
}
@ -79,11 +73,11 @@ public class UnOptVillagersRadius extends VillagerOptimizerCommand {
.matchLiteral("%distance%")
.replacement(Integer.toString(max_radius))
.build();
VillagerOptimizer.getLang(player.locale()).command_radius_limit_exceed
.forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(limit)));
VillagerOptimizer.getLang(player.locale()).command_radius_limit_exceed.forEach(line -> player.sendMessage(line.replaceText(limit)));
return true;
}
VillagerCache villagerCache = VillagerOptimizer.getCache();
int successCount = 0;
for (Entity entity : player.getNearbyEntities(safeRadius, safeRadius, safeRadius)) {
@ -92,7 +86,7 @@ public class UnOptVillagersRadius extends VillagerOptimizerCommand {
Villager.Profession profession = villager.getProfession();
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()) {
VillagerUnoptimizeEvent unOptimizeEvent = new VillagerUnoptimizeEvent(wVillager, player, OptimizationType.COMMAND);
@ -108,8 +102,7 @@ public class UnOptVillagersRadius extends VillagerOptimizerCommand {
.matchLiteral("%radius%")
.replacement(Integer.toString(safeRadius))
.build();
VillagerOptimizer.getLang(player.locale()).command_no_villagers_nearby
.forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(radius)));
VillagerOptimizer.getLang(player.locale()).command_no_villagers_nearby.forEach(line -> player.sendMessage(line.replaceText(radius)));
} else {
final TextReplacementConfig success_amount = TextReplacementConfig.builder()
.matchLiteral("%amount%")
@ -119,12 +112,13 @@ public class UnOptVillagersRadius extends VillagerOptimizerCommand {
.matchLiteral("%radius%")
.replacement(Integer.toString(safeRadius))
.build();
VillagerOptimizer.getLang(player.locale()).command_unoptimize_success
.forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(success_amount).replaceText(radius)));
VillagerOptimizer.getLang(player.locale()).command_unoptimize_success.forEach(line -> player.sendMessage(line
.replaceText(success_amount)
.replaceText(radius)
));
}
} catch (NumberFormatException e) {
VillagerOptimizer.getLang(player.locale()).command_radius_invalid
.forEach(line -> KyoriUtil.sendMessage(player, line));
VillagerOptimizer.getLang(player.locale()).command_radius_invalid.forEach(player::sendMessage);
}
return true;

View File

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

View File

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

View File

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

View File

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

View File

@ -5,7 +5,6 @@ import me.xginko.villageroptimizer.VillagerOptimizer;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.time.Duration;
import java.util.List;
import java.util.Locale;
@ -13,36 +12,37 @@ public class Config {
private final @NotNull ConfigFile config;
public final @NotNull Locale default_lang;
public final @NotNull Duration cache_keep_time;
public final boolean auto_lang, support_other_plugins;
public final long cache_keep_time_seconds;
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
this.config = ConfigFile.loadConfig(new File(VillagerOptimizer.getInstance().getDataFolder(), "config.yml"));
this.config = ConfigFile.loadConfig(new File(pluginFolder, "config.yml"));
structureConfig();
this.default_lang = Locale.forLanguageTag(
getString("general.default-language", "en_us",
"The default language that will be used if auto-language is false\n" +
"or no matching language file was found.")
"The default language that will be used if auto-language is false or no matching language file was found.")
.replace("_", "-"));
this.auto_lang = getBoolean("general.auto-language", true,
"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,
"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,
"Enable if you have previously used AntiVillagerLag\n" +
"(https://www.spigotmc.org/resources/antivillagerlag.102949/).\n" +
"Tries to read pre-existing info like optimization state so players\n" +
"don't need to reoptimize their villagers.");
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.");
this.support_other_plugins = getBoolean("general.support-avl-villagers", false, """
Enable if you have previously used AntiVillagerLag (https://www.spigotmc.org/resources/antivillagerlag.102949/).\s
Tries to read pre-existing info like optimization state so players don't need to reoptimize their villagers.""");
}
public void saveConfig() {
try {
this.config.save();
} catch (Throwable throwable) {
VillagerOptimizer.logger().error("Failed to save config file!", throwable);
} catch (Exception e) {
VillagerOptimizer.getLog().error("Failed to save config file! - " + e.getLocalizedMessage());
}
}
@ -51,10 +51,11 @@ public class Config {
this.createTitledSection("General", "general");
this.createTitledSection("Optimization", "optimization-methods");
this.config.addDefault("optimization-methods.commands.unoptimizevillagers", null);
this.config.addComment("optimization-methods.commands",
"If you want to disable commands, negate the following permissions:\n" +
"villageroptimizer.cmd.optimize\n" +
"villageroptimizer.cmd.unoptimize");
this.config.addComment("optimization-methods.commands", """
If you want to disable commands, negate the following permissions:\s
villageroptimizer.cmd.optimize\s
villageroptimizer.cmd.unoptimize
""");
this.config.addDefault("optimization-methods.nametag-optimization.enable", true);
this.createTitledSection("Villager Chunk Limit", "villager-chunk-limit");
this.createTitledSection("Gameplay", "gameplay");
@ -118,13 +119,13 @@ public class Config {
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);
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);
return this.config.getList(path);
return this.config.getStringList(path);
}
}

View File

@ -2,15 +2,12 @@ package me.xginko.villageroptimizer.config;
import io.github.thatsmusic99.configurationmaster.api.ConfigFile;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.utils.KyoriUtil;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class LanguageCache {
@ -20,7 +17,6 @@ public class LanguageCache {
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,
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_specify_radius, command_radius_invalid, command_no_villagers_nearby,
trades_restocked, optimize_for_trading, villager_leveling_up;
@ -31,77 +27,83 @@ public class LanguageCache {
// Check if the lang folder has already been created
File parent = langYML.getParentFile();
if (!parent.exists() && !parent.mkdir())
VillagerOptimizer.logger().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
VillagerOptimizer.getLog().error("Failed to create lang directory.");
// Check if the file already exists and save the one from the plugins resources folder if it does not
if (!langYML.exists())
plugin.saveResource("lang/" + locale + ".yml", false);
// Finally, load the lang file with configmaster
plugin.saveResource("lang" + File.separator + locale + ".yml", false);
// Finally load the lang file with configmaster
this.lang = ConfigFile.loadConfig(langYML);
// General
this.no_permission = getTranslation("messages.no-permission",
"<red>You don't have permission to use this command.");
this.trades_restocked = getListTranslation("messages.trades-restocked",
"<green>All trades have been restocked! Next restock in %time%");
List.of("<green>All trades have been restocked! Next restock in %time%"));
this.optimize_for_trading = getListTranslation("messages.optimize-to-trade",
"<red>You need to optimize this villager before you can trade with it.");
List.of("<red>You need to optimize this villager before you can trade with it."));
this.villager_leveling_up = getListTranslation("messages.villager-leveling-up",
"<yellow>Villager is currently leveling up! You can use the villager again in %time%.");
List.of("<yellow>Villager is currently leveling up! You can use the villager again in %time%."));
// Nametag
this.nametag_optimize_success = getListTranslation("messages.nametag.optimize-success",
"<green>Successfully optimized villager by using a nametag.");
List.of("<green>Successfully optimized villager by using a nametag."));
this.nametag_on_optimize_cooldown = getListTranslation("messages.nametag.optimize-on-cooldown",
"<gray>You need to wait %time% until you can optimize this villager again.");
List.of("<gray>You need to wait %time% until you can optimize this villager again."));
this.nametag_unoptimize_success = getListTranslation("messages.nametag.unoptimize-success",
"<green>Successfully unoptimized villager by using a nametag.");
List.of("<green>Successfully unoptimized villager by using a nametag."));
// Block
this.block_optimize_success = getListTranslation("messages.block.optimize-success",
"<green>%villagertype% villager successfully optimized using block %blocktype%.");
List.of("<green>%villagertype% villager successfully optimized using block %blocktype%."));
this.block_on_optimize_cooldown = getListTranslation("messages.block.optimize-on-cooldown",
"<gray>You need to wait %time% until you can optimize this villager again.");
List.of("<gray>You need to wait %time% until you can optimize this villager again."));
this.block_unoptimize_success = getListTranslation("messages.block.unoptimize-success",
"<green>Successfully unoptimized %villagertype% villager by removing %blocktype%.");
List.of("<green>Successfully unoptimized %villagertype% villager by removing %blocktype%."));
// Workstation
this.workstation_optimize_success = getListTranslation("messages.workstation.optimize-success",
"<green>%villagertype% villager successfully optimized using workstation %blocktype%.");
List.of("<green>%villagertype% villager successfully optimized using workstation %workstation%."));
this.workstation_on_optimize_cooldown = getListTranslation("messages.workstation.optimize-on-cooldown",
"<gray>You need to wait %time% until you can optimize this villager again.");
List.of("<gray>You need to wait %time% until you can optimize this villager again."));
this.workstation_unoptimize_success = getListTranslation("messages.workstation.unoptimize-success",
"<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.");
List.of("<green>Successfully unoptimized %villagertype% villager by removing workstation block %workstation%."));
// Command
this.command_optimize_success = getListTranslation("messages.command.optimize-success",
"<green>Successfully optimized %amount% villager(s) in a radius of %radius% blocks.");
List.of("<green>Successfully optimized %amount% villager(s) in a radius of %radius% blocks."));
this.command_radius_limit_exceed = getListTranslation("messages.command.radius-limit-exceed",
"<red>The radius you entered exceeds the limit of %distance% blocks.");
List.of("<red>The radius you entered exceeds the limit of %distance% blocks."));
this.command_optimize_fail = getListTranslation("messages.command.optimize-fail",
"<gray>%amount% villagers couldn't be optimized because they have recently been optimized.");
List.of("<gray>%amount% villagers couldn't be optimized because they have recently been optimized."));
this.command_unoptimize_success = getListTranslation("messages.command.unoptimize-success",
"<green>Successfully unoptimized %amount% villager(s) in a radius of %radius% blocks.");
List.of("<green>Successfully unoptimized %amount% villager(s) in a radius of %radius% blocks."));
this.command_specify_radius = getListTranslation("messages.command.specify-radius",
"<red>Please specify a radius.");
List.of("<red>Please specify a radius."));
this.command_radius_invalid = getListTranslation("messages.command.radius-invalid",
"<red>The radius you entered is not a valid number. Try again.");
List.of("<red>The radius you entered is not a valid number. Try again."));
this.command_no_villagers_nearby = getListTranslation("messages.command.no-villagers-nearby",
"<gray>Couldn't find any employed villagers within a radius of %radius%.");
List.of("<gray>Couldn't find any employed villagers within a radius of %radius%."));
try {
this.lang.save();
} catch (Throwable throwable) {
VillagerOptimizer.logger().error("Failed to save language file: " + langYML.getName(), throwable);
} catch (Exception e) {
VillagerOptimizer.getLog().error("Failed to save language file: "+ langYML.getName() +" - " + e.getLocalizedMessage());
}
}
public @NotNull Component getTranslation(@NotNull String path, @NotNull String defaultTranslation) {
this.lang.addDefault(path, defaultTranslation);
return MiniMessage.miniMessage().deserialize(KyoriUtil.translateChatColor(this.lang.getString(path, defaultTranslation)));
return MiniMessage.miniMessage().deserialize(this.lang.getString(path, defaultTranslation));
}
public @NotNull List<Component> getListTranslation(@NotNull String path, @NotNull String... defaultTranslation) {
this.lang.addDefault(path, Arrays.asList(defaultTranslation));
return this.lang.getStringList(path).stream().map(KyoriUtil::translateChatColor).map(MiniMessage.miniMessage()::deserialize).collect(Collectors.toList());
public @NotNull Component getTranslation(@NotNull String path, @NotNull String defaultTranslation, @NotNull String comment) {
this.lang.addDefault(path, defaultTranslation, comment);
return MiniMessage.miniMessage().deserialize(this.lang.getString(path, defaultTranslation));
}
public @NotNull List<Component> getListTranslation(@NotNull String path, @NotNull List<String> defaultTranslation) {
this.lang.addDefault(path, defaultTranslation);
return this.lang.getStringList(path).stream().map(MiniMessage.miniMessage()::deserialize).toList();
}
public @NotNull List<Component> getListTranslation(@NotNull String path, @NotNull List<String> defaultTranslation, @NotNull String comment) {
this.lang.addDefault(path, defaultTranslation, comment);
return this.lang.getStringList(path).stream().map(MiniMessage.miniMessage()::deserialize).toList();
}
}

View File

@ -1,4 +1,4 @@
package me.xginko.villageroptimizer.struct.enums;
package me.xginko.villageroptimizer.enums;
import net.kyori.adventure.key.Namespaced;
import org.bukkit.Keyed;
@ -10,9 +10,9 @@ import org.jetbrains.annotations.NotNull;
import java.util.Locale;
public final class Keyring {
public class Keyring {
public enum Space implements Namespaced {
public enum Spaces implements Namespaced {
VillagerOptimizer("VillagerOptimizer"),
AntiVillagerLag("AntiVillagerLag");
@ -20,7 +20,7 @@ public final class Keyring {
@Pattern("[a-z0-9_\\-.]+")
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);
}
@ -43,21 +43,24 @@ public final class Keyring {
* @return a {@link NamespacedKey} that can be used to test for and read data stored by plugins
* from a {@link PersistentDataContainer}
*/
@SuppressWarnings("deprecation")
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 {
OPTIMIZATION_TYPE("optimization-type"),
LAST_OPTIMIZE_SYSTIME_MILLIS("last-optimize"),
LAST_LEVELUP_SYSTIME_MILLIS("last-levelup"),
LAST_RESTOCK_WORLD_FULLTIME("last-restock-full-time");
LAST_OPTIMIZE("last-optimize"),
LAST_LEVELUP("last-levelup"),
LAST_RESTOCK("last-restock"),
LAST_OPTIMIZE_NAME("last-optimize-name");
private final @NotNull NamespacedKey key;
@SuppressWarnings("deprecation")
VillagerOptimizer(@NotNull String key) {
this.key = Keyring.getKey(Space.VillagerOptimizer.namespace(), key);
this.key = new NamespacedKey(Spaces.VillagerOptimizer.namespace(), key);
}
@Override
@ -69,7 +72,7 @@ public final class Keyring {
public enum AntiVillagerLag implements Keyed {
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
OPTIMIZED_ANY("Marker"), // Returns STRING -> "AVL"
OPTIMIZED_BLOCK("disabledByBlock"), // Returns STRING -> key().toString()
@ -77,8 +80,9 @@ public final class Keyring {
private final @NotNull NamespacedKey key;
@SuppressWarnings("deprecation")
AntiVillagerLag(@NotNull String avlKey) {
this.key = Keyring.getKey(Space.AntiVillagerLag.namespace(), avlKey);
this.key = new NamespacedKey(Spaces.AntiVillagerLag.namespace(), avlKey);
}
@Override

View File

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

View File

@ -0,0 +1,28 @@
package me.xginko.villageroptimizer.enums.permissions;
import org.bukkit.permissions.Permission;
import org.bukkit.permissions.PermissionDefault;
public enum Bypass {
TRADE_PREVENTION(new Permission("villageroptimizer.bypass.tradeprevention",
"Permission to bypass unoptimized trade prevention", PermissionDefault.FALSE)),
RESTOCK_COOLDOWN(new Permission("villageroptimizer.bypass.restockcooldown",
"Permission to bypass restock cooldown on optimized villagers", PermissionDefault.FALSE)),
NAMETAG_COOLDOWN(new Permission("villageroptimizer.bypass.nametagcooldown",
"Permission to bypass Nametag optimization cooldown", PermissionDefault.FALSE)),
BLOCK_COOLDOWN(new Permission("villageroptimizer.bypass.blockcooldown",
"Permission to bypass Block optimization cooldown", PermissionDefault.FALSE)),
WORKSTATION_COOLDOWN(new Permission("villageroptimizer.bypass.workstationcooldown",
"Permission to bypass Workstation optimization cooldown", PermissionDefault.FALSE)),
COMMAND_COOLDOWN(new Permission("villageroptimizer.bypass.commandcooldown",
"Permission to bypass command optimization cooldown", PermissionDefault.FALSE));
private final Permission permission;
Bypass(Permission permission) {
this.permission = permission;
}
public Permission get() {
return permission;
}
}

View File

@ -0,0 +1,26 @@
package me.xginko.villageroptimizer.enums.permissions;
import org.bukkit.permissions.Permission;
import org.bukkit.permissions.PermissionDefault;
public enum Commands {
VERSION(new Permission("villageroptimizer.cmd.version",
"Permission get the plugin version", PermissionDefault.OP)),
RELOAD(new Permission("villageroptimizer.cmd.reload",
"Permission to reload the plugin config", PermissionDefault.OP)),
DISABLE(new Permission("villageroptimizer.cmd.disable",
"Permission to disable the plugin", PermissionDefault.OP)),
OPTIMIZE_RADIUS(new Permission("villageroptimizer.cmd.optimize",
"Permission to optimize villagers in a radius", PermissionDefault.TRUE)),
UNOPTIMIZE_RADIUS(new Permission("villageroptimizer.cmd.unoptimize",
"Permission to unoptimize villagers in a radius", PermissionDefault.TRUE));
private final Permission permission;
Commands(Permission permission) {
this.permission = permission;
}
public Permission get() {
return permission;
}
}

View File

@ -0,0 +1,22 @@
package me.xginko.villageroptimizer.enums.permissions;
import org.bukkit.permissions.Permission;
import org.bukkit.permissions.PermissionDefault;
public enum Optimize {
NAMETAG(new Permission("villageroptimizer.optimize.nametag",
"Permission to optimize / unoptimize using Nametags", PermissionDefault.TRUE)),
BLOCK(new Permission("villageroptimizer.optimize.block",
"Permission to optimize / unoptimize using Blocks", PermissionDefault.TRUE)),
WORKSTATION(new Permission("villageroptimizer.optimize.workstation",
"Permission to optimize / unoptimize using Workstations", PermissionDefault.TRUE));
private final Permission permission;
Optimize(Permission permission) {
this.permission = permission;
}
public Permission get() {
return permission;
}
}

View File

@ -1,7 +1,7 @@
package me.xginko.villageroptimizer.events;
import me.xginko.villageroptimizer.wrapper.WrappedVillager;
import me.xginko.villageroptimizer.struct.enums.OptimizationType;
import me.xginko.villageroptimizer.WrappedVillager;
import me.xginko.villageroptimizer.enums.OptimizationType;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.Event;
@ -17,12 +17,7 @@ public class VillagerOptimizeEvent extends Event implements Cancellable {
private final @Nullable Player whoOptimised;
private boolean isCancelled = false;
public VillagerOptimizeEvent(
@NotNull WrappedVillager wrappedVillager,
@NotNull OptimizationType optimizationType,
@Nullable Player whoOptimised,
boolean isAsync
) throws IllegalArgumentException {
public VillagerOptimizeEvent(@NotNull WrappedVillager wrappedVillager, @NotNull OptimizationType optimizationType, @Nullable Player whoOptimised, boolean isAsync) throws IllegalArgumentException {
super(isAsync);
this.wrappedVillager = wrappedVillager;
this.whoOptimised = whoOptimised;
@ -33,11 +28,7 @@ public class VillagerOptimizeEvent extends Event implements Cancellable {
}
}
public VillagerOptimizeEvent(
@NotNull WrappedVillager wrappedVillager,
@NotNull OptimizationType optimizationType,
@Nullable Player whoOptimised
) throws IllegalArgumentException {
public VillagerOptimizeEvent(@NotNull WrappedVillager wrappedVillager, @NotNull OptimizationType optimizationType, @Nullable Player whoOptimised) throws IllegalArgumentException {
this.wrappedVillager = wrappedVillager;
this.whoOptimised = whoOptimised;
if (optimizationType.equals(OptimizationType.NONE)) {

View File

@ -1,7 +1,7 @@
package me.xginko.villageroptimizer.events;
import me.xginko.villageroptimizer.wrapper.WrappedVillager;
import me.xginko.villageroptimizer.struct.enums.OptimizationType;
import me.xginko.villageroptimizer.WrappedVillager;
import me.xginko.villageroptimizer.enums.OptimizationType;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.Event;
@ -17,23 +17,14 @@ public class VillagerUnoptimizeEvent extends Event implements Cancellable {
private final @Nullable Player whoUnoptimized;
private boolean isCancelled = false;
public VillagerUnoptimizeEvent(
@NotNull WrappedVillager wrappedVillager,
@Nullable Player whoUnoptimized,
@NotNull OptimizationType unOptimizeType,
boolean isAsync
) {
public VillagerUnoptimizeEvent(@NotNull WrappedVillager wrappedVillager, @Nullable Player whoUnoptimized, @NotNull OptimizationType unOptimizeType, boolean isAsync) {
super(isAsync);
this.wrappedVillager = wrappedVillager;
this.whoUnoptimized = whoUnoptimized;
this.unOptimizeType = unOptimizeType;
}
public VillagerUnoptimizeEvent(
@NotNull WrappedVillager wrappedVillager,
@Nullable Player whoUnoptimized,
@NotNull OptimizationType unOptimizeType
) {
public VillagerUnoptimizeEvent(@NotNull WrappedVillager wrappedVillager, @Nullable Player whoUnoptimized, @NotNull OptimizationType unOptimizeType) {
this.wrappedVillager = wrappedVillager;
this.whoUnoptimized = whoUnoptimized;
this.unOptimizeType = unOptimizeType;

View File

@ -1,22 +0,0 @@
package me.xginko.villageroptimizer.logging;
import net.kyori.adventure.text.logger.slf4j.ComponentLogger;
import net.kyori.adventure.text.logger.slf4j.ComponentLoggerProvider;
import net.kyori.adventure.text.serializer.ansi.ANSIComponentSerializer;
import org.jetbrains.annotations.NotNull;
import org.slf4j.LoggerFactory;
@SuppressWarnings("UnstableApiUsage")
public final class ComponentLoggerProviderImpl implements ComponentLoggerProvider {
private static final @NotNull ANSIComponentSerializer SERIALIZER = ANSIComponentSerializer.builder()
.flattener(TranslatableMapper.FLATTENER)
.build();
@Override
public @NotNull ComponentLogger logger(
final @NotNull LoggerHelper helper,
final @NotNull String name
) {
return helper.delegating(LoggerFactory.getLogger(name), SERIALIZER::serialize);
}
}

View File

@ -1,45 +0,0 @@
package me.xginko.villageroptimizer.logging;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TranslatableComponent;
import net.kyori.adventure.text.flattener.ComponentFlattener;
import net.kyori.adventure.translation.GlobalTranslator;
import net.kyori.adventure.translation.TranslationRegistry;
import net.kyori.adventure.translation.Translator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Locale;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
public enum TranslatableMapper implements BiConsumer<TranslatableComponent, Consumer<Component>> {
INSTANCE;
public static final @NotNull ComponentFlattener FLATTENER = ComponentFlattener.basic().toBuilder()
.complexMapper(TranslatableComponent.class, TranslatableMapper.INSTANCE)
.build();
@Override
public void accept(
final @NotNull TranslatableComponent translatableComponent,
final @NotNull Consumer<Component> componentConsumer
) {
for (final Translator source : GlobalTranslator.translator().sources()) {
if (source instanceof TranslationRegistry && ((TranslationRegistry) source).contains(translatableComponent.key())) {
componentConsumer.accept(GlobalTranslator.render(translatableComponent, Locale.getDefault()));
return;
}
}
final @Nullable String fallback = translatableComponent.fallback();
if (fallback == null) {
return;
}
for (final Translator source : GlobalTranslator.translator().sources()) {
if (source instanceof TranslationRegistry && ((TranslationRegistry) source).contains(fallback)) {
componentConsumer.accept(GlobalTranslator.render(Component.translatable(fallback), Locale.getDefault()));
return;
}
}
}
}

View File

@ -0,0 +1,29 @@
package me.xginko.villageroptimizer.models;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
public final class ExpiringSet<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();
}
public void add(E item) {
this.cache.put(item, PRESENT);
}
public boolean contains(E item) {
return this.cache.getIfPresent(item) != null;
}
}

View File

@ -1,13 +1,17 @@
package me.xginko.villageroptimizer.modules;
import com.cryptomorin.xseries.XEntityType;
import me.xginko.villageroptimizer.struct.models.ExpiringSet;
import me.xginko.villageroptimizer.utils.LocationUtil;
import me.xginko.villageroptimizer.utils.Util;
import me.xginko.villageroptimizer.wrapper.WrappedVillager;
import com.tcoded.folialib.impl.ServerImplementation;
import com.tcoded.folialib.wrapper.task.WrappedTask;
import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.utils.CommonUtil;
import net.kyori.adventure.text.Component;
import org.bukkit.Chunk;
import org.bukkit.Server;
import org.bukkit.World;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Villager;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
@ -16,118 +20,91 @@ import org.bukkit.event.Listener;
import org.bukkit.event.entity.CreatureSpawnEvent;
import org.bukkit.event.player.PlayerInteractEntityEvent;
import org.jetbrains.annotations.NotNull;
import space.arim.morepaperlib.scheduling.ScheduledTask;
import java.time.Duration;
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.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 Set<Villager.Profession> profession_whitelist;
private final ExpiringSet<Chunk> checked_chunks;
private final long check_period;
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() {
super("villager-chunk-limit");
config.master().addComment(configPath + ".enable",
"Checks chunks for too many villagers and removes excess villagers based on priority.");
this.check_period = config.getInt(configPath + ".check-period-in-ticks", 600,
"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 larger delay is less resource intense but could become inefficient.");
this.skip_unloaded_chunks = config.getBoolean(configPath + ".skip-not-fully-loaded-chunks", true,
shouldEnable();
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.""");
this.check_period = config.getInt("villager-chunk-limit.check-period-in-ticks", 600, """
Check all loaded chunks every X ticks. 1 second = 20 ticks\s
A shorter delay in between checks is more efficient but is also more resource intense.\s
A larger delay is less resource intense but could become inefficient.""");
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.");
this.checked_chunks = new ExpiringSet<>(Duration.ofSeconds(
Math.max(1, config.getInt(configPath + ".chunk-check-cooldown-seconds", 5,
"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,
this.log_enabled = config.getBoolean("villager-chunk-limit.log-removals", true);
this.non_optimized_max_per_chunk = config.getInt("villager-chunk-limit.unoptimized.max-per-chunk", 20,
"The maximum amount of unoptimized villagers per chunk.");
this.non_optimized_removal_priority = config.getList(configPath + ".unoptimized.removal-priority", new ArrayList<>(defaults),
"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")
.stream()
.map(configuredProfession -> {
try {
return Villager.Profession.valueOf(configuredProfession);
} catch (IllegalArgumentException e) {
warn("(unoptimized) 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.toList());
this.optimized_max_per_chunk = config.getInt(configPath + ".optimized.max-per-chunk", 60,
this.non_optimized_removal_priority = config.getList("villager-chunk-limit.unoptimized.removal-priority", List.of(
"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.\s
Use enums from https://jd.papermc.io/paper/1.20/org/bukkit/entity/Villager.Profession.html"""
).stream().map(configuredProfession -> {
try {
return Villager.Profession.valueOf(configuredProfession);
} catch (IllegalArgumentException e) {
VillagerOptimizer.getLog().warn("(villager-chunk-limit.unoptimized) 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).toList();
this.optimized_max_per_chunk = config.getInt("villager-chunk-limit.optimized.max-per-chunk", 60,
"The maximum amount of optimized villagers per chunk.");
this.optimized_removal_priority = config.getList(configPath + ".optimized.removal-priority", new ArrayList<>(defaults))
.stream()
.map(configuredProfession -> {
try {
return Villager.Profession.valueOf(configuredProfession);
} catch (IllegalArgumentException e) {
warn("(optimized) 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.toList());
this.optimized_removal_priority = config.getList("villager-chunk-limit.optimized.removal-priority", List.of(
"NONE", "NITWIT", "SHEPHERD", "FISHERMAN", "BUTCHER", "CARTOGRAPHER", "LEATHERWORKER",
"FLETCHER", "MASON", "FARMER", "ARMORER", "TOOLSMITH", "WEAPONSMITH", "CLERIC", "LIBRARIAN"
)).stream().map(configuredProfession -> {
try {
return Villager.Profession.valueOf(configuredProfession);
} catch (IllegalArgumentException e) {
VillagerOptimizer.getLog().warn("(villager-chunk-limit.optimized) 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).toList();
}
@Override
public void enable() {
plugin.getServer().getPluginManager().registerEvents(this, plugin);
periodic_chunk_check = scheduling.globalRegionalScheduler().runAtFixedRate(this, check_period, check_period);
final VillagerOptimizer plugin = VillagerOptimizer.getInstance();
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 || CommonUtil.isEntitiesLoaded(chunk)) {
this.manageVillagerCount(chunk);
}
}
}
}, check_period, check_period);
}
@Override
public boolean shouldEnable() {
return config.getBoolean(configPath + ".enable", false);
return VillagerOptimizer.getConfiguration().getBoolean("villager-chunk-limit.enable", false);
}
@Override
@ -136,58 +113,33 @@ public class VillagerChunkLimit extends VillagerOptimizerModule implements Runna
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)
private void onCreatureSpawn(CreatureSpawnEvent event) {
if (event.getEntityType() == XEntityType.VILLAGER.get()) {
scheduling.regionSpecificScheduler(event.getLocation()).run(() -> {
manageVillagerCount(event.getEntity().getChunk());
});
if (event.getEntityType() == EntityType.VILLAGER) {
this.manageVillagerCount(event.getEntity().getChunk());
}
}
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
@EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
private void onInteract(PlayerInteractEntityEvent event) {
if (event.getRightClicked().getType() == XEntityType.VILLAGER.get()) {
scheduling.regionSpecificScheduler(event.getRightClicked().getLocation()).run(() -> {
manageVillagerCount(event.getRightClicked().getChunk());
});
if (event.getRightClicked().getType() == EntityType.VILLAGER) {
this.manageVillagerCount(event.getRightClicked().getChunk());
}
}
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
List<Villager> optimized_villagers = new ArrayList<>();
List<Villager> not_optimized_villagers = new ArrayList<>();
for (Entity entity : chunk.getEntities()) {
if (entity.getType() != XEntityType.VILLAGER.get()) continue;
Villager villager = (Villager) entity;
// 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);
} else {
not_optimized_villagers.add(villager);
if (entity.getType().equals(EntityType.VILLAGER)) {
Villager villager = (Villager) entity;
if (villagerCache.getOrAdd(villager).isOptimized()) {
optimized_villagers.add(villager);
} else {
not_optimized_villagers.add(villager);
}
}
}
@ -202,11 +154,14 @@ public class VillagerChunkLimit extends VillagerOptimizerModule implements Runna
// Remove prioritized villagers that are too many
for (int i = 0; i < not_optimized_villagers_too_many; i++) {
Villager villager = not_optimized_villagers.get(i);
scheduling.entitySpecificScheduler(villager).run(kill -> {
scheduler.runAtEntity(villager, kill -> {
villager.remove();
if (log_enabled) info("Removed unoptimized villager with profession '" +
Util.toNiceString(villager.getProfession()) + "' at " + LocationUtil.toString(villager.getLocation()));
}, null);
if (log_enabled) {
VillagerOptimizer.getLog().info(Component.text(
"Removed unoptimized villager with profession '" + villager.getProfession().name() + "' at " +
CommonUtil.formatLocation(villager.getLocation())).color(VillagerOptimizer.plugin_style.color()));
}
});
}
}
@ -221,11 +176,15 @@ public class VillagerChunkLimit extends VillagerOptimizerModule implements Runna
// Remove prioritized villagers that are too many
for (int i = 0; i < optimized_villagers_too_many; i++) {
Villager villager = optimized_villagers.get(i);
scheduling.entitySpecificScheduler(villager).run(kill -> {
scheduler.runAtEntity(villager, kill -> {
villager.remove();
if (log_enabled) info("Removed unoptimized villager with profession '" +
Util.toNiceString(villager.getProfession()) + "' at " + LocationUtil.toString(villager.getLocation()));
}, null);
if (log_enabled) {
VillagerOptimizer.getLog().info(Component.text("Removed optimized villager with profession '" +
villager.getProfession().name() + "' at " +
CommonUtil.formatLocation(villager.getLocation())).color(VillagerOptimizer.plugin_style.color()));
}
});
}
}
}

View File

@ -1,87 +1,43 @@
package me.xginko.villageroptimizer.modules;
import com.github.benmanes.caffeine.cache.Cache;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.struct.Disableable;
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 me.xginko.villageroptimizer.modules.gameplay.*;
import me.xginko.villageroptimizer.modules.optimization.OptimizeByBlock;
import me.xginko.villageroptimizer.modules.optimization.OptimizeByNametag;
import me.xginko.villageroptimizer.modules.optimization.OptimizeByWorkstation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
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());
public static final Set<VillagerOptimizerModule> ENABLED_MODULES = new HashSet<>();
void enable();
void disable();
boolean shouldEnable();
public abstract boolean shouldEnable();
HashSet<VillagerOptimizerModule> modules = new HashSet<>();
protected final VillagerOptimizer plugin;
protected final Config config;
protected final Cache<Villager, WrappedVillager> wrapperCache;
protected final GracefulScheduling scheduling;
public final String configPath;
private final String logFormat;
static void reloadModules() {
modules.forEach(VillagerOptimizerModule::disable);
modules.clear();
public VillagerOptimizerModule(String configPath) {
this.plugin = VillagerOptimizer.getInstance();
this.config = VillagerOptimizer.config();
this.wrapperCache = VillagerOptimizer.wrappers();
this.scheduling = VillagerOptimizer.scheduling();
this.configPath = configPath;
shouldEnable(); // Ensure enable option is always first
String[] paths = configPath.split("\\.");
if (paths.length <= 2) {
this.logFormat = "<" + configPath + "> {}";
} else {
this.logFormat = "<" + paths[paths.length - 2] + "." + paths[paths.length - 1] + "> {}";
}
}
modules.add(new OptimizeByNametag());
modules.add(new OptimizeByBlock());
modules.add(new OptimizeByWorkstation());
public static void reloadModules() {
ENABLED_MODULES.forEach(VillagerOptimizerModule::disable);
ENABLED_MODULES.clear();
modules.add(new EnableLeashingVillagers());
modules.add(new FixOptimisationAfterCure());
modules.add(new RestockOptimizedTrades());
modules.add(new LevelOptimizedProfession());
modules.add(new RenameOptimizedVillagers());
modules.add(new MakeVillagersSpawnAdult());
modules.add(new PreventUnoptimizedTrading());
modules.add(new PreventOptimizedTargeting());
modules.add(new PreventOptimizedDamage());
modules.add(new UnoptimizeOnJobLoose());
for (Class<?> clazz : MODULES_PACKAGE.get(Scanners.SubTypes.of(VillagerOptimizerModule.class).asClass())) {
if (clazz.isInterface() || Modifier.isAbstract(clazz.getModifiers())) continue;
modules.add(new VillagerChunkLimit());
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.");
modules.forEach(module -> {
if (module.shouldEnable()) module.enable();
});
}
}

View File

@ -1,11 +1,15 @@
package me.xginko.villageroptimizer.modules.gameplay;
import com.cryptomorin.xseries.XEntityType;
import com.cryptomorin.xseries.XMaterial;
import com.tcoded.folialib.impl.ServerImplementation;
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.utils.LocationUtil;
import me.xginko.villageroptimizer.wrapper.WrappedVillager;
import me.xginko.villageroptimizer.utils.CommonUtil;
import net.kyori.adventure.text.Component;
import org.bukkit.GameMode;
import org.bukkit.Material;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.entity.Villager;
import org.bukkit.event.EventHandler;
@ -16,21 +20,27 @@ import org.bukkit.event.entity.PlayerLeashEntityEvent;
import org.bukkit.event.player.PlayerInteractEntityEvent;
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;
public EnableLeashingVillagers() {
super("gameplay.villagers-can-be-leashed");
config.master().addComment(configPath + ".enable",
"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,
shouldEnable();
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.""");
this.only_optimized = config.getBoolean("gameplay.villagers-can-be-leashed.only-optimized", false,
"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
public void enable() {
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@ -41,22 +51,21 @@ public class EnableLeashingVillagers extends VillagerOptimizerModule implements
@Override
public boolean shouldEnable() {
return config.getBoolean(configPath + ".enable", false);
return VillagerOptimizer.getConfiguration().getBoolean("gameplay.villagers-can-be-leashed.enable", false);
}
@SuppressWarnings("deprecation")
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
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 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();
Villager villager = (Villager) event.getRightClicked();
if (villager.isLeashed()) return;
if (only_optimized && !wrapperCache.get(villager, WrappedVillager::new).isOptimized()) return;
event.setCancelled(true); // Cancel the event, so we don't interact with the villager
if (only_optimized && !villagerCache.getOrAdd(villager).isOptimized()) return;
// Call event for compatibility with other plugins, constructing non deprecated if available
PlayerLeashEntityEvent leashEvent;
@ -69,15 +78,16 @@ public class EnableLeashingVillagers extends VillagerOptimizerModule implements
// If canceled by any plugin, do nothing
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
if (!villager.setLeashHolder(player)) return;
if (player.getGameMode().equals(GameMode.SURVIVAL))
handItem.subtract(1); // Manually consume for survival players
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 " +
CommonUtil.formatLocation(villager.getLocation())).color(VillagerOptimizer.plugin_style.color()));
}
}, null);
});
}
}

View File

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

View File

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

View File

@ -1,7 +1,8 @@
package me.xginko.villageroptimizer.modules.gameplay;
import com.cryptomorin.xseries.XEntityType;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Villager;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
@ -9,19 +10,13 @@ import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.CreatureSpawnEvent;
public class MakeVillagersSpawnAdult extends VillagerOptimizerModule implements Listener {
public class MakeVillagersSpawnAdult implements VillagerOptimizerModule, Listener {
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.");
}
public MakeVillagersSpawnAdult() {}
@Override
public void enable() {
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@ -32,13 +27,17 @@ public class MakeVillagersSpawnAdult extends VillagerOptimizerModule implements
@Override
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.\s
This is to save some more resources as players don't have to keep unoptimized\s
villagers loaded because they have to wait for them to turn into adults before they can\s
optimize them.""");
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onVillagerSpawn(CreatureSpawnEvent event) {
if (event.getEntityType() == XEntityType.VILLAGER.get()) {
final Villager villager = (Villager) event.getEntity();
if (event.getEntityType() == EntityType.VILLAGER) {
Villager villager = (Villager) event.getEntity();
if (!villager.isAdult()) villager.setAdult();
}
}

View File

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

View File

@ -1,9 +1,11 @@
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.wrapper.WrappedVillager;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Mob;
import org.bukkit.entity.Villager;
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.EntityTargetEvent;
public class PreventOptimizedTargeting extends VillagerOptimizerModule implements Listener {
public class PreventOptimizedTargeting implements VillagerOptimizerModule, Listener {
private final VillagerCache villagerCache;
public PreventOptimizedTargeting() {
super("gameplay.prevent-entities-from-targeting-optimized");
config.master().addComment(configPath + ".enable",
"Prevents hostile entities from targeting optimized villagers.");
this.villagerCache = VillagerOptimizer.getCache();
}
@Override
public void enable() {
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@ -33,16 +36,18 @@ public class PreventOptimizedTargeting extends VillagerOptimizerModule implement
@Override
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)
private void onTarget(EntityTargetEvent event) {
final Entity target = event.getTarget();
// Yes, instanceof checks would look way more beautiful here but checking type is much faster
Entity target = event.getTarget();
if (
target != null
&& target.getType() == XEntityType.VILLAGER.get()
&& wrapperCache.get((Villager) target, WrappedVillager::new).isOptimized()
&& target.getType() == EntityType.VILLAGER
&& villagerCache.getOrAdd((Villager) target).isOptimized()
) {
event.setTarget(null);
event.setCancelled(true);
@ -50,12 +55,12 @@ public class PreventOptimizedTargeting extends VillagerOptimizerModule implement
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onEntityTargetVillager(com.destroystokyo.paper.event.entity.EntityPathfindEvent event) {
final Entity target = event.getTargetEntity();
private void onEntityTargetVillager(EntityPathfindEvent event) {
Entity target = event.getTargetEntity();
if (
target != null
&& target.getType() == XEntityType.VILLAGER.get()
&& wrapperCache.get((Villager) target, WrappedVillager::new).isOptimized()
&& target.getType() == EntityType.VILLAGER
&& villagerCache.getOrAdd((Villager) target).isOptimized()
) {
event.setCancelled(true);
}
@ -64,11 +69,11 @@ public class PreventOptimizedTargeting extends VillagerOptimizerModule implement
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onEntityAttackVillager(EntityDamageByEntityEvent event) {
if (
event.getEntityType() == XEntityType.VILLAGER.get()
&& event.getDamager() instanceof Mob
&& wrapperCache.get((Villager) event.getEntity(), WrappedVillager::new).isOptimized()
event.getEntityType() == EntityType.VILLAGER
&& event.getDamager() instanceof Mob attacker
&& villagerCache.getOrAdd((Villager) event.getEntity()).isOptimized()
) {
((Mob) event.getDamager()).setTarget(null);
attacker.setTarget(null);
}
}
}

View File

@ -1,10 +1,10 @@
package me.xginko.villageroptimizer.modules.gameplay;
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.Bypass;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import me.xginko.villageroptimizer.utils.KyoriUtil;
import me.xginko.villageroptimizer.wrapper.WrappedVillager;
import org.bukkit.entity.Player;
import org.bukkit.entity.Villager;
import org.bukkit.event.EventHandler;
@ -15,22 +15,26 @@ import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryType;
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;
public PreventUnoptimizedTrading() {
super("gameplay.prevent-trading-with-unoptimized");
config.master().addComment(configPath + ".enable",
"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" +
"Inventories can still be opened so players can move villagers around.");
this.notify_player = config.getBoolean(configPath + ".notify-player", true,
shouldEnable();
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.\s
Use this if you have a lot of villagers and therefore want to force your players to optimize them.\s
Inventories can still be opened so players can move villagers around.""");
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.");
}
@Override
public void enable() {
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@ -41,36 +45,40 @@ public class PreventUnoptimizedTrading extends VillagerOptimizerModule implement
@Override
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.NORMAL, ignoreCancelled = true)
private void onTradeOpen(TradeSelectEvent event) {
if (event.getInventory().getType() != InventoryType.MERCHANT) return;
if (event.getWhoClicked().hasPermission(Permissions.Bypass.TRADE_PREVENTION.get())) return;
if (!(event.getInventory().getHolder() instanceof Villager)) return;
if (wrapperCache.get((Villager) event.getInventory().getHolder(), WrappedVillager::new).isOptimized()) return;
if (event.getWhoClicked().hasPermission(Bypass.TRADE_PREVENTION.get())) return;
event.setCancelled(true);
if (notify_player) {
Player player = (Player) event.getWhoClicked();
VillagerOptimizer.getLang(player.locale()).optimize_for_trading.forEach(line -> KyoriUtil.sendMessage(player, line));
if (
event.getInventory().getType().equals(InventoryType.MERCHANT)
&& event.getInventory().getHolder() instanceof Villager villager
&& !villagerCache.getOrAdd(villager).isOptimized()
) {
event.setCancelled(true);
if (notify_player) {
Player player = (Player) event.getWhoClicked();
VillagerOptimizer.getLang(player.locale()).optimize_for_trading.forEach(player::sendMessage);
}
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
private void onInventoryClick(InventoryClickEvent event) {
if (event.getInventory().getType() != InventoryType.MERCHANT) return;
if (event.getWhoClicked().hasPermission(Permissions.Bypass.TRADE_PREVENTION.get())) return;
if (!(event.getInventory().getHolder() instanceof Villager)) return;
if (wrapperCache.get((Villager) event.getInventory().getHolder(), WrappedVillager::new).isOptimized()) return;
if (event.getWhoClicked().hasPermission(Bypass.TRADE_PREVENTION.get())) return;
event.setCancelled(true);
if (notify_player) {
Player player = (Player) event.getWhoClicked();
VillagerOptimizer.getLang(player.locale()).optimize_for_trading.forEach(line -> KyoriUtil.sendMessage(player, line));
if (
event.getInventory().getType().equals(InventoryType.MERCHANT)
&& event.getInventory().getHolder() instanceof Villager villager
&& !villagerCache.getOrAdd(villager).isOptimized()
) {
event.setCancelled(true);
if (notify_player) {
Player player = (Player) event.getWhoClicked();
VillagerOptimizer.getLang(player.locale()).optimize_for_trading.forEach(player::sendMessage);
}
}
}
}

View File

@ -0,0 +1,80 @@
package me.xginko.villageroptimizer.modules.gameplay;
import com.tcoded.folialib.impl.ServerImplementation;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.WrappedVillager;
import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.events.VillagerOptimizeEvent;
import me.xginko.villageroptimizer.events.VillagerUnoptimizeEvent;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
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;
public class RenameOptimizedVillagers implements VillagerOptimizerModule, Listener {
private final ServerImplementation scheduler;
private final Component optimized_name;
private final boolean overwrite_previous_name;
public RenameOptimizedVillagers() {
shouldEnable();
this.scheduler = VillagerOptimizer.getFoliaLib().getImpl();
Config config = VillagerOptimizer.getConfiguration();
config.master().addComment("gameplay.rename-optimized-villagers.enable", """
Will change a villager's name to the name configured below when they are optimized.\s
These names will be removed when unoptimized again if they were not changed in the meantime.""");
this.optimized_name = MiniMessage.miniMessage().deserialize(config.getString("gameplay.rename-optimized-villagers.optimized-name", "<green>Optimized",
"The name that will be used to mark optimized villagers. Uses MiniMessage format."));
this.overwrite_previous_name = config.getBoolean("gameplay.rename-optimized-villagers.overwrite-existing-name", false,
"If set to true, will rename even if the villager has already been named.");
}
@Override
public void enable() {
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@Override
public void disable() {
HandlerList.unregisterAll(this);
}
@Override
public boolean shouldEnable() {
return VillagerOptimizer.getConfiguration().getBoolean("gameplay.rename-optimized-villagers.enable", false);
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onOptimize(VillagerOptimizeEvent event) {
WrappedVillager wVillager = event.getWrappedVillager();
Villager villager = wVillager.villager();
if (overwrite_previous_name || villager.customName() == null) {
scheduler.runAtEntityLater(villager, () -> {
villager.customName(optimized_name);
wVillager.memorizeName(optimized_name);
}, 10L);
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onUnOptimize(VillagerUnoptimizeEvent event) {
WrappedVillager wVillager = event.getWrappedVillager();
Villager villager = wVillager.villager();
scheduler.runAtEntityLater(villager, () -> {
final Component currentName = villager.customName();
final Component memorizedName = wVillager.getMemorizedName();
if (currentName != null && currentName.equals(memorizedName))
villager.customName(null);
if (memorizedName != null)
wVillager.forgetName();
}, 10L);
}
}

View File

@ -1,14 +1,17 @@
package me.xginko.villageroptimizer.modules.gameplay;
import com.cryptomorin.xseries.XEntityType;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.enums.permissions.Bypass;
import me.xginko.villageroptimizer.WrappedVillager;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import me.xginko.villageroptimizer.struct.enums.Permissions;
import me.xginko.villageroptimizer.utils.KyoriUtil;
import me.xginko.villageroptimizer.utils.LocationUtil;
import me.xginko.villageroptimizer.utils.Util;
import me.xginko.villageroptimizer.wrapper.WrappedVillager;
import me.xginko.villageroptimizer.utils.CommonUtil;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextReplacementConfig;
import org.bukkit.Location;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.entity.Villager;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
@ -17,32 +20,30 @@ import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerInteractEntityEvent;
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;
public RestockOptimizedTrades() {
super("gameplay.restock-optimized-trades");
config.master().addComment(configPath,
"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.");
this.restockDayTimes = new TreeSet<>(Comparator.reverseOrder());
this.restockDayTimes.addAll(config.getList(configPath + ".restock-times", Arrays.asList(1000L, 13000L),
"At which (tick-)times during the day villagers will restock.\n" +
"There are 24.000 ticks in a single minecraft day."));
this.notify_player = config.getBoolean(configPath + ".notify-player", true,
shouldEnable();
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\s
don't have enough AI to restock their trades naturally, so this is here as a workaround.""");
this.restock_delay_millis = config.getInt("gameplay.restock-optimized-trades.delay-in-ticks", 1000,
"1 second = 20 ticks. There are 24.000 ticks in a single minecraft day.") * 50L;
this.notify_player = config.getBoolean("gameplay.restock-optimized-trades.notify-player", true,
"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
public void enable() {
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@ -56,55 +57,32 @@ public class RestockOptimizedTrades extends VillagerOptimizerModule implements L
return true;
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onPlayerInteractEntity(PlayerInteractEntityEvent event) {
if (event.getRightClicked().getType() != XEntityType.VILLAGER.get()) return;
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
private void onInteract(PlayerInteractEntityEvent event) {
if (!event.getRightClicked().getType().equals(EntityType.VILLAGER)) return;
WrappedVillager wrapped = wrapperCache.get((Villager) event.getRightClicked(), WrappedVillager::new);
if (!wrapped.isOptimized()) return;
WrappedVillager wVillager = villagerCache.getOrAdd((Villager) event.getRightClicked());
if (!wVillager.isOptimized()) return;
Player player = event.getPlayer();
if (event.getPlayer().hasPermission(Permissions.Bypass.RESTOCK_COOLDOWN.get())) {
wrapped.restock();
return;
}
final boolean player_bypassing = player.hasPermission(Bypass.RESTOCK_COOLDOWN.get());
long lastRestockFullTimeTicks = wrapped.getLastRestockFullTime();
long currentFullTimeTicks = wrapped.currentFullTimeTicks();
long currentDayTimeTicks = wrapped.currentDayTimeTicks();
long currentDay = currentFullTimeTicks - currentDayTimeTicks;
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 (wVillager.canRestock(restock_delay_millis) || player_bypassing) {
wVillager.restock();
wVillager.saveRestockTime();
if (notify_player && !player_bypassing) {
final TextReplacementConfig timeLeft = TextReplacementConfig.builder()
.matchLiteral("%time%")
.replacement(CommonUtil.formatDuration(Duration.ofMillis(wVillager.getRestockCooldownMillis(restock_delay_millis))))
.build();
VillagerOptimizer.getLang(player.locale()).trades_restocked.forEach(line -> player.sendMessage(line.replaceText(timeLeft)));
}
if (!restocked) {
wrapped.restock();
wrapped.saveRestockTime();
restocked = true;
if (log_enabled) {
final Location location = wVillager.villager().getLocation();
VillagerOptimizer.getLog().info(Component.text("Restocked optimized villager at " +
"x=" + location.getBlockX() + ", y=" + location.getBlockY() + ", z=" + location.getBlockZ() +
", world=" + location.getWorld().getName()).style(VillagerOptimizer.plugin_style));
}
}
if (!restocked) return;
if (notify_player) {
final TextReplacementConfig timeLeft = TextReplacementConfig.builder()
.matchLiteral("%time%")
.replacement(Util.formatDuration(Duration.ofMillis(ticksTillRestock * 50L)))
.build();
VillagerOptimizer.getLang(event.getPlayer().locale()).trades_restocked
.forEach(line -> KyoriUtil.sendMessage(event.getPlayer(), line.replaceText(timeLeft)));
}
if (log_enabled) {
info("Restocked optimized villager at " + LocationUtil.toString(wrapped.villager.getLocation()));
}
}
}

View File

@ -1,24 +1,27 @@
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.wrapper.WrappedVillager;
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.VillagerCareerChangeEvent;
public class UnoptimizeOnJobLoose extends VillagerOptimizerModule implements Listener {
public class UnoptimizeOnJobLoose implements VillagerOptimizerModule, Listener {
private final VillagerCache villagerCache;
public UnoptimizeOnJobLoose() {
super("gameplay.unoptimize-on-job-loose");
config.master().addComment(configPath + ".enable",
"Villagers that get their jobs reset will become unoptimized again.");
this.villagerCache = VillagerOptimizer.getCache();
}
@Override
public void enable() {
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@ -29,15 +32,18 @@ public class UnoptimizeOnJobLoose extends VillagerOptimizerModule implements Lis
@Override
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)
private void onJobReset(VillagerCareerChangeEvent event) {
if (event.getReason() != VillagerCareerChangeEvent.ChangeReason.LOSING_JOB) return;
final WrappedVillager wrapped = wrapperCache.get(event.getEntity(), WrappedVillager::new);
if (wrapped.isOptimized()) {
wrapped.setOptimizationType(OptimizationType.NONE);
if (!event.getReason().equals(VillagerCareerChangeEvent.ChangeReason.LOSING_JOB)) return;
WrappedVillager wrappedVillager = villagerCache.getOrAdd(event.getEntity());
if (wrappedVillager.isOptimized()) {
wrappedVillager.setOptimizationType(OptimizationType.NONE);
}
}
}

View File

@ -1,50 +0,0 @@
package me.xginko.villageroptimizer.modules.gameplay;
import me.xginko.villageroptimizer.events.VillagerOptimizeEvent;
import me.xginko.villageroptimizer.events.VillagerUnoptimizeEvent;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
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;
public class VisuallyHighlightOptimized extends VillagerOptimizerModule implements Listener {
public VisuallyHighlightOptimized() {
super("gameplay.outline-optimized-villagers");
config.master().addComment("gameplay.outline-optimized-villagers.enable",
"Will make optimized villagers glow.");
}
@Override
public void enable() {
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@Override
public void disable() {
HandlerList.unregisterAll(this);
}
@Override
public boolean shouldEnable() {
return config.getBoolean("gameplay.outline-optimized-villagers.enable", false);
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onOptimize(VillagerOptimizeEvent event) {
Villager villager = event.getWrappedVillager().villager;
scheduling.entitySpecificScheduler(villager).run(glow -> {
if (!villager.isGlowing()) villager.setGlowing(true);
}, null);
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onUnOptimize(VillagerUnoptimizeEvent event) {
Villager villager = event.getWrappedVillager().villager;
scheduling.entitySpecificScheduler(villager).run(unGlow -> {
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,20 +1,23 @@
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.struct.enums.OptimizationType;
import me.xginko.villageroptimizer.struct.enums.Permissions;
import me.xginko.villageroptimizer.WrappedVillager;
import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.enums.OptimizationType;
import me.xginko.villageroptimizer.enums.permissions.Bypass;
import me.xginko.villageroptimizer.enums.permissions.Optimize;
import me.xginko.villageroptimizer.events.VillagerOptimizeEvent;
import me.xginko.villageroptimizer.events.VillagerUnoptimizeEvent;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import me.xginko.villageroptimizer.utils.KyoriUtil;
import me.xginko.villageroptimizer.utils.LocationUtil;
import me.xginko.villageroptimizer.utils.Util;
import me.xginko.villageroptimizer.wrapper.WrappedVillager;
import me.xginko.villageroptimizer.utils.CommonUtil;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextReplacementConfig;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.entity.Villager;
import org.bukkit.event.EventHandler;
@ -25,60 +28,58 @@ import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import java.time.Duration;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
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 long cooldown_millis;
private final double search_radius;
private final boolean only_while_sneaking, notify_player, log_enabled;
public OptimizeByBlock() {
super("optimization-methods.block-optimization");
config.master().addComment(configPath + ".enable",
"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.");
List<String> defaults = Stream.of(XMaterial.LAPIS_BLOCK, XMaterial.GLOWSTONE, XMaterial.IRON_BLOCK)
.filter(XMaterial::isSupported)
.map(Enum::name)
.collect(Collectors.toList());
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 {
return Material.valueOf(configuredMaterial);
} catch (IllegalArgumentException e) {
warn("Material '" + configuredMaterial + "' not recognized. Please use correct Material enums from: " +
"https://jd.papermc.io/paper/1.20/org/bukkit/Material.html");
return null;
}
})
.filter(Objects::nonNull)
.collect(Collectors.toCollection(() -> EnumSet.noneOf(Material.class)));
shouldEnable();
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.\s
If a configured block is broken nearby, the closest villager will become unoptimized again.""");
this.blocks_that_disable = config.getList("optimization-methods.block-optimization.materials", List.of(
"LAPIS_BLOCK", "GLOWSTONE", "IRON_BLOCK"
), "Values here need to be valid bukkit Material enums for your server version."
).stream().map(configuredMaterial -> {
try {
return Material.valueOf(configuredMaterial);
} catch (IllegalArgumentException e) {
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");
return null;
}
}).filter(Objects::nonNull).collect(Collectors.toCollection(HashSet::new));
this.cooldown_millis = TimeUnit.SECONDS.toMillis(
config.getInt(configPath + ".optimize-cooldown-seconds", 600,
"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."));
this.search_radius = config.getDouble(configPath + ".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 closest unoptimized villager to the player will be optimized.") / 2;
this.only_while_sneaking = config.getBoolean(configPath + ".only-when-sneaking", true,
"Only optimize/unoptimize by block when player is sneaking during place or break.");
this.notify_player = config.getBoolean(configPath + ".notify-player", true,
config.getInt("optimization-methods.block-optimization.optimize-cooldown-seconds", 600, """
Cooldown in seconds until a villager can be optimized again by using specific blocks.\s
Here for configuration freedom. Recommended to leave as is to not enable any exploitable behavior."""));
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.\s
The closest unoptimized villager to the player will be optimized.""") / 2;
this.only_while_sneaking = config.getBoolean("optimization-methods.block-optimization.only-when-sneaking", true,
"Only optimize/unoptimize by workstation when player is sneaking during place or break.");
this.notify_player = config.getBoolean("optimization-methods.block-optimization.notify-player", true,
"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
public void enable() {
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@ -89,30 +90,31 @@ public class OptimizeByBlock extends VillagerOptimizerModule implements Listener
@Override
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)
private void onBlockPlace(BlockPlaceEvent event) {
final Block placed = event.getBlock();
Block placed = event.getBlock();
if (!blocks_that_disable.contains(placed.getType())) return;
final Player player = event.getPlayer();
if (!player.hasPermission(Permissions.Optimize.BLOCK.get())) return;
Player player = event.getPlayer();
if (!player.hasPermission(Optimize.BLOCK.get())) return;
if (only_while_sneaking && !player.isSneaking()) return;
final Location blockLoc = placed.getLocation().toCenterLocation();
WrappedVillager closestOptimizableVillager = null;
double closestDistance = Double.MAX_VALUE;
for (Villager villager : blockLoc.getNearbyEntitiesByType(Villager.class, search_radius)) {
for (Entity entity : blockLoc.getNearbyEntities(search_radius, search_radius, search_radius)) {
if (!entity.getType().equals(EntityType.VILLAGER)) continue;
Villager villager = (Villager) entity;
final Villager.Profession profession = villager.getProfession();
if (profession.equals(Villager.Profession.NONE) || profession.equals(Villager.Profession.NITWIT)) continue;
final double distance = LocationUtil.relDistance3DSquared(villager.getLocation(), blockLoc);
if (distance >= closestDistance) continue;
WrappedVillager wVillager = villagerCache.getOrAdd(villager);
final double distance = entity.getLocation().distanceSquared(blockLoc);
final WrappedVillager wVillager = wrapperCache.get(villager, WrappedVillager::new);
if (wVillager.canOptimize(cooldown_millis)) {
if (distance < closestDistance && wVillager.canOptimize(cooldown_millis)) {
closestOptimizableVillager = wVillager;
closestDistance = distance;
}
@ -120,66 +122,64 @@ public class OptimizeByBlock extends VillagerOptimizerModule implements Listener
if (closestOptimizableVillager == null) return;
if (closestOptimizableVillager.canOptimize(cooldown_millis) || player.hasPermission(Permissions.Bypass.BLOCK_COOLDOWN.get())) {
VillagerOptimizeEvent optimizeEvent = new VillagerOptimizeEvent(
closestOptimizableVillager,
OptimizationType.BLOCK,
player,
event.isAsynchronous()
);
if (closestOptimizableVillager.canOptimize(cooldown_millis) || player.hasPermission(Bypass.BLOCK_COOLDOWN.get())) {
VillagerOptimizeEvent optimizeEvent = new VillagerOptimizeEvent(closestOptimizableVillager, OptimizationType.BLOCK, player, event.isAsynchronous());
if (!optimizeEvent.callEvent()) return;
closestOptimizableVillager.setOptimizationType(optimizeEvent.getOptimizationType());
closestOptimizableVillager.saveOptimizeTime();
if (notify_player) {
final TextReplacementConfig vilProfession = TextReplacementConfig.builder()
.matchLiteral("%vil_profession%")
.replacement(Util.toNiceString(closestOptimizableVillager.villager.getProfession()))
.replacement(closestOptimizableVillager.villager().getProfession().toString().toLowerCase())
.build();
final TextReplacementConfig placedMaterial = TextReplacementConfig.builder()
.matchLiteral("%blocktype%")
.replacement(Util.toNiceString(placed.getType()))
.replacement(placed.getType().toString().toLowerCase())
.build();
VillagerOptimizer.getLang(player.locale()).block_optimize_success
.forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(vilProfession).replaceText(placedMaterial)));
VillagerOptimizer.getLang(player.locale()).block_optimize_success.forEach(line -> player.sendMessage(line
.replaceText(vilProfession)
.replaceText(placedMaterial)
));
}
if (log_enabled) {
info(player.getName() + " optimized villager at " +
LocationUtil.toString(closestOptimizableVillager.villager.getLocation()));
VillagerOptimizer.getLog().info(Component.text(player.getName() + " optimized villager by block at " +
CommonUtil.formatLocation(closestOptimizableVillager.villager().getLocation())).color(VillagerOptimizer.plugin_style.color()));
}
} else {
closestOptimizableVillager.sayNo();
CommonUtil.shakeHead(closestOptimizableVillager.villager());
if (notify_player) {
final TextReplacementConfig timeLeft = TextReplacementConfig.builder()
.matchLiteral("%time%")
.replacement(Util.formatDuration(Duration.ofMillis(closestOptimizableVillager.getOptimizeCooldownMillis(cooldown_millis))))
.replacement(CommonUtil.formatDuration(Duration.ofMillis(closestOptimizableVillager.getOptimizeCooldownMillis(cooldown_millis))))
.build();
VillagerOptimizer.getLang(player.locale()).block_on_optimize_cooldown
.forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(timeLeft)));
VillagerOptimizer.getLang(player.locale()).block_on_optimize_cooldown.forEach(line -> player.sendMessage(line.replaceText(timeLeft)));
}
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onBlockBreak(BlockBreakEvent event) {
final Block broken = event.getBlock();
Block broken = event.getBlock();
if (!blocks_that_disable.contains(broken.getType())) return;
final Player player = event.getPlayer();
if (!player.hasPermission(Permissions.Optimize.BLOCK.get())) return;
Player player = event.getPlayer();
if (!player.hasPermission(Optimize.BLOCK.get())) return;
if (only_while_sneaking && !player.isSneaking()) return;
final Location blockLoc = broken.getLocation().toCenterLocation();
WrappedVillager closestOptimizedVillager = null;
double closestDistance = Double.MAX_VALUE;
for (Villager villager : blockLoc.getNearbyEntitiesByType(Villager.class, search_radius)) {
final double distance = LocationUtil.relDistance3DSquared(villager.getLocation(), blockLoc);
if (distance >= closestDistance) continue;
for (Entity entity : blockLoc.getNearbyEntities(search_radius, search_radius, search_radius)) {
if (!entity.getType().equals(EntityType.VILLAGER)) continue;
Villager villager = (Villager) entity;
final WrappedVillager wVillager = wrapperCache.get(villager, WrappedVillager::new);
if (wVillager.isOptimized()) {
WrappedVillager wVillager = villagerCache.getOrAdd(villager);
final double distance = entity.getLocation().distanceSquared(blockLoc);
if (distance < closestDistance && wVillager.isOptimized()) {
closestOptimizedVillager = wVillager;
closestDistance = distance;
}
@ -187,32 +187,29 @@ public class OptimizeByBlock extends VillagerOptimizerModule implements Listener
if (closestOptimizedVillager == null) return;
VillagerUnoptimizeEvent unOptimizeEvent = new VillagerUnoptimizeEvent(
closestOptimizedVillager,
player,
OptimizationType.BLOCK,
event.isAsynchronous()
);
VillagerUnoptimizeEvent unOptimizeEvent = new VillagerUnoptimizeEvent(closestOptimizedVillager, player, OptimizationType.BLOCK, event.isAsynchronous());
if (!unOptimizeEvent.callEvent()) return;
closestOptimizedVillager.setOptimizationType(OptimizationType.NONE);
if (notify_player) {
final TextReplacementConfig vilProfession = TextReplacementConfig.builder()
.matchLiteral("%vil_profession%")
.replacement(Util.toNiceString(closestOptimizedVillager.villager.getProfession()))
.replacement(closestOptimizedVillager.villager().getProfession().toString().toLowerCase())
.build();
final TextReplacementConfig brokenMaterial = TextReplacementConfig.builder()
.matchLiteral("%blocktype%")
.replacement(Util.toNiceString(broken.getType()))
.replacement(broken.getType().toString().toLowerCase())
.build();
VillagerOptimizer.getLang(player.locale()).block_unoptimize_success
.forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(vilProfession).replaceText(brokenMaterial)));
VillagerOptimizer.getLang(player.locale()).block_unoptimize_success.forEach(line -> player.sendMessage(line
.replaceText(vilProfession)
.replaceText(brokenMaterial)
));
}
if (log_enabled) {
info(player.getName() + " unoptimized villager using " + Util.toNiceString(broken.getType()) +
LocationUtil.toString(closestOptimizedVillager.villager.getLocation()));
VillagerOptimizer.getLog().info(Component.text(player.getName() + " unoptimized villager by block at " +
CommonUtil.formatLocation(closestOptimizedVillager.villager().getLocation())).color(VillagerOptimizer.plugin_style.color()));
}
}
}

View File

@ -1,20 +1,21 @@
package me.xginko.villageroptimizer.modules.optimization;
import com.cryptomorin.xseries.XEntityType;
import com.cryptomorin.xseries.XMaterial;
import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.struct.enums.OptimizationType;
import me.xginko.villageroptimizer.struct.enums.Permissions;
import me.xginko.villageroptimizer.WrappedVillager;
import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.enums.OptimizationType;
import me.xginko.villageroptimizer.enums.permissions.Bypass;
import me.xginko.villageroptimizer.enums.permissions.Optimize;
import me.xginko.villageroptimizer.events.VillagerOptimizeEvent;
import me.xginko.villageroptimizer.events.VillagerUnoptimizeEvent;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import me.xginko.villageroptimizer.utils.KyoriUtil;
import me.xginko.villageroptimizer.utils.LocationUtil;
import me.xginko.villageroptimizer.utils.Util;
import me.xginko.villageroptimizer.wrapper.WrappedVillager;
import me.xginko.villageroptimizer.utils.CommonUtil;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextReplacementConfig;
import org.bukkit.ChatColor;
import org.bukkit.GameMode;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.bukkit.Material;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.entity.Villager;
import org.bukkit.event.EventHandler;
@ -26,39 +27,41 @@ import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import java.time.Duration;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
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 long cooldown;
private final boolean consume_nametag, notify_player, log_enabled;
public OptimizeByNametag() {
super("optimization-methods.nametag-optimization");
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.nametags = config.getList(configPath + ".names", Arrays.asList("Optimize", "DisableAI"),
shouldEnable();
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.\s
Nametag optimized villagers will be unoptimized again when they are renamed to something else.""");
this.nametags = config.getList("optimization-methods.nametag-optimization.names", List.of("Optimize", "DisableAI"),
"Names are case insensitive, capital letters won't matter.")
.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.");
this.cooldown = TimeUnit.SECONDS.toMillis(
config.getInt(configPath + ".optimize-cooldown-seconds", 600,
"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."));
this.notify_player = config.getBoolean(configPath + ".notify-player", true,
this.cooldown = config.getInt("optimization-methods.nametag-optimization.optimize-cooldown-seconds", 600, """
Cooldown in seconds until a villager can be optimized again using a nametag.\s
Here for configuration freedom. Recommended to leave as is to not enable any exploitable behavior.""") * 1000L;
this.notify_player = config.getBoolean("optimization-methods.nametag-optimization.notify-player", true,
"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
public void enable() {
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@ -69,88 +72,75 @@ public class OptimizeByNametag extends VillagerOptimizerModule implements Listen
@Override
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)
private void onPlayerInteractEntity(PlayerInteractEntityEvent event) {
if (event.getRightClicked().getType() != XEntityType.VILLAGER.get()) return;
final Player player = event.getPlayer();
if (!player.hasPermission(Permissions.Optimize.NAMETAG.get())) return;
if (!event.getRightClicked().getType().equals(EntityType.VILLAGER)) return;
Player player = event.getPlayer();
if (!player.hasPermission(Optimize.NAMETAG.get())) return;
final ItemStack usedItem = player.getInventory().getItem(event.getHand());
if (usedItem != null && usedItem.getType() != XMaterial.NAME_TAG.parseMaterial()) return;
if (!usedItem.hasItemMeta()) return;
final ItemMeta meta = usedItem.getItemMeta();
ItemStack usedItem = player.getInventory().getItem(event.getHand());
if (!usedItem.getType().equals(Material.NAME_TAG)) return;
ItemMeta meta = usedItem.getItemMeta();
if (!meta.hasDisplayName()) return;
final String nameTagPlainText = ChatColor.stripColor(meta.getDisplayName());
final WrappedVillager wrapped = wrapperCache.get((Villager) event.getRightClicked(), WrappedVillager::new);
if (nametags.contains(nameTagPlainText.toLowerCase())) {
if (wrapped.canOptimize(cooldown) || player.hasPermission(Permissions.Bypass.NAMETAG_COOLDOWN.get())) {
VillagerOptimizeEvent optimizeEvent = new VillagerOptimizeEvent(
wrapped,
OptimizationType.NAMETAG,
player,
event.isAsynchronous()
);
// Get component name first, so we can manually name the villager when canceling the event to avoid item consumption.
Component newVillagerName = meta.displayName();
assert newVillagerName != null; // Legitimate since we checked for hasDisplayName()
final String name = PlainTextComponentSerializer.plainText().serialize(newVillagerName);
Villager villager = (Villager) event.getRightClicked();
WrappedVillager wVillager = villagerCache.getOrAdd(villager);
if (nametags.contains(name.toLowerCase())) {
if (wVillager.canOptimize(cooldown) || player.hasPermission(Bypass.NAMETAG_COOLDOWN.get())) {
VillagerOptimizeEvent optimizeEvent = new VillagerOptimizeEvent(wVillager, OptimizationType.NAMETAG, player, event.isAsynchronous());
if (!optimizeEvent.callEvent()) return;
wrapped.setOptimizationType(optimizeEvent.getOptimizationType());
wrapped.saveOptimizeTime();
if (!consume_nametag && player.getGameMode() == GameMode.SURVIVAL) {
player.getInventory().addItem(usedItem.asOne());
if (!consume_nametag) {
event.setCancelled(true);
villager.customName(newVillagerName);
}
wVillager.setOptimizationType(optimizeEvent.getOptimizationType());
wVillager.saveOptimizeTime();
if (notify_player) {
VillagerOptimizer.getLang(player.locale()).nametag_optimize_success
.forEach(line -> KyoriUtil.sendMessage(player, line));
VillagerOptimizer.getLang(player.locale()).nametag_optimize_success.forEach(player::sendMessage);
}
if (log_enabled) {
info(player.getName() + " optimized villager using nametag '" + nameTagPlainText + "' at " +
LocationUtil.toString(wrapped.villager.getLocation()));
VillagerOptimizer.getLog().info(Component.text(player.getName() +
" optimized villager by nametag '" + name + "' at " +
CommonUtil.formatLocation(wVillager.villager().getLocation())).color(VillagerOptimizer.plugin_style.color()));
}
} else {
event.setCancelled(true);
wrapped.sayNo();
CommonUtil.shakeHead(villager);
if (notify_player) {
final TextReplacementConfig timeLeft = TextReplacementConfig.builder()
.matchLiteral("%time%")
.replacement(Util.formatDuration(Duration.ofMillis(wrapped.getOptimizeCooldownMillis(cooldown))))
.replacement(CommonUtil.formatDuration(Duration.ofMillis(wVillager.getOptimizeCooldownMillis(cooldown))))
.build();
VillagerOptimizer.getLang(player.locale()).nametag_on_optimize_cooldown
.forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(timeLeft)));
VillagerOptimizer.getLang(player.locale()).nametag_on_optimize_cooldown.forEach(line -> player.sendMessage(line.replaceText(timeLeft)));
}
}
} else {
if (wrapped.isOptimized()) {
VillagerUnoptimizeEvent unOptimizeEvent = new VillagerUnoptimizeEvent(
wrapped,
player,
OptimizationType.NAMETAG,
event.isAsynchronous()
);
if (wVillager.isOptimized()) {
VillagerUnoptimizeEvent unOptimizeEvent = new VillagerUnoptimizeEvent(wVillager, player, OptimizationType.NAMETAG, event.isAsynchronous());
if (!unOptimizeEvent.callEvent()) return;
wrapped.setOptimizationType(OptimizationType.NONE);
if (!consume_nametag && player.getGameMode() == GameMode.SURVIVAL) {
player.getInventory().addItem(usedItem.asOne());
}
wVillager.setOptimizationType(OptimizationType.NONE);
if (notify_player) {
VillagerOptimizer.getLang(player.locale()).nametag_unoptimize_success
.forEach(line -> KyoriUtil.sendMessage(player, line));
VillagerOptimizer.getLang(player.locale()).nametag_unoptimize_success.forEach(player::sendMessage);
}
if (log_enabled) {
info(player.getName() + " unoptimized villager using nametag '" + nameTagPlainText + "' at " +
LocationUtil.toString(wrapped.villager.getLocation()));
VillagerOptimizer.getLog().info(Component.text(player.getName() +
" unoptimized villager by nametag '" + name + "' at " +
CommonUtil.formatLocation(wVillager.villager().getLocation())).color(VillagerOptimizer.plugin_style.color()));
}
}
}

View File

@ -1,18 +1,26 @@
package me.xginko.villageroptimizer.modules.optimization;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.tcoded.folialib.impl.ServerImplementation;
import com.tcoded.folialib.wrapper.task.WrappedTask;
import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.struct.enums.OptimizationType;
import me.xginko.villageroptimizer.struct.enums.Permissions;
import me.xginko.villageroptimizer.WrappedVillager;
import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.enums.OptimizationType;
import me.xginko.villageroptimizer.enums.permissions.Bypass;
import me.xginko.villageroptimizer.enums.permissions.Optimize;
import me.xginko.villageroptimizer.events.VillagerOptimizeEvent;
import me.xginko.villageroptimizer.events.VillagerUnoptimizeEvent;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import me.xginko.villageroptimizer.utils.KyoriUtil;
import me.xginko.villageroptimizer.utils.LocationUtil;
import me.xginko.villageroptimizer.utils.Util;
import me.xginko.villageroptimizer.wrapper.WrappedVillager;
import me.xginko.villageroptimizer.utils.CommonUtil;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextReplacementConfig;
import org.bukkit.Location;
import org.bukkit.block.Block;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.entity.Villager;
import org.bukkit.event.EventHandler;
@ -21,43 +29,56 @@ import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.util.NumberConversions;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
public class OptimizeByWorkstation extends VillagerOptimizerModule implements Listener {
public class OptimizeByWorkstation implements VillagerOptimizerModule, Listener {
private final long cooldown_millis;
private final double search_radius;
private final int check_duration_ticks;
private final ServerImplementation scheduler;
private final VillagerCache villagerCache;
private final Cache<Location, WrappedTask> pending_optimizations;
private final long cooldown_millis, delay_millis, resettable_delay_millis;
private final double search_radius, search_radius_squared;
private final boolean only_while_sneaking, log_enabled, notify_player;
public OptimizeByWorkstation() {
super("optimization-methods.workstation-optimization");
config.master().addComment(configPath + ".enable",
"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.");
this.check_duration_ticks = Math.max(config.getInt(configPath + ".check-linger-duration-ticks", 100,
"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);
this.search_radius = config.getDouble(configPath + ".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 closest unoptimized villager to the player will be optimized.");
shouldEnable();
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,\s
if near their workstation. If the workstation is broken, the villager will become unoptimized again.""");
this.delay_millis = Math.max(config.getInt("optimization-methods.workstation-optimization.delay.default-delay-in-ticks", 10, """
The delay in ticks the plugin should wait before trying to optimize the closest villager on workstation place.\s
Gives the villager time to claim the placed workstation. Minimum delay is 1 Tick (Not recommended)"""), 1) * 50L;
this.resettable_delay_millis = Math.max(config.getInt("optimization-methods.workstation-optimization.delay.resettable-delay-in-ticks", 60, """
The delay in ticks the plugin should wait before trying to optimize a villager that can loose its profession\s
by having their workstation destroyed.\s
Intended to fix issues while trade rolling."""), 1) * 50L;
this.pending_optimizations = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMillis(Math.max(resettable_delay_millis, delay_millis) + 500L))
.build();
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.\s
The closest unoptimized villager to the player will be optimized.""");
this.search_radius_squared = NumberConversions.square(search_radius);
this.cooldown_millis = TimeUnit.SECONDS.toMillis(
Math.max(1, config.getInt(configPath + ".optimize-cooldown-seconds", 600,
"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.")));
this.only_while_sneaking = config.getBoolean(configPath + ".only-when-sneaking", true,
"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,
config.getInt("optimization-methods.workstation-optimization.optimize-cooldown-seconds", 600, """
Cooldown in seconds until a villager can be optimized again using a workstation.\s
Here for configuration freedom. Recommended to leave as is to not enable any exploitable behavior."""));
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");
this.notify_player = config.getBoolean("optimization-methods.workstation-optimization.notify-player", true,
"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
public void enable() {
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@ -68,146 +89,142 @@ public class OptimizeByWorkstation extends VillagerOptimizerModule implements Li
@Override
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)
private void onBlockPlace(BlockPlaceEvent event) {
final Block placed = event.getBlock();
final Villager.Profession workstationProfession = Util.getWorkstationProfession(placed.getType());
if (workstationProfession == null) return;
final Villager.Profession workstationProfession = CommonUtil.getWorkstationProfession(placed.getType());
if (workstationProfession.equals(Villager.Profession.NONE)) return;
final Player player = event.getPlayer();
if (!player.hasPermission(Optimize.WORKSTATION.get())) return;
if (only_while_sneaking && !player.isSneaking()) return;
if (!player.hasPermission(Permissions.Optimize.WORKSTATION.get())) return;
final Location workstationLoc = placed.getLocation();
final AtomicBoolean taskComplete = new AtomicBoolean();
final AtomicInteger taskAliveTicks = new AtomicInteger();
final Location workstationLoc = placed.getLocation().toCenterLocation();
WrappedVillager toOptimize = null;
scheduling.regionSpecificScheduler(workstationLoc).runAtFixedRate(repeatingTask -> {
if (taskComplete.get() || taskAliveTicks.getAndAdd(10) > check_duration_ticks) {
repeatingTask.cancel();
for (Entity entity : workstationLoc.getNearbyEntities(search_radius, search_radius, search_radius)) {
if (!entity.getType().equals(EntityType.VILLAGER)) continue;
Villager villager = (Villager) entity;
if (!villager.getProfession().equals(workstationProfession)) continue;
WrappedVillager wVillager = villagerCache.getOrAdd(villager);
final Location jobSite = wVillager.getJobSite();
if (jobSite == null) continue;
if (jobSite.distanceSquared(workstationLoc) > search_radius_squared) continue;
if (wVillager.canOptimize(cooldown_millis)) {
toOptimize = wVillager;
break;
}
}
if (toOptimize == null) return;
WrappedVillager finalToOptimize = toOptimize;
pending_optimizations.put(placed.getLocation(), scheduler.runAtLocationLater(workstationLoc, () -> {
if (!finalToOptimize.canOptimize(cooldown_millis) && !player.hasPermission(Bypass.WORKSTATION_COOLDOWN.get())) {
CommonUtil.shakeHead(finalToOptimize.villager());
if (notify_player) {
final TextReplacementConfig timeLeft = TextReplacementConfig.builder()
.matchLiteral("%time%")
.replacement(CommonUtil.formatDuration(Duration.ofMillis(finalToOptimize.getOptimizeCooldownMillis(cooldown_millis))))
.build();
VillagerOptimizer.getLang(player.locale()).nametag_on_optimize_cooldown.forEach(line -> player.sendMessage(line
.replaceText(timeLeft)
));
}
return;
}
for (Villager villager : workstationLoc.getNearbyEntitiesByType(Villager.class, search_radius)) {
scheduling.entitySpecificScheduler(villager).run(() -> {
if (villager.getProfession() != workstationProfession) return;
WrappedVillager wrapped = wrapperCache.get(villager, WrappedVillager::new);
VillagerOptimizeEvent optimizeEvent = new VillagerOptimizeEvent(finalToOptimize, OptimizationType.WORKSTATION, player, event.isAsynchronous());
if (!optimizeEvent.callEvent()) return;
Location jobSite = wrapped.getJobSite();
if (jobSite == null || jobSite.getWorld().getUID() != workstationLoc.getWorld().getUID()) return;
if (LocationUtil.relDistance3DSquared(jobSite, workstationLoc) > 1) return;
finalToOptimize.setOptimizationType(optimizeEvent.getOptimizationType());
finalToOptimize.saveOptimizeTime();
if (!wrapped.canOptimize(cooldown_millis) && !player.hasPermission(Permissions.Bypass.WORKSTATION_COOLDOWN.get())) {
wrapped.sayNo();
if (notify_player) {
final TextReplacementConfig timeLeft = TextReplacementConfig.builder()
.matchLiteral("%time%")
.replacement(Util.formatDuration(Duration.ofMillis(wrapped.getOptimizeCooldownMillis(cooldown_millis))))
.build();
VillagerOptimizer.getLang(player.locale()).nametag_on_optimize_cooldown
.forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(timeLeft)));
}
taskComplete.set(true);
return;
}
VillagerOptimizeEvent optimizeEvent = new VillagerOptimizeEvent(
wrapped,
OptimizationType.WORKSTATION,
player,
event.isAsynchronous()
);
if (!optimizeEvent.callEvent()) return;
wrapped.setOptimizationType(optimizeEvent.getOptimizationType());
wrapped.saveOptimizeTime();
if (notify_player) {
final TextReplacementConfig vilProfession = TextReplacementConfig.builder()
.matchLiteral("%vil_profession%")
.replacement(Util.toNiceString(wrapped.villager.getProfession()))
.build();
final TextReplacementConfig placedWorkstation = TextReplacementConfig.builder()
.matchLiteral("%blocktype%")
.replacement(Util.toNiceString(placed.getType()))
.build();
VillagerOptimizer.getLang(player.locale()).workstation_optimize_success
.forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(vilProfession).replaceText(placedWorkstation)));
}
if (log_enabled) {
info(player.getName() + " optimized villager using workstation " + Util.toNiceString(placed.getType()) + " at " +
LocationUtil.toString(wrapped.villager.getLocation()));
}
taskComplete.set(true);
}, null);
if (notify_player) {
final TextReplacementConfig vilProfession = TextReplacementConfig.builder()
.matchLiteral("%vil_profession%")
.replacement(finalToOptimize.villager().getProfession().toString().toLowerCase())
.build();
final TextReplacementConfig placedWorkstation = TextReplacementConfig.builder()
.matchLiteral("%workstation%")
.replacement(placed.getType().toString().toLowerCase())
.build();
VillagerOptimizer.getLang(player.locale()).workstation_optimize_success.forEach(line -> player.sendMessage(line
.replaceText(vilProfession)
.replaceText(placedWorkstation)
));
}
}, 1L, 10L);
if (log_enabled) {
VillagerOptimizer.getLog().info(Component.text(player.getName() +
" optimized villager by workstation (" + placed.getType().toString().toLowerCase() + ") at " +
CommonUtil.formatLocation(finalToOptimize.villager().getLocation())).color(VillagerOptimizer.plugin_style.color()));
}
}, toOptimize.canLooseProfession() ? resettable_delay_millis : delay_millis, TimeUnit.MILLISECONDS));
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onBlockBreak(BlockBreakEvent event) {
final Block broken = event.getBlock();
final Villager.Profession workstationProfession = Util.getWorkstationProfession(broken.getType());
if (workstationProfession == null) return;
// Cancel any pending optimization for this block
WrappedTask pendingOpt = pending_optimizations.getIfPresent(broken.getLocation());
if (pendingOpt != null) pendingOpt.cancel();
final Villager.Profession workstationProfession = CommonUtil.getWorkstationProfession(broken.getType());
if (workstationProfession.equals(Villager.Profession.NONE)) return;
final Player player = event.getPlayer();
if (!player.hasPermission(Permissions.Optimize.WORKSTATION.get())) return;
if (!player.hasPermission(Optimize.WORKSTATION.get())) return;
if (only_while_sneaking && !player.isSneaking()) return;
final Location workstationLoc = broken.getLocation();
WrappedVillager closestOptimized = null;
WrappedVillager closestOptimizedVillager = null;
double closestDistance = Double.MAX_VALUE;
for (Villager villager : workstationLoc.getNearbyEntitiesByType(Villager.class, search_radius)) {
for (Entity entity : workstationLoc.getNearbyEntities(search_radius, search_radius, search_radius)) {
if (!entity.getType().equals(EntityType.VILLAGER)) continue;
Villager villager = (Villager) entity;
if (!villager.getProfession().equals(workstationProfession)) continue;
final double distance = LocationUtil.relDistance3DSquared(villager.getLocation(), workstationLoc);
if (distance >= closestDistance) continue;
WrappedVillager wrapped = wrapperCache.get(villager, WrappedVillager::new);
WrappedVillager wVillager = villagerCache.getOrAdd(villager);
final double distance = entity.getLocation().distanceSquared(workstationLoc);
if (wrapped.isOptimized()) {
closestOptimized = wrapped;
if (distance < closestDistance && wVillager.isOptimized()) {
closestOptimizedVillager = wVillager;
closestDistance = distance;
}
}
if (closestOptimized == null) return;
VillagerUnoptimizeEvent unOptimizeEvent = new VillagerUnoptimizeEvent(
closestOptimized,
player,
OptimizationType.WORKSTATION,
event.isAsynchronous()
);
if (closestOptimizedVillager == null) return;
VillagerUnoptimizeEvent unOptimizeEvent = new VillagerUnoptimizeEvent(closestOptimizedVillager, player, OptimizationType.WORKSTATION, event.isAsynchronous());
if (!unOptimizeEvent.callEvent()) return;
closestOptimized.setOptimizationType(OptimizationType.NONE);
closestOptimizedVillager.setOptimizationType(OptimizationType.NONE);
if (notify_player) {
final TextReplacementConfig vilProfession = TextReplacementConfig.builder()
.matchLiteral("%vil_profession%")
.replacement(Util.toNiceString(closestOptimized.villager.getProfession()))
.replacement(closestOptimizedVillager.villager().getProfession().toString().toLowerCase())
.build();
final TextReplacementConfig brokenWorkstation = TextReplacementConfig.builder()
.matchLiteral("%blocktype%")
.replacement(Util.toNiceString(broken.getType()))
.matchLiteral("%workstation%")
.replacement(broken.getType().toString().toLowerCase())
.build();
VillagerOptimizer.getLang(player.locale()).workstation_unoptimize_success
.forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(vilProfession).replaceText(brokenWorkstation)));
VillagerOptimizer.getLang(player.locale()).workstation_unoptimize_success.forEach(line -> player.sendMessage(line
.replaceText(vilProfession)
.replaceText(brokenWorkstation)
));
}
if (log_enabled) {
info(player.getName() + " unoptimized villager using workstation " + Util.toNiceString(broken.getType()) + " at " +
LocationUtil.toString(closestOptimized.villager.getLocation()));
VillagerOptimizer.getLog().info(Component.text(player.getName() +
" unoptimized villager by workstation (" + broken.getType().toString().toLowerCase() + ") at " +
CommonUtil.formatLocation(closestOptimizedVillager.villager().getLocation())).color(VillagerOptimizer.plugin_style.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,95 +0,0 @@
package me.xginko.villageroptimizer.struct.enums;
import org.bukkit.Bukkit;
import org.bukkit.permissions.Permission;
import org.bukkit.permissions.PermissionDefault;
public final class Permissions {
public enum Bypass {
TRADE_PREVENTION(new Permission("villageroptimizer.bypass.tradeprevention",
"Permission to bypass unoptimized trade prevention", PermissionDefault.FALSE)),
RESTOCK_COOLDOWN(new Permission("villageroptimizer.bypass.restockcooldown",
"Permission to bypass restock cooldown on optimized villagers", PermissionDefault.FALSE)),
NAMETAG_COOLDOWN(new Permission("villageroptimizer.bypass.nametagcooldown",
"Permission to bypass Nametag optimization cooldown", PermissionDefault.FALSE)),
BLOCK_COOLDOWN(new Permission("villageroptimizer.bypass.blockcooldown",
"Permission to bypass Block optimization cooldown", PermissionDefault.FALSE)),
WORKSTATION_COOLDOWN(new Permission("villageroptimizer.bypass.workstationcooldown",
"Permission to bypass Workstation optimization cooldown", PermissionDefault.FALSE)),
COMMAND_COOLDOWN(new Permission("villageroptimizer.bypass.commandcooldown",
"Permission to bypass command optimization cooldown", PermissionDefault.FALSE));
private final Permission permission;
Bypass(Permission permission) {
this.permission = permission;
}
public Permission get() {
return permission;
}
}
public enum Commands {
VERSION(new Permission("villageroptimizer.cmd.version",
"Permission get the plugin version", PermissionDefault.OP)),
RELOAD(new Permission("villageroptimizer.cmd.reload",
"Permission to reload the plugin config", PermissionDefault.OP)),
DISABLE(new Permission("villageroptimizer.cmd.disable",
"Permission to disable the plugin", PermissionDefault.OP)),
OPTIMIZE_RADIUS(new Permission("villageroptimizer.cmd.optimize",
"Permission to optimize villagers in a radius", PermissionDefault.TRUE)),
UNOPTIMIZE_RADIUS(new Permission("villageroptimizer.cmd.unoptimize",
"Permission to unoptimize villagers in a radius", PermissionDefault.TRUE));
private final Permission permission;
Commands(Permission permission) {
this.permission = permission;
}
public Permission get() {
return permission;
}
}
public enum Optimize {
NAMETAG(new Permission("villageroptimizer.optimize.nametag",
"Permission to optimize / unoptimize using Nametags", PermissionDefault.TRUE)),
BLOCK(new Permission("villageroptimizer.optimize.block",
"Permission to optimize / unoptimize using Blocks", PermissionDefault.TRUE)),
WORKSTATION(new Permission("villageroptimizer.optimize.workstation",
"Permission to optimize / unoptimize using Workstations", PermissionDefault.TRUE));
private final Permission permission;
Optimize(Permission permission) {
this.permission = permission;
}
public Permission get() {
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,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

@ -0,0 +1,67 @@
package me.xginko.villageroptimizer.utils;
import org.bukkit.Chunk;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.entity.Villager;
import org.jetbrains.annotations.NotNull;
import java.time.Duration;
public class CommonUtil {
public static @NotNull String formatDuration(Duration duration) {
final int seconds = duration.toSecondsPart();
final int minutes = duration.toMinutesPart();
final int hours = duration.toHoursPart();
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 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 void shakeHead(@NotNull Villager villager) {
try {
villager.shakeHead();
} catch (NoSuchMethodError ignored) {}
}
public static Villager.Profession getWorkstationProfession(@NotNull Material workstation) {
return switch (workstation) {
case BARREL -> Villager.Profession.FISHERMAN;
case CARTOGRAPHY_TABLE -> Villager.Profession.CARTOGRAPHER;
case SMOKER -> Villager.Profession.BUTCHER;
case SMITHING_TABLE -> Villager.Profession.TOOLSMITH;
case GRINDSTONE -> Villager.Profession.WEAPONSMITH;
case BLAST_FURNACE -> Villager.Profession.ARMORER;
case CAULDRON -> Villager.Profession.LEATHERWORKER;
case BREWING_STAND -> Villager.Profession.CLERIC;
case COMPOSTER -> Villager.Profession.FARMER;
case FLETCHING_TABLE -> Villager.Profession.FLETCHER;
case LOOM -> Villager.Profession.SHEPHERD;
case LECTERN -> Villager.Profession.LIBRARIAN;
case STONECUTTER -> Villager.Profession.MASON;
default -> Villager.Profession.NONE;
};
}
}

View File

@ -1,53 +0,0 @@
package me.xginko.villageroptimizer.utils;
import me.xginko.villageroptimizer.VillagerOptimizer;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextReplacementConfig;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import java.util.Locale;
public class KyoriUtil {
public static void sendMessage(@NotNull CommandSender sender, @NotNull Component message) {
VillagerOptimizer.audiences().sender(sender).sendMessage(message);
}
public static void sendActionBar(@NotNull CommandSender sender, @NotNull Component message) {
VillagerOptimizer.audiences().sender(sender).sendActionBar(message);
}
public static @NotNull Component toUpperCase(@NotNull Component input, @NotNull Locale locale) {
return input.replaceText(TextReplacementConfig.builder()
.match("(?s).*")
.replacement((result, builder) -> builder.content(result.group(0).toUpperCase(locale)))
.build());
}
public static @NotNull String translateChatColor(@NotNull String string) {
string = string.replace("&0", "<black>");
string = string.replace("&1", "<dark_blue>");
string = string.replace("&2", "<dark_green>");
string = string.replace("&3", "<dark_aqua>");
string = string.replace("&4", "<dark_red>");
string = string.replace("&5", "<dark_purple>");
string = string.replace("&6", "<gold>");
string = string.replace("&7", "<gray>");
string = string.replace("&8", "<dark_gray>");
string = string.replace("&9", "<blue>");
string = string.replace("&a", "<green>");
string = string.replace("&b", "<aqua>");
string = string.replace("&c", "<red>");
string = string.replace("&d", "<light_purple>");
string = string.replace("&e", "<yellow>");
string = string.replace("&f", "<white>");
string = string.replace("&k", "<obfuscated>");
string = string.replace("&l", "<bold>");
string = string.replace("&m", "<strikethrough>");
string = string.replace("&n", "<underlined>");
string = string.replace("&o", "<italic>");
string = string.replace("&r", "<reset>");
return string;
}
}

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,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 +0,0 @@
me.xginko.villageroptimizer.logging.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}
website: ${project.url}
api-version: '1.16'
softdepend:
- AntiVillagerLag
folia-supported: true
commands: