Compare commits

..

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

123 changed files with 5569 additions and 4503 deletions

1
.github/FUNDING.yml vendored
View File

@ -1 +0,0 @@
ko_fi: xginko

View File

@ -1,26 +0,0 @@
---
name: 'Bug report about: Create a report to help us improve title: "[BUG]"'
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Additional context**
Add any other context about the problem here.

View File

@ -1,18 +0,0 @@
---
name: 'Feature request about: Suggest an idea for this project title:'
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@ -1,22 +0,0 @@
name: Build and upload jar
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 8
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '8'
- name: Build with Maven
run: mvn -B package --file pom.xml

View File

@ -12,13 +12,6 @@ There are 4 methods to do so:
It aims to be highly customizable and performant. Offering a multilang system, which displays messages based on the player client's language setting as well as an optimize- and unoptimize event, so you may extend the plugin with your own custom solutions.
### Commands:
| Command | Aliases | Description |
|:---------------------------------------------:|:-------------------:|:------------------------------------------:|
| /villageroptimizer [reload, version, disable] | voptimizer, vo | VillagerOptimizer admin commands |
| /optimizevillagers <blockradius> | noai, optvils | Optmize villagers in a radius around you |
| /unoptimizevillagers <blockradius> | noaiundo, unoptvils | Unoptmize villagers in a radius around you |
Other features:
- Prevent trading with unoptimized villagers to encourage players to optimize them
- Smart villager chunk limit with configurable max numbers for optimized and unoptimized villagers and a villager profession based priorisation system (you can configure what kind of villagers should be deleted first, like for example nitwits or jobless villagers.)
@ -31,4 +24,7 @@ Other features:
### Found a bug or got an idea for an enhancement? Open an issue or join the [discord](https://discord.com/invite/3UgsYf3qyc)!
# Supports Paper/Folia 1.16 - 1.20.4
# Important:
### The 1.16 version is meant to be used on Paper servers 1.16-1.19.4
### The 1.20.2 version is meant to be used on Paper 1.20+ and Folia 1.19.4+ servers

View File

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
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>
<parent>
<groupId>me.xginko.VillagerOptimizer</groupId>
<artifactId>VillagerOptimizer</artifactId>
<version>1.0.1</version>
</parent>
<artifactId>1.16.5</artifactId>
<name>${project.parent.artifactId}-${project.parent.version}--${project.artifactId}</name>
<packaging>jar</packaging>
<properties>
<java.version>16</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<relocations>
<relocation>
<pattern>com.github.benmanes.caffeine</pattern>
<shadedPattern>me.xginko.villageroptimizer.caffeine</shadedPattern>
</relocation>
<relocation>
<pattern>org.bstats</pattern>
<shadedPattern>me.xginko.villageroptimizer.bstats</shadedPattern>
</relocation>
</relocations>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
</build>
<repositories>
<repository>
<id>papermc-repo</id>
<url>https://repo.papermc.io/repository/maven-public/</url>
</repository>
<repository>
<id>sonatype</id>
<url>https://oss.sonatype.org/content/groups/public/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>com.destroystokyo.paper</groupId>
<artifactId>paper-api</artifactId>
<version>1.16.5-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-minimessage</artifactId>
<version>4.14.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-plain</artifactId>
<version>4.14.0</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;
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 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

@ -0,0 +1,173 @@
package me.xginko.villageroptimizer;
import me.xginko.villageroptimizer.commands.VillagerOptimizerCommand;
import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.config.LanguageCache;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
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 org.bstats.bukkit.Metrics;
import org.bukkit.NamespacedKey;
import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import java.util.jar.JarFile;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public final class VillagerOptimizer extends JavaPlugin {
private static VillagerOptimizer instance;
private static VillagerCache villagerCache;
private static HashMap<String, LanguageCache> languageCacheMap;
private static Config config;
private static Logger logger;
public final static Style plugin_style = Style.style(TextColor.color(102,255,230), TextDecoration.BOLD);
@Override
public void onEnable() {
instance = this;
logger = getLogger();
ConsoleCommandSender console = getServer().getConsoleSender();
console.sendMessage(Component.text("╭────────────────────────────────────────────────────────────╮").style(plugin_style));
console.sendMessage(Component.text("│ │").style(plugin_style));
console.sendMessage(Component.text("│ │").style(plugin_style));
console.sendMessage(Component.text("│ _ __ _ __ __ │").style(plugin_style));
console.sendMessage(Component.text("│ | | / /(_)/ // /___ _ ___ _ ___ ____ │").style(plugin_style));
console.sendMessage(Component.text("│ | |/ // // // // _ `// _ `// -_)/ __/ │").style(plugin_style));
console.sendMessage(Component.text("│ |___//_//_//_/ \\_,_/ \\_, / \\__//_/ │").style(plugin_style));
console.sendMessage(Component.text("│ ____ __ _ /___/_ │").style(plugin_style));
console.sendMessage(Component.text("│ / __ \\ ___ / /_ (_)__ _ (_)___ ___ ____ │").style(plugin_style));
console.sendMessage(Component.text("│ / /_/ // _ \\/ __// // ' \\ / //_ // -_)/ __/ │").style(plugin_style));
console.sendMessage(Component.text("\\____// .__/\\__//_//_/_/_//_/ /__/\\__//_/ │").style(plugin_style));
console.sendMessage(Component.text("│ /_/ by xGinko │").style(plugin_style));
console.sendMessage(Component.text("│ │").style(plugin_style));
console.sendMessage(Component.text("│ │").style(plugin_style));
console.sendMessage(Component.text("").style(plugin_style).append(Component.text("https://github.com/xGinko/VillagerOptimizer").color(NamedTextColor.GRAY)).append(Component.text("").style(plugin_style)));
console.sendMessage(Component.text("│ │").style(plugin_style));
console.sendMessage(Component.text("│ │").style(plugin_style));
console.sendMessage(Component.text("").style(plugin_style).append(Component.text(" ➤ Loading Translations...").style(plugin_style)).append(Component.text("").style(plugin_style)));
reloadLang(true);
console.sendMessage(Component.text("").style(plugin_style).append(Component.text(" ➤ Loading Config...").style(plugin_style)).append(Component.text("").style(plugin_style)));
reloadConfiguration();
console.sendMessage(Component.text("").style(plugin_style).append(Component.text(" ✓ Done.").color(NamedTextColor.WHITE).decorate(TextDecoration.BOLD)).append(Component.text("").style(plugin_style)));
console.sendMessage(Component.text("│ │").style(plugin_style));
console.sendMessage(Component.text("│ │").style(plugin_style));
console.sendMessage(Component.text("╰────────────────────────────────────────────────────────────╯").style(plugin_style));
new Metrics(this, 19954);
}
public static VillagerOptimizer getInstance() {
return instance;
}
public static NamespacedKey getKey(String key) {
return new NamespacedKey(instance, key);
}
public static Config getConfiguration() {
return config;
}
public static VillagerCache getCache() {
return villagerCache;
}
public static Logger getLog() {
return logger;
}
public static LanguageCache getLang(Locale locale) {
return getLang(locale.toString().toLowerCase());
}
public static LanguageCache getLang(CommandSender commandSender) {
return commandSender instanceof Player player ? getLang(player.locale()) : getLang(config.default_lang);
}
public static LanguageCache getLang(String lang) {
return config.auto_lang ? languageCacheMap.getOrDefault(lang.replace("-", "_"), languageCacheMap.get(config.default_lang.toString().toLowerCase())) : languageCacheMap.get(config.default_lang.toString().toLowerCase());
}
public void reloadPlugin() {
reloadLang(false);
reloadConfiguration();
}
private void reloadConfiguration() {
try {
config = new Config();
villagerCache = new VillagerCache(config.cache_keep_time_seconds);
VillagerOptimizerCommand.reloadCommands();
VillagerOptimizerModule.reloadModules();
config.saveConfig();
} catch (Exception e) {
logger.severe("Error loading config! - " + e.getLocalizedMessage());
e.printStackTrace();
}
}
private void reloadLang(boolean startup) {
languageCacheMap = new HashMap<>();
ConsoleCommandSender console = getServer().getConsoleSender();
try {
File langDirectory = new File(getDataFolder() + "/lang");
Files.createDirectories(langDirectory.toPath());
for (String fileName : getDefaultLanguageFiles()) {
String localeString = fileName.substring(fileName.lastIndexOf('/') + 1, fileName.lastIndexOf('.'));
if (startup) console.sendMessage(
Component.text("").style(plugin_style)
.append(Component.text(" "+localeString).color(NamedTextColor.WHITE).decorate(TextDecoration.BOLD))
.append(Component.text("").style(plugin_style)));
else logger.info("Found language file for " + localeString);
LanguageCache langCache = new LanguageCache(localeString);
languageCacheMap.put(localeString, langCache);
}
Pattern langPattern = Pattern.compile("([a-z]{1,3}_[a-z]{1,3})(\\.yml)", Pattern.CASE_INSENSITIVE);
for (File langFile : langDirectory.listFiles()) {
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) console.sendMessage(
Component.text("").style(plugin_style)
.append(Component.text(" "+localeString).color(NamedTextColor.WHITE).decorate(TextDecoration.BOLD))
.append(Component.text("").style(plugin_style)));
else logger.info("Found language file for " + localeString);
LanguageCache langCache = new LanguageCache(localeString);
languageCacheMap.put(localeString, langCache);
}
}
}
} catch (Exception e) {
if (startup) console.sendMessage(
Component.text("").style(plugin_style)
.append(Component.text("LANG ERROR").color(NamedTextColor.RED).decorate(TextDecoration.BOLD))
.append(Component.text("").style(plugin_style)));
else logger.severe("Error loading language files! Language files will not reload to avoid errors, make sure to correct this before restarting the server!");
e.printStackTrace();
}
}
private Set<String> getDefaultLanguageFiles() {
Set<String> languageFiles = new HashSet<>();
try (JarFile jarFile = new JarFile(this.getFile())) {
jarFile.entries().asIterator().forEachRemaining(jarFileEntry -> {
final String path = jarFileEntry.getName();
if (path.startsWith("lang/") && path.endsWith(".yml"))
languageFiles.add(path);
});
} catch (IOException e) {
logger.severe("Error while getting default language files! - " + e.getLocalizedMessage());
e.printStackTrace();
}
return languageFiles;
}
}

View File

@ -0,0 +1,188 @@
package me.xginko.villageroptimizer;
import me.xginko.villageroptimizer.enums.Keys;
import me.xginko.villageroptimizer.enums.OptimizationType;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.entity.Villager;
import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.persistence.PersistentDataType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public final class WrappedVillager {
private final @NotNull Villager villager;
private final @NotNull PersistentDataContainer dataContainer;
WrappedVillager(@NotNull Villager villager) {
this.villager = villager;
this.dataContainer = villager.getPersistentDataContainer();
}
/**
* @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 this plugin, otherwise false.
*/
public boolean isOptimized() {
return dataContainer.has(Keys.OPTIMIZATION_TYPE.key(), 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) {
return getLastOptimize() + cooldown_millis <= System.currentTimeMillis();
}
/**
* @param type OptimizationType the villager should be set to.
*/
public void setOptimization(OptimizationType type) {
if (type.equals(OptimizationType.NONE) && isOptimized()) {
dataContainer.remove(Keys.OPTIMIZATION_TYPE.key());
villager.setAware(true);
villager.setAI(true);
} else {
dataContainer.set(Keys.OPTIMIZATION_TYPE.key(), PersistentDataType.STRING, type.name());
villager.setAware(false);
}
}
/**
* @return The current OptimizationType of the villager.
*/
public @NotNull OptimizationType getOptimizationType() {
return isOptimized() ? OptimizationType.valueOf(dataContainer.get(Keys.OPTIMIZATION_TYPE.key(), PersistentDataType.STRING)) : OptimizationType.NONE;
}
/**
* Saves the system time in millis when the villager was last optimized.
*/
public void saveOptimizeTime() {
dataContainer.set(Keys.LAST_OPTIMIZE.key(), 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() {
return dataContainer.has(Keys.LAST_OPTIMIZE.key(), PersistentDataType.LONG) ? dataContainer.get(Keys.LAST_OPTIMIZE.key(), PersistentDataType.LONG) : 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) {
return dataContainer.has(Keys.LAST_OPTIMIZE.key(), PersistentDataType.LONG) ? (System.currentTimeMillis() - (dataContainer.get(Keys.LAST_OPTIMIZE.key(), PersistentDataType.LONG) + cooldown_millis)) : 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() {
villager.getRecipes().forEach(recipe -> recipe.setUses(0));
}
/**
* Saves the time of the in-game world when the entity was last restocked.
*/
public void saveRestockTime() {
dataContainer.set(Keys.LAST_RESTOCK.key(), PersistentDataType.LONG, villager.getWorld().getFullTime());
}
/**
* @return The time of the in-game world when the entity was last restocked.
*/
public long getLastRestock() {
return dataContainer.has(Keys.LAST_RESTOCK.key(), PersistentDataType.LONG) ? dataContainer.get(Keys.LAST_RESTOCK.key(), PersistentDataType.LONG) : 0L;
}
public long getRestockCooldownMillis(final long cooldown_millis) {
return dataContainer.has(Keys.LAST_RESTOCK.key(), PersistentDataType.LONG) ? (villager.getWorld().getFullTime() - (dataContainer.get(Keys.LAST_RESTOCK.key(), PersistentDataType.LONG) + cooldown_millis)) : 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) {
return getLastLevelUpTime() + cooldown_millis <= villager.getWorld().getFullTime();
}
/**
* Saves the time of the in-game world when the entity was last leveled up.
*/
public void saveLastLevelUp() {
dataContainer.set(Keys.LAST_LEVELUP.key(), 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() {
return dataContainer.has(Keys.LAST_LEVELUP.key(), PersistentDataType.LONG) ? dataContainer.get(Keys.LAST_LEVELUP.key(), PersistentDataType.LONG) : 0L;
}
public long getLevelCooldownMillis(final long cooldown_millis) {
return dataContainer.has(Keys.LAST_LEVELUP.key(), PersistentDataType.LONG) ? (villager.getWorld().getFullTime() - (dataContainer.get(Keys.LAST_LEVELUP.key(), PersistentDataType.LONG) + cooldown_millis)) : cooldown_millis;
}
public void memorizeName(final Component customName) {
dataContainer.set(Keys.LAST_OPTIMIZE_NAME.key(), PersistentDataType.STRING, MiniMessage.miniMessage().serialize(customName));
}
public @Nullable Component getMemorizedName() {
return dataContainer.has(Keys.LAST_OPTIMIZE_NAME.key(), PersistentDataType.STRING) ? MiniMessage.miniMessage().deserialize(dataContainer.get(Keys.LAST_OPTIMIZE_NAME.key(), PersistentDataType.STRING)) : null;
}
public void forgetName() {
dataContainer.remove(Keys.LAST_OPTIMIZE_NAME.key());
}
}

View File

@ -0,0 +1,11 @@
package me.xginko.villageroptimizer.commands;
import net.kyori.adventure.text.TextComponent;
import org.bukkit.command.CommandSender;
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

@ -0,0 +1,35 @@
package me.xginko.villageroptimizer.commands;
import me.xginko.villageroptimizer.VillagerOptimizer;
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.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandMap;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import java.util.HashSet;
public interface VillagerOptimizerCommand extends CommandExecutor {
String label();
HashSet<VillagerOptimizerCommand> commands = new HashSet<>();
static void reloadCommands() {
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
CommandMap commandMap = plugin.getServer().getCommandMap();
commands.forEach(command -> plugin.getCommand(command.label()).unregister(commandMap));
commands.clear();
commands.add(new VillagerOptimizerCmd());
commands.add(new OptVillagersRadius());
commands.add(new UnOptVillagersRadius());
commands.forEach(command -> plugin.getCommand(command.label()).setExecutor(command));
}
@Override
boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args);
}

View File

@ -1,19 +1,20 @@
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;
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;
import net.kyori.adventure.text.format.TextDecoration;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
@ -21,90 +22,79 @@ 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, TabCompleter {
private final List<String> tabCompletes = List.of("5", "10", "25", "50");
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);
return true;
}
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
return args.length == 1 ? tabCompletes : null;
}
if (!(sender instanceof Player)) {
KyoriUtil.sendMessage(sender, Component.text("This command can only be executed by a player.")
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
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 (!sender.hasPermission(Permissions.Commands.OPTIMIZE_RADIUS.get())) {
sender.sendMessage(VillagerOptimizer.getLang(sender).no_permission);
return true;
}
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;
}
try {
final int specifiedRadius = Integer.parseInt(args[0]);
// Turn negative numbers into positive ones
final int safeRadius = (int) Math.sqrt(specifiedRadius * specifiedRadius);
int specifiedRadius = Integer.parseInt(args[0]);
if (safeRadius == 0) {
VillagerOptimizer.getLang(player.locale()).command_radius_invalid
.forEach(line -> KyoriUtil.sendMessage(sender, line));
return true;
}
if (safeRadius > max_radius) {
if (specifiedRadius > max_radius) {
final TextReplacementConfig limit = TextReplacementConfig.builder()
.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());
for (Entity entity : player.getNearbyEntities(safeRadius, safeRadius, safeRadius)) {
for (Entity entity : player.getNearbyEntities(specifiedRadius, specifiedRadius, specifiedRadius)) {
if (!entity.getType().equals(EntityType.VILLAGER)) continue;
Villager villager = (Villager) entity;
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);
if (optimizeEvent.callEvent()) {
wVillager.setOptimizationType(optimizeEvent.getOptimizationType());
wVillager.setOptimization(optimizeEvent.getOptimizationType());
wVillager.saveOptimizeTime();
successCount++;
}
@ -116,10 +106,9 @@ public class OptVillagersRadius extends VillagerOptimizerCommand {
if (successCount <= 0 && failCount <= 0) {
final TextReplacementConfig radius = TextReplacementConfig.builder()
.matchLiteral("%radius%")
.replacement(Integer.toString(safeRadius))
.replacement(Integer.toString(specifiedRadius))
.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;
}
@ -130,22 +119,22 @@ public class OptVillagersRadius extends VillagerOptimizerCommand {
.build();
final TextReplacementConfig radius = TextReplacementConfig.builder()
.matchLiteral("%radius%")
.replacement(Integer.toString(safeRadius))
.replacement(Integer.toString(specifiedRadius))
.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,18 +1,19 @@
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;
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;
import net.kyori.adventure.text.format.TextDecoration;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
@ -20,84 +21,72 @@ 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, TabCompleter {
private final List<String> tabCompletes = List.of("5", "10", "25", "50");
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);
return true;
}
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
return args.length == 1 ? tabCompletes : null;
}
if (!(sender instanceof Player)) {
KyoriUtil.sendMessage(sender, Component.text("This command can only be executed by a player.")
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
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 (!sender.hasPermission(Permissions.Commands.UNOPTIMIZE_RADIUS.get())) {
sender.sendMessage(VillagerOptimizer.getLang(sender).no_permission);
return true;
}
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;
}
try {
final int specifiedRadius = Integer.parseInt(args[0]);
// Turn negative numbers into positive ones
final int safeRadius = (int) Math.sqrt(specifiedRadius * specifiedRadius);
int specifiedRadius = Integer.parseInt(args[0]);
if (safeRadius == 0) {
VillagerOptimizer.getLang(player.locale()).command_radius_invalid
.forEach(line -> KyoriUtil.sendMessage(sender, line));
return true;
}
if (safeRadius > max_radius) {
if (specifiedRadius > max_radius) {
final TextReplacementConfig limit = TextReplacementConfig.builder()
.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)) {
for (Entity entity : player.getNearbyEntities(specifiedRadius, specifiedRadius, specifiedRadius)) {
if (!entity.getType().equals(EntityType.VILLAGER)) continue;
Villager villager = (Villager) entity;
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);
if (unOptimizeEvent.callEvent()) {
wVillager.setOptimizationType(OptimizationType.NONE);
wVillager.setOptimization(OptimizationType.NONE);
successCount++;
}
}
@ -106,10 +95,9 @@ public class UnOptVillagersRadius extends VillagerOptimizerCommand {
if (successCount <= 0) {
final TextReplacementConfig radius = TextReplacementConfig.builder()
.matchLiteral("%radius%")
.replacement(Integer.toString(safeRadius))
.replacement(Integer.toString(specifiedRadius))
.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%")
@ -117,14 +105,15 @@ public class UnOptVillagersRadius extends VillagerOptimizerCommand {
.build();
final TextReplacementConfig radius = TextReplacementConfig.builder()
.matchLiteral("%radius%")
.replacement(Integer.toString(safeRadius))
.replacement(Integer.toString(specifiedRadius))
.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

@ -0,0 +1,79 @@
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.enums.Permissions;
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.bukkit.command.TabCompleter;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
public class VillagerOptimizerCmd implements TabCompleter, VillagerOptimizerCommand {
private final List<SubCommand> subCommands = new ArrayList<>(3);
private final List<String> tabCompleter = new ArrayList<>(3);
public VillagerOptimizerCmd() {
subCommands.add(new ReloadSubCmd());
subCommands.add(new VersionSubCmd());
subCommands.add(new DisableSubCmd());
subCommands.forEach(subCommand -> tabCompleter.add(subCommand.getLabel()));
}
@Override
public String label() {
return "villageroptimizer";
}
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, String[] args) {
return args.length == 1 ? tabCompleter : null;
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
if (args.length > 0) {
boolean cmdExists = false;
for (SubCommand subCommand : subCommands) {
if (args[0].equalsIgnoreCase(subCommand.getLabel())) {
subCommand.perform(sender, args);
cmdExists = true;
break;
}
}
if (!cmdExists) sendCommandOverview(sender);
} else {
sendCommandOverview(sender);
}
return true;
}
private void sendCommandOverview(CommandSender sender) {
if (!sender.hasPermission(Permissions.Commands.RELOAD.get()) && !sender.hasPermission(Permissions.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))
);
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))
);
sender.sendMessage(Component.text("-----------------------------------------------------").color(NamedTextColor.GRAY));
}
}

View File

@ -0,0 +1,45 @@
package me.xginko.villageroptimizer.commands.villageroptimizer.subcommands;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.commands.SubCommand;
import me.xginko.villageroptimizer.enums.Permissions;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.CommandSender;
import org.bukkit.event.HandlerList;
public class DisableSubCmd extends SubCommand {
@Override
public String getLabel() {
return "disable";
}
@Override
public TextComponent getDescription() {
return Component.text("Disable all plugin tasks and listeners.").color(NamedTextColor.GRAY);
}
@Override
public TextComponent getSyntax() {
return Component.text("/villageroptimizer disable").color(VillagerOptimizer.plugin_style.color());
}
@Override
public void perform(CommandSender sender, String[] args) {
if (sender.hasPermission(Permissions.Commands.DISABLE.get())) {
sender.sendMessage(Component.text("Disabling VillagerOptimizer...").color(NamedTextColor.RED));
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
HandlerList.unregisterAll(plugin);
plugin.getServer().getScheduler().cancelTasks(plugin);
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));
} else {
sender.sendMessage(VillagerOptimizer.getLang(sender).no_permission);
}
}
}

View File

@ -0,0 +1,41 @@
package me.xginko.villageroptimizer.commands.villageroptimizer.subcommands;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.commands.SubCommand;
import me.xginko.villageroptimizer.enums.Permissions;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.CommandSender;
public class ReloadSubCmd extends SubCommand {
@Override
public String getLabel() {
return "reload";
}
@Override
public TextComponent getDescription() {
return Component.text("Reload the plugin configuration.").color(NamedTextColor.GRAY);
}
@Override
public TextComponent getSyntax() {
return Component.text("/villageroptimizer reload").color(VillagerOptimizer.plugin_style.color());
}
@Override
public void perform(CommandSender sender, String[] args) {
if (sender.hasPermission(Permissions.Commands.RELOAD.get())) {
sender.sendMessage(Component.text("Reloading VillagerOptimizer...").color(NamedTextColor.WHITE));
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> {
plugin.reloadPlugin();
sender.sendMessage(Component.text("Reload complete.").color(NamedTextColor.GREEN));
});
} else {
sender.sendMessage(VillagerOptimizer.getLang(sender).no_permission);
}
}
}

View File

@ -0,0 +1,53 @@
package me.xginko.villageroptimizer.commands.villageroptimizer.subcommands;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.commands.SubCommand;
import me.xginko.villageroptimizer.enums.Permissions;
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.CommandSender;
import org.bukkit.plugin.PluginDescriptionFile;
public class VersionSubCmd extends SubCommand {
@Override
public String getLabel() {
return "version";
}
@Override
public TextComponent getDescription() {
return Component.text("Show the plugin version.").color(NamedTextColor.GRAY);
}
@Override
public TextComponent getSyntax() {
return Component.text("/villageroptimizer version").color(VillagerOptimizer.plugin_style.color());
}
@Override
public void perform(CommandSender sender, String[] args) {
if (sender.hasPermission(Permissions.Commands.VERSION.get())) {
final PluginDescriptionFile pluginMeta = VillagerOptimizer.getInstance().getDescription();
sender.sendMessage(
Component.newline()
.append(
Component.text(pluginMeta.getName()+" "+pluginMeta.getVersion())
.style(VillagerOptimizer.plugin_style)
.clickEvent(ClickEvent.openUrl(pluginMeta.getWebsite()))
)
.append(Component.text(" by ").color(NamedTextColor.GRAY))
.append(
Component.text(pluginMeta.getAuthors().get(0))
.color(NamedTextColor.WHITE)
.clickEvent(ClickEvent.openUrl("https://github.com/xGinko"))
)
.append(Component.newline())
);
} else {
sender.sendMessage(VillagerOptimizer.getLang(sender).no_permission);
}
}
}

View File

@ -0,0 +1,146 @@
package me.xginko.villageroptimizer.config;
import io.github.thatsmusic99.configurationmaster.api.ConfigFile;
import io.github.thatsmusic99.configurationmaster.api.ConfigSection;
import me.xginko.villageroptimizer.VillagerOptimizer;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.util.List;
import java.util.Locale;
import java.util.Map;
public class Config {
private final @NotNull ConfigFile config;
public final @NotNull Locale default_lang;
public final boolean auto_lang;
public final long cache_keep_time_seconds;
public Config() throws Exception {
this.config = loadConfig(new File(VillagerOptimizer.getInstance().getDataFolder(), "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 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_seconds = getInt("general.cache-keep-time-seconds", 30,
"The amount of time in seconds a villager will be kept in the plugin's cache.");
}
private ConfigFile loadConfig(File ymlFile) throws Exception {
File parent = ymlFile.getParentFile();
if (!parent.exists() && !parent.mkdir())
VillagerOptimizer.getLog().severe("Unable to create plugin config directory.");
return ConfigFile.loadConfig(ymlFile);
}
public void saveConfig() {
try {
config.save();
} catch (Exception e) {
VillagerOptimizer.getLog().severe("Failed to save config file! - " + e.getLocalizedMessage());
}
}
private void structureConfig() {
config.addDefault("config-version", 1.00);
createTitledSection("General", "general");
createTitledSection("Optimization", "optimization-methods");
config.addDefault("optimization-methods.commands.unoptimizevillagers", null);
config.addComment("optimization-methods.commands", """
If you want to disable commands, negate the following permissions:\s
villageroptimizer.cmd.optimize\s
villageroptimizer.cmd.unoptimize
""");
config.addDefault("optimization-methods.nametag-optimization.enable", true);
createTitledSection("Villager Chunk Limit", "villager-chunk-limit");
createTitledSection("Gameplay", "gameplay");
config.addDefault("gameplay.restock-optimized-trades", null);
config.addDefault("gameplay.level-optimized-profession", null);
config.addDefault("gameplay.rename-optimized-villagers.enable", true);
config.addDefault("gameplay.villagers-spawn-as-adults.enable", false);
config.addDefault("gameplay.prevent-trading-with-unoptimized.enable", false);
config.addDefault("gameplay.prevent-entities-from-targeting-optimized.enable", true);
config.addDefault("gameplay.prevent-damage-to-optimized.enable", true);
}
public void createTitledSection(@NotNull String title, @NotNull String path) {
config.addSection(title);
config.addDefault(path, null);
}
public @NotNull ConfigFile master() {
return config;
}
public boolean getBoolean(@NotNull String path, boolean def, @NotNull String comment) {
config.addDefault(path, def, comment);
return config.getBoolean(path, def);
}
public boolean getBoolean(@NotNull String path, boolean def) {
config.addDefault(path, def);
return config.getBoolean(path, def);
}
public @NotNull String getString(@NotNull String path, @NotNull String def, @NotNull String comment) {
config.addDefault(path, def, comment);
return config.getString(path, def);
}
public @NotNull String getString(@NotNull String path, @NotNull String def) {
config.addDefault(path, def);
return config.getString(path, def);
}
public double getDouble(@NotNull String path, @NotNull Double def, @NotNull String comment) {
config.addDefault(path, def, comment);
return config.getDouble(path, def);
}
public double getDouble(@NotNull String path, @NotNull Double def) {
config.addDefault(path, def);
return config.getDouble(path, def);
}
public int getInt(@NotNull String path, int def, @NotNull String comment) {
config.addDefault(path, def, comment);
return config.getInteger(path, def);
}
public int getInt(@NotNull String path, int def) {
config.addDefault(path, def);
return config.getInteger(path, def);
}
public @NotNull List<String> getList(@NotNull String path, @NotNull List<String> def, @NotNull String comment) {
config.addDefault(path, def, comment);
return config.getStringList(path);
}
public @NotNull List<String> getList(@NotNull String path, @NotNull List<String> def) {
config.addDefault(path, def);
return config.getStringList(path);
}
public @NotNull ConfigSection getConfigSection(@NotNull String path, @NotNull Map<String, Object> defaultKeyValue) {
config.addDefault(path, null);
config.makeSectionLenient(path);
defaultKeyValue.forEach((string, object) -> config.addExample(path+"."+string, object));
return config.getConfigSection(path);
}
public @NotNull ConfigSection getConfigSection(@NotNull String path, @NotNull Map<String, Object> defaultKeyValue, @NotNull String comment) {
config.addDefault(path, null, comment);
config.makeSectionLenient(path);
defaultKeyValue.forEach((string, object) -> config.addExample(path+"."+string, object));
return config.getConfigSection(path);
}
public void addComment(@NotNull String path, @NotNull String comment) {
config.addComment(path, comment);
}
}

View File

@ -0,0 +1,109 @@
package me.xginko.villageroptimizer.config;
import io.github.thatsmusic99.configurationmaster.api.ConfigFile;
import me.xginko.villageroptimizer.VillagerOptimizer;
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.List;
public class LanguageCache {
private final @NotNull ConfigFile lang;
public final @NotNull Component no_permission;
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,
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;
public LanguageCache(String locale) throws Exception {
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
File langYML = new File(plugin.getDataFolder() + File.separator + "lang", locale + ".yml");
// Check if the lang folder has already been created
File parent = langYML.getParentFile();
if (!parent.exists() && !parent.mkdir())
VillagerOptimizer.getLog().severe("Unable 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
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",
List.of("<green>All trades have been restocked! Next restock in %time%"));
this.optimize_for_trading = getListTranslation("messages.optimize-to-trade",
List.of("<red>You need to optimize this villager before you can trade with it."));
this.villager_leveling_up = getListTranslation("messages.villager-leveling-up",
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",
List.of("<green>Successfully optimized villager by using a nametag."));
this.nametag_on_optimize_cooldown = getListTranslation("messages.nametag.optimize-on-cooldown",
List.of("<gray>You need to wait %time% until you can optimize this villager again."));
this.nametag_unoptimize_success = getListTranslation("messages.nametag.unoptimize-success",
List.of("<green>Successfully unoptimized villager by using a nametag."));
// Block
this.block_optimize_success = getListTranslation("messages.block.optimize-success",
List.of("<green>%villagertype% villager successfully optimized using block %blocktype%."));
this.block_on_optimize_cooldown = getListTranslation("messages.block.optimize-on-cooldown",
List.of("<gray>You need to wait %time% until you can optimize this villager again."));
this.block_unoptimize_success = getListTranslation("messages.block.unoptimize-success",
List.of("<green>Successfully unoptimized %villagertype% villager by removing %blocktype%."));
// Workstation
this.workstation_optimize_success = getListTranslation("messages.workstation.optimize-success",
List.of("<green>%villagertype% villager successfully optimized using workstation %workstation%."));
this.workstation_on_optimize_cooldown = getListTranslation("messages.workstation.optimize-on-cooldown",
List.of("<gray>You need to wait %time% until you can optimize this villager again."));
this.workstation_unoptimize_success = getListTranslation("messages.workstation.unoptimize-success",
List.of("<green>Successfully unoptimized %villagertype% villager by removing workstation block %workstation%."));
// Command
this.command_optimize_success = getListTranslation("messages.command.optimize-success",
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",
List.of("<red>The radius you entered exceeds the limit of %distance% blocks."));
this.command_optimize_fail = getListTranslation("messages.command.optimize-fail",
List.of("<gray>%amount% villagers couldn't be optimized because they have recently been optimized."));
this.command_unoptimize_success = getListTranslation("messages.command.unoptimize-success",
List.of("<green>Successfully unoptimized %amount% villager(s) in a radius of %radius% blocks."));
this.command_specify_radius = getListTranslation("messages.command.specify-radius",
List.of("<red>Please specify a radius."));
this.command_radius_invalid = getListTranslation("messages.command.radius-invalid",
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",
List.of("<gray>Couldn't find any employed villagers within a radius of %radius%."));
try {
lang.save();
} catch (Exception e) {
VillagerOptimizer.getLog().severe("Failed to save language file: "+ lang.getFile().getName() +" - " + e.getLocalizedMessage());
}
}
public @NotNull Component getTranslation(@NotNull String path, @NotNull String defaultTranslation) {
lang.addDefault(path, defaultTranslation);
return MiniMessage.miniMessage().deserialize(lang.getString(path, defaultTranslation));
}
public @NotNull Component getTranslation(@NotNull String path, @NotNull String defaultTranslation, @NotNull String comment) {
lang.addDefault(path, defaultTranslation, comment);
return MiniMessage.miniMessage().deserialize(lang.getString(path, defaultTranslation));
}
public @NotNull List<Component> getListTranslation(@NotNull String path, @NotNull List<String> defaultTranslation) {
lang.addDefault(path, defaultTranslation);
return lang.getStringList(path).stream().map(MiniMessage.miniMessage()::deserialize).toList();
}
public @NotNull List<Component> getListTranslation(@NotNull String path, @NotNull List<String> defaultTranslation, @NotNull String comment) {
lang.addDefault(path, defaultTranslation, comment);
return lang.getStringList(path).stream().map(MiniMessage.miniMessage()::deserialize).toList();
}
}

View File

@ -0,0 +1,24 @@
package me.xginko.villageroptimizer.enums;
import me.xginko.villageroptimizer.VillagerOptimizer;
import org.bukkit.NamespacedKey;
public enum Keys {
OPTIMIZATION_TYPE(VillagerOptimizer.getKey("optimization-type")),
LAST_OPTIMIZE(VillagerOptimizer.getKey("last-optimize")),
LAST_LEVELUP(VillagerOptimizer.getKey("last-levelup")),
LAST_RESTOCK(VillagerOptimizer.getKey("last-restock")),
LAST_OPTIMIZE_NAME(VillagerOptimizer.getKey("last-optimize-name"));
private final NamespacedKey key;
Keys(NamespacedKey key) {
this.key = key;
}
public NamespacedKey key() {
return key;
}
}

View File

@ -1,11 +1,11 @@
package me.xginko.villageroptimizer.struct.enums;
package me.xginko.villageroptimizer.enums;
public enum OptimizationType {
CHUNK_LIMIT,
REGIONAL_ACTIVITY,
COMMAND,
NAMETAG,
WORKSTATION,
BLOCK,
NONE
}

View File

@ -0,0 +1,45 @@
package me.xginko.villageroptimizer.enums;
public class Permissions {
public enum Commands {
VERSION("villageroptimizer.cmd.version"),
RELOAD("villageroptimizer.cmd.reload"),
DISABLE("villageroptimizer.cmd.disable"),
OPTIMIZE_RADIUS("villageroptimizer.cmd.optimize"),
UNOPTIMIZE_RADIUS("villageroptimizer.cmd.unoptimize");
private final String permission;
Commands(String permission) {
this.permission = permission;
}
public String get() {
return permission;
}
}
public enum Optimize {
NAMETAG("villageroptimizer.optimize.nametag"),
BLOCK("villageroptimizer.optimize.block"),
WORKSTATION("villageroptimizer.optimize.workstation");
private final String permission;
Optimize(String permission) {
this.permission = permission;
}
public String get() {
return permission;
}
}
public enum Bypass {
TRADE_PREVENTION("villageroptimizer.bypass.tradeprevention"),
RESTOCK_COOLDOWN("villageroptimizer.bypass.restockcooldown"),
NAMETAG_COOLDOWN("villageroptimizer.bypass.nametagcooldown"),
BLOCK_COOLDOWN("villageroptimizer.bypass.blockcooldown"),
WORKSTATION_COOLDOWN("villageroptimizer.bypass.workstationcooldown"),
COMMAND_COOLDOWN("villageroptimizer.bypass.commandcooldown");
private final String permission;
Bypass(String permission) {
this.permission = permission;
}
public String 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;
@ -13,37 +13,28 @@ public class VillagerOptimizeEvent extends Event implements Cancellable {
private static final @NotNull HandlerList handlers = new HandlerList();
private final @NotNull WrappedVillager wrappedVillager;
private @NotNull OptimizationType optimizationType;
private @NotNull OptimizationType type;
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 type, @Nullable Player whoOptimised, boolean isAsync) throws IllegalArgumentException {
super(isAsync);
this.wrappedVillager = wrappedVillager;
this.whoOptimised = whoOptimised;
if (optimizationType.equals(OptimizationType.NONE)) {
if (type.equals(OptimizationType.NONE)) {
throw new IllegalArgumentException("OptimizationType can't be NONE.");
} else {
this.optimizationType = optimizationType;
this.type = type;
}
}
public VillagerOptimizeEvent(
@NotNull WrappedVillager wrappedVillager,
@NotNull OptimizationType optimizationType,
@Nullable Player whoOptimised
) throws IllegalArgumentException {
public VillagerOptimizeEvent(@NotNull WrappedVillager wrappedVillager, @NotNull OptimizationType type, @Nullable Player whoOptimised) throws IllegalArgumentException {
this.wrappedVillager = wrappedVillager;
this.whoOptimised = whoOptimised;
if (optimizationType.equals(OptimizationType.NONE)) {
if (type.equals(OptimizationType.NONE)) {
throw new IllegalArgumentException("OptimizationType can't be NONE.");
} else {
this.optimizationType = optimizationType;
this.type = type;
}
}
@ -52,14 +43,14 @@ public class VillagerOptimizeEvent extends Event implements Cancellable {
}
public @NotNull OptimizationType getOptimizationType() {
return optimizationType;
return type;
}
public void setOptimizationType(@NotNull OptimizationType optimizationType) throws IllegalArgumentException {
if (optimizationType.equals(OptimizationType.NONE)) {
public void setOptimizationType(@NotNull OptimizationType type) throws IllegalArgumentException {
if (type.equals(OptimizationType.NONE)) {
throw new IllegalArgumentException("OptimizationType can't be NONE.");
} else {
this.optimizationType = optimizationType;
this.type = type;
}
}

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;
@ -13,30 +13,21 @@ public class VillagerUnoptimizeEvent extends Event implements Cancellable {
private static final @NotNull HandlerList handlers = new HandlerList();
private final @NotNull WrappedVillager wrappedVillager;
private final @NotNull OptimizationType unOptimizeType;
private final @NotNull OptimizationType unoptimizeType;
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;
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;
this.unoptimizeType = unoptimizeType;
}
public @NotNull WrappedVillager getWrappedVillager() {
@ -48,7 +39,7 @@ public class VillagerUnoptimizeEvent extends Event implements Cancellable {
}
public @NotNull OptimizationType getWhichTypeUnoptimized() {
return unOptimizeType;
return unoptimizeType;
}
@Override

View File

@ -0,0 +1,171 @@
package me.xginko.villageroptimizer.modules;
import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.utils.LogUtil;
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;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.CreatureSpawnEvent;
import org.bukkit.event.player.PlayerInteractEntityEvent;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.logging.Level;
public class VillagerChunkLimit implements VillagerOptimizerModule, Listener, Runnable {
private final Server server;
private final VillagerCache villagerCache;
private final List<Villager.Profession> non_optimized_removal_priority = new ArrayList<>(16);
private final List<Villager.Profession> optimized_removal_priority = new ArrayList<>(16);
private final long check_period;
private final int non_optimized_max_per_chunk, optimized_max_per_chunk;
private final boolean log_enabled;
protected VillagerChunkLimit() {
shouldEnable();
this.server = VillagerOptimizer.getInstance().getServer();
this.villagerCache = VillagerOptimizer.getCache();
Config config = VillagerOptimizer.getConfiguration();
config.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.log_enabled = config.getBoolean("villager-chunk-limit.log-removals", false);
this.non_optimized_max_per_chunk = config.getInt("villager-chunk-limit.unoptimized.max-per-chunk", 20,
"The maximum amount of unoptimized villagers per chunk.");
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"""
).forEach(configuredProfession -> {
try {
Villager.Profession profession = Villager.Profession.valueOf(configuredProfession);
this.non_optimized_removal_priority.add(profession);
} catch (IllegalArgumentException e) {
LogUtil.moduleLog(Level.WARNING, "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.");
}
});
this.optimized_max_per_chunk = config.getInt("villager-chunk-limit.optimized.max-per-chunk", 60,
"The maximum amount of optimized villagers per chunk.");
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"
)).forEach(configuredProfession -> {
try {
Villager.Profession profession = Villager.Profession.valueOf(configuredProfession);
this.optimized_removal_priority.add(profession);
} catch (IllegalArgumentException e) {
LogUtil.moduleLog(Level.WARNING, "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.");
}
});
}
@Override
public void enable() {
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
server.getPluginManager().registerEvents(this, plugin);
server.getScheduler().scheduleSyncRepeatingTask(plugin, this, check_period, check_period);
}
@Override
public boolean shouldEnable() {
return VillagerOptimizer.getConfiguration().getBoolean("villager-chunk-limit.enable", false);
}
@Override
public void run() {
for (World world : server.getWorlds()) {
for (Chunk chunk : world.getLoadedChunks()) {
this.manageVillagerCount(chunk);
}
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onCreatureSpawn(CreatureSpawnEvent event) {
Entity spawned = event.getEntity();
if (spawned.getType().equals(EntityType.VILLAGER)) {
this.manageVillagerCount(spawned.getChunk());
}
}
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
private void onInteract(PlayerInteractEntityEvent event) {
Entity clicked = event.getRightClicked();
if (clicked.getType().equals(EntityType.VILLAGER)) {
this.manageVillagerCount(clicked.getChunk());
}
}
private void manageVillagerCount(@NotNull Chunk 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().equals(EntityType.VILLAGER)) {
Villager villager = (Villager) entity;
if (villagerCache.getOrAdd(villager).isOptimized()) {
optimized_villagers.add(villager);
} else {
not_optimized_villagers.add(villager);
}
}
}
// Check if there are more unoptimized villagers in that chunk than allowed
final int not_optimized_villagers_too_many = not_optimized_villagers.size() - non_optimized_max_per_chunk;
if (not_optimized_villagers_too_many > 0) {
// Sort villagers by profession priority
not_optimized_villagers.sort(Comparator.comparingInt(villager -> {
final Villager.Profession profession = villager.getProfession();
return non_optimized_removal_priority.contains(profession) ? non_optimized_removal_priority.indexOf(profession) : Integer.MAX_VALUE;
}));
// 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);
villager.remove();
if (log_enabled) LogUtil.moduleLog(Level.INFO, "villager-chunk-limit",
"Removed unoptimized villager of profession type '"+villager.getProfession().name()+"' at "+villager.getLocation()
);
}
}
// Check if there are more optimized villagers in that chunk than allowed
final int optimized_villagers_too_many = optimized_villagers.size() - optimized_max_per_chunk;
if (optimized_villagers_too_many > 0) {
// Sort villagers by profession priority
optimized_villagers.sort(Comparator.comparingInt(villager -> {
final Villager.Profession profession = villager.getProfession();
return optimized_removal_priority.contains(profession) ? optimized_removal_priority.indexOf(profession) : Integer.MAX_VALUE;
}));
// Remove prioritized villagers that are too many
for (int i = 0; i < optimized_villagers_too_many; i++) {
Villager villager = optimized_villagers.get(i);
villager.remove();
if (log_enabled) LogUtil.moduleLog(Level.INFO, "villager-chunk-limit",
"Removed optimized villager of profession type '"+villager.getProfession().name()+"' at "+villager.getLocation()
);
}
}
}
}

View File

@ -0,0 +1,43 @@
package me.xginko.villageroptimizer.modules;
import me.xginko.villageroptimizer.VillagerOptimizer;
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 org.bukkit.event.HandlerList;
import java.util.HashSet;
public interface VillagerOptimizerModule {
void enable();
boolean shouldEnable();
HashSet<VillagerOptimizerModule> modules = new HashSet<>();
static void reloadModules() {
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
HandlerList.unregisterAll(plugin);
plugin.getServer().getScheduler().cancelTasks(plugin);
modules.clear();
modules.add(new OptimizeByNametag());
modules.add(new OptimizeByBlock());
modules.add(new OptimizeByWorkstation());
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 VillagerChunkLimit());
modules.forEach(module -> {
if (module.shouldEnable()) module.enable();
});
}
}

View File

@ -0,0 +1,84 @@
package me.xginko.villageroptimizer.modules.gameplay;
import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.WrappedVillager;
import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import me.xginko.villageroptimizer.utils.CommonUtil;
import net.kyori.adventure.text.TextReplacementConfig;
import org.bukkit.entity.Player;
import org.bukkit.entity.Villager;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
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;
public class LevelOptimizedProfession implements VillagerOptimizerModule, Listener {
private final VillagerOptimizer plugin;
private final VillagerCache villagerCache;
private final boolean notify_player;
private final long cooldown;
public LevelOptimizedProfession() {
shouldEnable();
this.plugin = VillagerOptimizer.getInstance();
this.villagerCache = VillagerOptimizer.getCache();
Config config = VillagerOptimizer.getConfiguration();
config.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 = 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.""") * 1000L;
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);
}
@Override
public boolean shouldEnable() {
return true;
}
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
private void onTradeScreenClose(InventoryCloseEvent event) {
if (
event.getInventory().getType().equals(InventoryType.MERCHANT)
&& event.getInventory().getHolder() instanceof Villager villager
) {
WrappedVillager wVillager = villagerCache.getOrAdd(villager);
if (!wVillager.isOptimized()) return;
if (wVillager.canLevelUp(cooldown)) {
if (wVillager.calculateLevel() > villager.getVillagerLevel()) {
villager.addPotionEffect(new PotionEffect(PotionEffectType.SLOW, 120, 120, false, false));
villager.setAware(true);
plugin.getServer().getScheduler().scheduleSyncDelayedTask(plugin, () -> {
villager.setAware(false);
wVillager.saveLastLevelUp();
}, 100L);
}
} else {
if (notify_player) {
Player player = (Player) event.getPlayer();
final TextReplacementConfig timeLeft = TextReplacementConfig.builder()
.matchLiteral("%time%")
.replacement(CommonUtil.formatTime(wVillager.getLevelCooldownMillis(cooldown)))
.build();
VillagerOptimizer.getLang(player.locale()).villager_leveling_up.forEach(line -> player.sendMessage(line.replaceText(timeLeft)));
}
}
}
}
}

View File

@ -0,0 +1,38 @@
package me.xginko.villageroptimizer.modules.gameplay;
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;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.CreatureSpawnEvent;
public class MakeVillagersSpawnAdult implements VillagerOptimizerModule, Listener {
public MakeVillagersSpawnAdult() {}
@Override
public void enable() {
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@Override
public boolean shouldEnable() {
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().equals(EntityType.VILLAGER)) {
Villager villager = (Villager) event.getEntity();
if (!villager.isAdult()) villager.setAdult();
}
}
}

View File

@ -0,0 +1,65 @@
package me.xginko.villageroptimizer.modules.gameplay;
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.LogUtil;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Villager;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageEvent;
import java.util.Arrays;
import java.util.HashSet;
public class PreventOptimizedDamage implements VillagerOptimizerModule, Listener {
private final VillagerCache villagerCache;
private final HashSet<EntityDamageEvent.DamageCause> damage_causes_to_cancel = new HashSet<>();
public PreventOptimizedDamage() {
shouldEnable();
this.villagerCache = VillagerOptimizer.getCache();
Config config = VillagerOptimizer.getConfiguration();
config.addComment("gameplay.prevent-damage-to-optimized.enable",
"Configure what kind of damage you want to cancel for optimized villagers here.");
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"""
).forEach(configuredDamageCause -> {
try {
EntityDamageEvent.DamageCause damageCause = EntityDamageEvent.DamageCause.valueOf(configuredDamageCause);
this.damage_causes_to_cancel.add(damageCause);
} catch (IllegalArgumentException e) {
LogUtil.damageCauseNotRecognized("prevent-damage-to-optimized", configuredDamageCause);
}
});
}
@Override
public void enable() {
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@Override
public boolean shouldEnable() {
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().equals(EntityType.VILLAGER)
&& damage_causes_to_cancel.contains(event.getCause())
&& villagerCache.getOrAdd((Villager) event.getEntity()).isOptimized()
) {
event.setCancelled(true);
}
}
}

View File

@ -0,0 +1,73 @@
package me.xginko.villageroptimizer.modules.gameplay;
import com.destroystokyo.paper.event.entity.EntityPathfindEvent;
import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
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;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.entity.EntityTargetEvent;
public class PreventOptimizedTargeting implements VillagerOptimizerModule, Listener {
private final VillagerCache villagerCache;
public PreventOptimizedTargeting() {
this.villagerCache = VillagerOptimizer.getCache();
}
@Override
public void enable() {
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@Override
public boolean shouldEnable() {
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) {
// Yes, instanceof checks would look way more beautiful here but checking type is much faster
Entity target = event.getTarget();
if (
target != null
&& target.getType().equals(EntityType.VILLAGER)
&& villagerCache.getOrAdd((Villager) target).isOptimized()
) {
event.setTarget(null);
event.setCancelled(true);
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onEntityTargetVillager(EntityPathfindEvent event) {
Entity target = event.getTargetEntity();
if (
target != null
&& target.getType().equals(EntityType.VILLAGER)
&& villagerCache.getOrAdd((Villager) target).isOptimized()
) {
event.setCancelled(true);
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onEntityAttackVillager(EntityDamageByEntityEvent event) {
if (
event.getEntityType().equals(EntityType.VILLAGER)
&& event.getDamager() instanceof Mob attacker
&& villagerCache.getOrAdd((Villager) event.getEntity()).isOptimized()
) {
attacker.setTarget(null);
}
}
}

View File

@ -0,0 +1,74 @@
package me.xginko.villageroptimizer.modules.gameplay;
import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.enums.Permissions;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import org.bukkit.entity.Player;
import org.bukkit.entity.Villager;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.event.inventory.TradeSelectEvent;
public class PreventUnoptimizedTrading implements VillagerOptimizerModule, Listener {
private final VillagerCache villagerCache;
private final boolean notify_player;
public PreventUnoptimizedTrading() {
shouldEnable();
this.villagerCache = VillagerOptimizer.getCache();
Config config = VillagerOptimizer.getConfiguration();
config.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);
}
@Override
public boolean shouldEnable() {
return VillagerOptimizer.getConfiguration().getBoolean("gameplay.prevent-trading-with-unoptimized.enable", false);
}
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
private void onTradeOpen(TradeSelectEvent event) {
Player player = (Player) event.getWhoClicked();
if (player.hasPermission(Permissions.Bypass.TRADE_PREVENTION.get())) return;
if (
event.getInventory().getType().equals(InventoryType.MERCHANT)
&& event.getInventory().getHolder() instanceof Villager villager
&& !villagerCache.getOrAdd(villager).isOptimized()
) {
event.setCancelled(true);
if (notify_player)
VillagerOptimizer.getLang(player.locale()).optimize_for_trading.forEach(player::sendMessage);
}
}
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
private void onInventoryClick(InventoryClickEvent event) {
Player player = (Player) event.getWhoClicked();
if (player.hasPermission(Permissions.Bypass.TRADE_PREVENTION.get())) return;
if (
event.getInventory().getType().equals(InventoryType.MERCHANT)
&& event.getInventory().getHolder() instanceof Villager villager
&& !villagerCache.getOrAdd(villager).isOptimized()
) {
event.setCancelled(true);
if (notify_player)
VillagerOptimizer.getLang(player.locale()).optimize_for_trading.forEach(player::sendMessage);
}
}
}

View File

@ -0,0 +1,73 @@
package me.xginko.villageroptimizer.modules.gameplay;
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.Listener;
public class RenameOptimizedVillagers implements VillagerOptimizerModule, Listener {
private final VillagerOptimizer plugin;
private final Component optimized_name;
private final boolean overwrite_previous_name;
public RenameOptimizedVillagers() {
shouldEnable();
this.plugin = VillagerOptimizer.getInstance();
Config config = VillagerOptimizer.getConfiguration();
config.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() {
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@Override
public boolean shouldEnable() {
return VillagerOptimizer.getConfiguration().getBoolean("gameplay.rename-optimized-villagers.enable", true);
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onOptimize(VillagerOptimizeEvent event) {
WrappedVillager wVillager = event.getWrappedVillager();
Villager villager = wVillager.villager();
plugin.getServer().getScheduler().scheduleSyncDelayedTask(plugin, () -> {
if (overwrite_previous_name || villager.customName() == null) {
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();
plugin.getServer().getScheduler().scheduleSyncDelayedTask(plugin, () -> {
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

@ -0,0 +1,74 @@
package me.xginko.villageroptimizer.modules.gameplay;
import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.WrappedVillager;
import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.enums.Permissions;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import me.xginko.villageroptimizer.utils.CommonUtil;
import net.kyori.adventure.text.TextReplacementConfig;
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;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerInteractEntityEvent;
public class RestockOptimizedTrades implements VillagerOptimizerModule, Listener {
private final VillagerCache villagerCache;
private final long restock_delay_millis;
private final boolean log_enabled, notify_player;
public RestockOptimizedTrades() {
shouldEnable();
this.villagerCache = VillagerOptimizer.getCache();
Config config = VillagerOptimizer.getConfiguration();
config.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("gameplay.restock-optimized-trades.log", false);
}
@Override
public void enable() {
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@Override
public boolean shouldEnable() {
return true;
}
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
private void onInteract(PlayerInteractEntityEvent event) {
if (!event.getRightClicked().getType().equals(EntityType.VILLAGER)) return;
WrappedVillager wVillager = villagerCache.getOrAdd((Villager) event.getRightClicked());
if (!wVillager.isOptimized()) return;
Player player = event.getPlayer();
final boolean player_bypassing = player.hasPermission(Permissions.Bypass.RESTOCK_COOLDOWN.get());
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.formatTime(wVillager.getRestockCooldownMillis(restock_delay_millis)))
.build();
VillagerOptimizer.getLang(player.locale()).trades_restocked.forEach(line -> player.sendMessage(line.replaceText(timeLeft)));
}
if (log_enabled)
VillagerOptimizer.getLog().info("Restocked optimized villager at "+ wVillager.villager().getLocation());
}
}
}

View File

@ -0,0 +1,193 @@
package me.xginko.villageroptimizer.modules.optimization;
import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.WrappedVillager;
import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.enums.OptimizationType;
import me.xginko.villageroptimizer.enums.Permissions;
import me.xginko.villageroptimizer.events.VillagerOptimizeEvent;
import me.xginko.villageroptimizer.events.VillagerUnoptimizeEvent;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import me.xginko.villageroptimizer.utils.CommonUtil;
import me.xginko.villageroptimizer.utils.LogUtil;
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;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import java.util.HashSet;
import java.util.List;
public class OptimizeByBlock implements VillagerOptimizerModule, Listener {
private final VillagerCache villagerCache;
private final HashSet<Material> blocks_that_disable = new HashSet<>(4);
private final long cooldown;
private final double search_radius;
private final boolean only_while_sneaking, notify_player, log_enabled;
public OptimizeByBlock() {
shouldEnable();
this.villagerCache = VillagerOptimizer.getCache();
Config config = VillagerOptimizer.getConfiguration();
config.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.""");
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."
).forEach(configuredMaterial -> {
try {
Material disableBlock = Material.valueOf(configuredMaterial);
this.blocks_that_disable.add(disableBlock);
} catch (IllegalArgumentException e) {
LogUtil.materialNotRecognized("block-optimization", configuredMaterial);
}
});
this.cooldown = 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.""") * 1000L;
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("optimization-methods.block-optimization.log", false);
}
@Override
public void enable() {
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@Override
public boolean shouldEnable() {
return VillagerOptimizer.getConfiguration().getBoolean("optimization-methods.block-optimization.enable", false);
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onBlockPlace(BlockPlaceEvent event) {
Block placed = event.getBlock();
if (!blocks_that_disable.contains(placed.getType())) return;
Player player = event.getPlayer();
if (!player.hasPermission(Permissions.Optimize.BLOCK.get())) return;
if (only_while_sneaking && !player.isSneaking()) return;
final Location blockLoc = placed.getLocation();
WrappedVillager closestOptimizableVillager = null;
double closestDistance = Double.MAX_VALUE;
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;
WrappedVillager wVillager = villagerCache.getOrAdd(villager);
final double distance = entity.getLocation().distance(blockLoc);
if (distance < closestDistance && wVillager.canOptimize(cooldown)) {
closestOptimizableVillager = wVillager;
closestDistance = distance;
}
}
if (closestOptimizableVillager == null) return;
if (closestOptimizableVillager.canOptimize(cooldown) || player.hasPermission(Permissions.Bypass.BLOCK_COOLDOWN.get())) {
VillagerOptimizeEvent optimizeEvent = new VillagerOptimizeEvent(closestOptimizableVillager, OptimizationType.BLOCK, player, event.isAsynchronous());
if (!optimizeEvent.callEvent()) return;
closestOptimizableVillager.setOptimization(optimizeEvent.getOptimizationType());
closestOptimizableVillager.saveOptimizeTime();
if (notify_player) {
final TextReplacementConfig vilProfession = TextReplacementConfig.builder()
.matchLiteral("%vil_profession%")
.replacement(closestOptimizableVillager.villager().getProfession().toString().toLowerCase())
.build();
final TextReplacementConfig placedMaterial = TextReplacementConfig.builder()
.matchLiteral("%blocktype%")
.replacement(placed.getType().toString().toLowerCase())
.build();
VillagerOptimizer.getLang(player.locale()).block_optimize_success.forEach(line -> player.sendMessage(line
.replaceText(vilProfession)
.replaceText(placedMaterial)
));
}
if (log_enabled)
VillagerOptimizer.getLog().info("Villager was optimized by block at "+closestOptimizableVillager.villager().getLocation());
} else {
if (notify_player) {
final TextReplacementConfig timeLeft = TextReplacementConfig.builder()
.matchLiteral("%time%")
.replacement(CommonUtil.formatTime(closestOptimizableVillager.getOptimizeCooldownMillis(cooldown)))
.build();
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) {
Block broken = event.getBlock();
if (!blocks_that_disable.contains(broken.getType())) return;
Player player = event.getPlayer();
if (!player.hasPermission(Permissions.Optimize.BLOCK.get())) return;
if (only_while_sneaking && !player.isSneaking()) return;
final Location blockLoc = broken.getLocation();
WrappedVillager closestOptimizedVillager = null;
double closestDistance = Double.MAX_VALUE;
for (Entity entity : blockLoc.getNearbyEntities(search_radius, search_radius, search_radius)) {
if (!entity.getType().equals(EntityType.VILLAGER)) continue;
Villager villager = (Villager) entity;
WrappedVillager wVillager = villagerCache.getOrAdd(villager);
final double distance = entity.getLocation().distance(blockLoc);
if (distance < closestDistance && wVillager.isOptimized()) {
closestOptimizedVillager = wVillager;
closestDistance = distance;
}
}
if (closestOptimizedVillager == null) return;
VillagerUnoptimizeEvent unOptimizeEvent = new VillagerUnoptimizeEvent(closestOptimizedVillager, player, OptimizationType.BLOCK, event.isAsynchronous());
if (!unOptimizeEvent.callEvent()) return;
closestOptimizedVillager.setOptimization(OptimizationType.NONE);
if (notify_player) {
final TextReplacementConfig vilProfession = TextReplacementConfig.builder()
.matchLiteral("%vil_profession%")
.replacement(closestOptimizedVillager.villager().getProfession().toString().toLowerCase())
.build();
final TextReplacementConfig brokenMaterial = TextReplacementConfig.builder()
.matchLiteral("%blocktype%")
.replacement(broken.getType().toString().toLowerCase())
.build();
VillagerOptimizer.getLang(player.locale()).block_unoptimize_success.forEach(line -> player.sendMessage(line
.replaceText(vilProfession)
.replaceText(brokenMaterial)
));
}
if (log_enabled)
VillagerOptimizer.getLog().info("Villager unoptimized because nearby optimization block broken at: "+closestOptimizedVillager.villager().getLocation());
}
}

View File

@ -0,0 +1,126 @@
package me.xginko.villageroptimizer.modules.optimization;
import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.WrappedVillager;
import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.enums.OptimizationType;
import me.xginko.villageroptimizer.enums.Permissions;
import me.xginko.villageroptimizer.events.VillagerOptimizeEvent;
import me.xginko.villageroptimizer.events.VillagerUnoptimizeEvent;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import me.xginko.villageroptimizer.utils.CommonUtil;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextReplacementConfig;
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;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerInteractEntityEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import java.util.HashSet;
import java.util.List;
public class OptimizeByNametag implements VillagerOptimizerModule, Listener {
private final VillagerCache villagerCache;
private final HashSet<String> nametags = new HashSet<>(4);
private final long cooldown;
private final boolean consume_nametag, notify_player, log_enabled;
public OptimizeByNametag() {
shouldEnable();
this.villagerCache = VillagerOptimizer.getCache();
Config config = VillagerOptimizer.getConfiguration();
config.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.addAll(config.getList("optimization-methods.nametag-optimization.names", List.of("Optimize", "DisableAI"),
"Names are case insensitive, capital letters won't matter.").stream().map(String::toLowerCase).toList());
this.consume_nametag = config.getBoolean("optimization-methods.nametag-optimization.nametags-get-consumed", true,
"Enable or disable consumption of the used nametag item.");
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("optimization-methods.nametag-optimization.log", false);
}
@Override
public void enable() {
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@Override
public boolean shouldEnable() {
return VillagerOptimizer.getConfiguration().getBoolean("optimization-methods.nametag-optimization.enable", true);
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onPlayerInteractEntity(PlayerInteractEntityEvent event) {
if (!event.getRightClicked().getType().equals(EntityType.VILLAGER)) return;
Player player = event.getPlayer();
if (!player.hasPermission(Permissions.Optimize.NAMETAG.get())) return;
ItemStack usedItem = player.getInventory().getItem(event.getHand());
if (usedItem == null || !usedItem.getType().equals(Material.NAME_TAG)) return;
ItemMeta meta = usedItem.getItemMeta();
if (!meta.hasDisplayName()) return;
// 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(Permissions.Bypass.NAMETAG_COOLDOWN.get())) {
VillagerOptimizeEvent optimizeEvent = new VillagerOptimizeEvent(wVillager, OptimizationType.NAMETAG, player, event.isAsynchronous());
if (!optimizeEvent.callEvent()) return;
if (!consume_nametag) {
event.setCancelled(true);
villager.customName(newVillagerName);
}
wVillager.setOptimization(optimizeEvent.getOptimizationType());
wVillager.saveOptimizeTime();
if (notify_player)
VillagerOptimizer.getLang(player.locale()).nametag_optimize_success.forEach(player::sendMessage);
if (log_enabled)
VillagerOptimizer.getLog().info(player.getName() + " optimized a villager using nametag: '" + name + "'");
} else {
event.setCancelled(true);
if (notify_player) {
final TextReplacementConfig timeLeft = TextReplacementConfig.builder()
.matchLiteral("%time%")
.replacement(CommonUtil.formatTime(wVillager.getOptimizeCooldownMillis(cooldown)))
.build();
VillagerOptimizer.getLang(player.locale()).nametag_on_optimize_cooldown.forEach(line -> player.sendMessage(line.replaceText(timeLeft)));
}
}
} else {
if (wVillager.isOptimized()) {
VillagerUnoptimizeEvent unOptimizeEvent = new VillagerUnoptimizeEvent(wVillager, player, OptimizationType.NAMETAG, event.isAsynchronous());
if (!unOptimizeEvent.callEvent()) return;
wVillager.setOptimization(OptimizationType.NONE);
if (notify_player)
VillagerOptimizer.getLang(player.locale()).nametag_unoptimize_success.forEach(player::sendMessage);
if (log_enabled)
VillagerOptimizer.getLog().info(event.getPlayer().getName() + " disabled optimizations for a villager using nametag: '" + name + "'");
}
}
}
}

View File

@ -0,0 +1,200 @@
package me.xginko.villageroptimizer.modules.optimization;
import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.WrappedVillager;
import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.enums.OptimizationType;
import me.xginko.villageroptimizer.enums.Permissions;
import me.xginko.villageroptimizer.events.VillagerOptimizeEvent;
import me.xginko.villageroptimizer.events.VillagerUnoptimizeEvent;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import me.xginko.villageroptimizer.utils.CommonUtil;
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;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent;
public class OptimizeByWorkstation implements VillagerOptimizerModule, Listener {
private final VillagerCache villagerCache;
private final long cooldown;
private final double search_radius;
private final boolean only_while_sneaking, log_enabled, notify_player;
public OptimizeByWorkstation() {
shouldEnable();
this.villagerCache = VillagerOptimizer.getCache();
Config config = VillagerOptimizer.getConfiguration();
config.addComment("optimization-methods.workstation-optimization.enable", """
When enabled, the closest villager near a matching workstation being placed will be optimized.\s
If a nearby matching workstation is broken, the villager will become unoptimized again.""");
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.""") / 2;
this.cooldown = 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.""") * 1000L;
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("optimization-methods.workstation-optimization.log", false);
}
@Override
public void enable() {
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@Override
public boolean shouldEnable() {
return VillagerOptimizer.getConfiguration().getBoolean("optimization-methods.workstation-optimization.enable", false);
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onBlockPlace(BlockPlaceEvent event) {
Block placed = event.getBlock();
Villager.Profession workstationProfession = getWorkstationProfession(placed.getType());
if (workstationProfession.equals(Villager.Profession.NONE)) return;
Player player = event.getPlayer();
if (!player.hasPermission(Permissions.Optimize.WORKSTATION.get())) return;
if (only_while_sneaking && !player.isSneaking()) return;
final Location workstationLoc = placed.getLocation();
WrappedVillager closestOptimizableVillager = null;
double closestDistance = Double.MAX_VALUE;
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 double distance = entity.getLocation().distance(workstationLoc);
if (distance < closestDistance && wVillager.canOptimize(cooldown)) {
closestOptimizableVillager = wVillager;
closestDistance = distance;
}
}
if (closestOptimizableVillager == null) return;
if (closestOptimizableVillager.canOptimize(cooldown) || player.hasPermission(Permissions.Bypass.WORKSTATION_COOLDOWN.get())) {
VillagerOptimizeEvent optimizeEvent = new VillagerOptimizeEvent(closestOptimizableVillager, OptimizationType.WORKSTATION, player, event.isAsynchronous());
if (!optimizeEvent.callEvent()) return;
closestOptimizableVillager.setOptimization(optimizeEvent.getOptimizationType());
closestOptimizableVillager.saveOptimizeTime();
if (notify_player) {
final TextReplacementConfig vilProfession = TextReplacementConfig.builder()
.matchLiteral("%vil_profession%")
.replacement(closestOptimizableVillager.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)
));
}
if (log_enabled)
VillagerOptimizer.getLog().info(player.getName() + " optimized a villager using workstation: '" + placed.getType().toString().toLowerCase() + "'");
} else {
if (notify_player) {
final TextReplacementConfig timeLeft = TextReplacementConfig.builder()
.matchLiteral("%time%")
.replacement(CommonUtil.formatTime(closestOptimizableVillager.getOptimizeCooldownMillis(cooldown)))
.build();
VillagerOptimizer.getLang(player.locale()).nametag_on_optimize_cooldown.forEach(line -> player.sendMessage(line
.replaceText(timeLeft)
));
}
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onBlockBreak(BlockBreakEvent event) {
Block broken = event.getBlock();
Villager.Profession workstationProfession = getWorkstationProfession(broken.getType());
if (workstationProfession.equals(Villager.Profession.NONE)) return;
Player player = event.getPlayer();
if (!player.hasPermission(Permissions.Optimize.WORKSTATION.get())) return;
if (only_while_sneaking && !player.isSneaking()) return;
final Location workstationLoc = broken.getLocation();
WrappedVillager closestOptimizedVillager = null;
double closestDistance = Double.MAX_VALUE;
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 double distance = entity.getLocation().distance(workstationLoc);
if (distance < closestDistance && wVillager.canOptimize(cooldown)) {
closestOptimizedVillager = wVillager;
closestDistance = distance;
}
}
if (closestOptimizedVillager == null) return;
VillagerUnoptimizeEvent unOptimizeEvent = new VillagerUnoptimizeEvent(closestOptimizedVillager, player, OptimizationType.WORKSTATION, event.isAsynchronous());
if (!unOptimizeEvent.callEvent()) return;
closestOptimizedVillager.setOptimization(OptimizationType.NONE);
if (notify_player) {
final TextReplacementConfig vilProfession = TextReplacementConfig.builder()
.matchLiteral("%vil_profession%")
.replacement(closestOptimizedVillager.villager().getProfession().toString().toLowerCase())
.build();
final TextReplacementConfig brokenWorkstation = TextReplacementConfig.builder()
.matchLiteral("%workstation%")
.replacement(broken.getType().toString().toLowerCase())
.build();
VillagerOptimizer.getLang(player.locale()).workstation_unoptimize_success.forEach(line -> player.sendMessage(line
.replaceText(vilProfession)
.replaceText(brokenWorkstation)
));
}
if (log_enabled)
VillagerOptimizer.getLog().info(player.getName() + " unoptimized a villager by breaking workstation: '" + broken.getType().toString().toLowerCase() + "'");
}
private Villager.Profession getWorkstationProfession(final 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

@ -0,0 +1,24 @@
package me.xginko.villageroptimizer.utils;
import org.jetbrains.annotations.NotNull;
import java.time.Duration;
import static java.lang.String.format;
public class CommonUtil {
public static @NotNull String formatTime(final long millis) {
Duration duration = Duration.ofMillis(millis);
final int seconds = duration.toSecondsPart();
final int minutes = duration.toMinutesPart();
final int hours = duration.toHoursPart();
if (hours > 0) {
return format("%02dh %02dm %02ds", hours, minutes, seconds);
} else if (minutes > 0) {
return format("%02dm %02ds", minutes, seconds);
} else {
return format("%02ds", seconds);
}
}
}

View File

@ -0,0 +1,26 @@
package me.xginko.villageroptimizer.utils;
import me.xginko.villageroptimizer.VillagerOptimizer;
import java.util.logging.Level;
public class LogUtil {
public static void moduleLog(Level logLevel, String path, String logMessage) {
VillagerOptimizer.getLog().log(logLevel, "(" + path + ") " + logMessage);
}
public static void materialNotRecognized(String path, String material) {
moduleLog(Level.WARNING, path, "Material '" + material + "' not recognized. Please use correct Material enums from: " +
"https://jd.papermc.io/paper/1.20/org/bukkit/Material.html");
}
public static void damageCauseNotRecognized(String path, String cause) {
moduleLog(Level.WARNING, path, "DamageCause '" + cause + "' not recognized. Please use correct DamageCause enums from: " +
"https://jd.papermc.io/paper/1.20/org/bukkit/event/entity/EntityDamageEvent.DamageCause.html");
}
public static void entityTypeNotRecognized(String path, String entityType) {
moduleLog(Level.WARNING, path, "EntityType '" + entityType + "' not recognized. Please use correct Spigot EntityType enums for your Minecraft version!");
}
}

View File

@ -29,7 +29,7 @@ messages:
- "<green>Оптимизация была успешно снята с %vil_profession% удалением %blocktype%."
command:
optimize-success:
- "<green>Успешно оптимизировано %amount% жителей в радиусе %radius% блоков."
- "<green>Успешно оптимизинованы %amount% жителей в радиусе %radius% блоков."
optimize-fail:
- "<gray>%amount% жителей не могут быть оптимизированы снова так как они уже были оптимизированы недавно."
radius-limit-exceed:
@ -41,4 +41,4 @@ messages:
radius-invalid:
- "<red>Указанный вами радиус не является действительным значением."
no-villagers-nearby:
- "<gray>Не найдено работающих жителей в радиусе %radius% блоков."
- "<gray>Не найдено работающих жителей в радиусе %radius% блоков."

View File

@ -0,0 +1,98 @@
name: VillagerOptimizer
version: '${project.version}'
main: me.xginko.villageroptimizer.VillagerOptimizer
authors: [ xGinko ]
description: ${project.description}
website: ${project.url}
api-version: '1.16'
folia-supported: false
commands:
villageroptimizer:
usage: /villageroptimizer [ reload, version, disable ]
description: VillagerOptimizer admin commands
aliases:
- voptimizer
- vo
optimizevillagers:
usage: /optimizevillagers <blockradius>
description: Optmize villagers in a radius around you
aliases:
- optvils
- noai
unoptimizevillagers:
usage: /unoptimizevillagers <blockradius>
description: Unoptmize villagers in a radius around you
aliases:
- unoptvils
- noaiundo
permissions:
villageroptimizer.ignore:
description: Players with this permission won't be able to use the plugin features
children:
villageroptimizer.optimize.nametag: false
villageroptimizer.optimize.block: false
villageroptimizer.optimize.workstation: false
villageroptimizer.playerdefaults:
description: Default permissions for players
default: true
children:
villageroptimizer.cmd.optimize: true
villageroptimizer.cmd.unoptimize: true
villageroptimizer.optimize.*: true
villageroptimizer.*:
description: All plugin permissions
children:
villageroptimizer.cmd.*: true
villageroptimizer.bypass.*: true
villageroptimizer.optimize.*: true
villageroptimizer.optimize.*:
description: Optimization type permissions
children:
villageroptimizer.optimize.nametag: true
villageroptimizer.optimize.block: true
villageroptimizer.optimize.workstation: true
villageroptimizer.optimize.nametag:
description: Optimize/Unoptimize villagers using nametags
villageroptimizer.optimize.block:
description: Optimize/Unoptimize villagers using specific blocks
villageroptimizer.optimize.workstation:
description: Optimize/Unoptimize villagers using workstations
villageroptimizer.cmd.*:
description: All command permissions
children:
villageroptimizer.cmd.reload: true
villageroptimizer.cmd.disable: true
villageroptimizer.cmd.version: true
villageroptimizer.cmd.optimize: true
villageroptimizer.cmd.unoptimize: true
villageroptimizer.cmd.disable:
description: Disable the plugin
villageroptimizer.cmd.reload:
description: Reload the plugin configuration
villageroptimizer.cmd.version:
description: Show the plugin version
villageroptimizer.cmd.optimize:
description: Optimize villagers in a radius
villageroptimizer.cmd.unoptimize:
description: Unoptimize villagers in a radius
villageroptimizer.bypass.*:
description: All bypass permissions
children:
villageroptimizer.bypass.tradeprevention: true
villageroptimizer.bypass.restockcooldown: true
villageroptimizer.bypass.nametagcooldown: true
villageroptimizer.bypass.blockcooldown: true
villageroptimizer.bypass.workstationcooldown: true
villageroptimizer.bypass.commandcooldown: true
villageroptimizer.bypass.tradeprevention:
description: Bypass unoptimized trading prevention if enabled
villageroptimizer.bypass.restockcooldown:
description: Bypass permission for optimized trade restock cooldown
villageroptimizer.bypass.nametagcooldown:
description: Bypass permission for nametag optimization cooldown
villageroptimizer.bypass.blockcooldown:
description: Bypass permission for block optimization cooldown
villageroptimizer.bypass.workstationcooldown:
description: Bypass permission for workstation optimization cooldown
villageroptimizer.bypass.commandcooldown:
description: Bypass permission for command optimization cooldown

View File

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
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>
<parent>
<groupId>me.xginko.VillagerOptimizer</groupId>
<artifactId>VillagerOptimizer</artifactId>
<version>1.0.1</version>
</parent>
<artifactId>1.20.2</artifactId>
<name>${project.parent.artifactId}-${project.parent.version}--${project.artifactId}</name>
<packaging>jar</packaging>
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<relocations>
<relocation>
<pattern>com.github.benmanes.caffeine</pattern>
<shadedPattern>me.xginko.villageroptimizer.caffeine</shadedPattern>
</relocation>
<relocation>
<pattern>org.bstats</pattern>
<shadedPattern>me.xginko.villageroptimizer.bstats</shadedPattern>
</relocation>
</relocations>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
</build>
<repositories>
<repository>
<id>papermc-repo</id>
<url>https://repo.papermc.io/repository/maven-public/</url>
</repository>
<repository>
<id>sonatype</id>
<url>https://oss.sonatype.org/content/groups/public/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>io.papermc.paper</groupId>
<artifactId>paper-api</artifactId>
<version>1.20.2-R0.1-SNAPSHOT</version>
<scope>provided</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;
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 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

@ -0,0 +1,173 @@
package me.xginko.villageroptimizer;
import me.xginko.villageroptimizer.commands.VillagerOptimizerCommand;
import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.config.LanguageCache;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
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 org.bstats.bukkit.Metrics;
import org.bukkit.NamespacedKey;
import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import java.util.jar.JarFile;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public final class VillagerOptimizer extends JavaPlugin {
private static VillagerOptimizer instance;
private static VillagerCache villagerCache;
private static HashMap<String, LanguageCache> languageCacheMap;
private static Config config;
private static Logger logger;
public final static Style plugin_style = Style.style(TextColor.color(102,255,230), TextDecoration.BOLD);
@Override
public void onEnable() {
instance = this;
logger = getLogger();
ConsoleCommandSender console = getServer().getConsoleSender();
console.sendMessage(Component.text("╭────────────────────────────────────────────────────────────╮").style(plugin_style));
console.sendMessage(Component.text("│ │").style(plugin_style));
console.sendMessage(Component.text("│ │").style(plugin_style));
console.sendMessage(Component.text("│ _ __ _ __ __ │").style(plugin_style));
console.sendMessage(Component.text("│ | | / /(_)/ // /___ _ ___ _ ___ ____ │").style(plugin_style));
console.sendMessage(Component.text("│ | |/ // // // // _ `// _ `// -_)/ __/ │").style(plugin_style));
console.sendMessage(Component.text("│ |___//_//_//_/ \\_,_/ \\_, / \\__//_/ │").style(plugin_style));
console.sendMessage(Component.text("│ ____ __ _ /___/_ │").style(plugin_style));
console.sendMessage(Component.text("│ / __ \\ ___ / /_ (_)__ _ (_)___ ___ ____ │").style(plugin_style));
console.sendMessage(Component.text("│ / /_/ // _ \\/ __// // ' \\ / //_ // -_)/ __/ │").style(plugin_style));
console.sendMessage(Component.text("\\____// .__/\\__//_//_/_/_//_/ /__/\\__//_/ │").style(plugin_style));
console.sendMessage(Component.text("│ /_/ by xGinko │").style(plugin_style));
console.sendMessage(Component.text("│ │").style(plugin_style));
console.sendMessage(Component.text("│ │").style(plugin_style));
console.sendMessage(Component.text("").style(plugin_style).append(Component.text("https://github.com/xGinko/VillagerOptimizer").color(NamedTextColor.GRAY)).append(Component.text("").style(plugin_style)));
console.sendMessage(Component.text("│ │").style(plugin_style));
console.sendMessage(Component.text("│ │").style(plugin_style));
console.sendMessage(Component.text("").style(plugin_style).append(Component.text(" ➤ Loading Translations...").style(plugin_style)).append(Component.text("").style(plugin_style)));
reloadLang(true);
console.sendMessage(Component.text("").style(plugin_style).append(Component.text(" ➤ Loading Config...").style(plugin_style)).append(Component.text("").style(plugin_style)));
reloadConfiguration();
console.sendMessage(Component.text("").style(plugin_style).append(Component.text(" ✓ Done.").color(NamedTextColor.WHITE).decorate(TextDecoration.BOLD)).append(Component.text("").style(plugin_style)));
console.sendMessage(Component.text("│ │").style(plugin_style));
console.sendMessage(Component.text("│ │").style(plugin_style));
console.sendMessage(Component.text("╰────────────────────────────────────────────────────────────╯").style(plugin_style));
new Metrics(this, 19954);
}
public static VillagerOptimizer getInstance() {
return instance;
}
public static NamespacedKey getKey(String key) {
return new NamespacedKey(instance, key);
}
public static Config getConfiguration() {
return config;
}
public static VillagerCache getCache() {
return villagerCache;
}
public static Logger getLog() {
return logger;
}
public static LanguageCache getLang(Locale locale) {
return getLang(locale.toString().toLowerCase());
}
public static LanguageCache getLang(CommandSender commandSender) {
return commandSender instanceof Player player ? getLang(player.locale()) : getLang(config.default_lang);
}
public static LanguageCache getLang(String lang) {
return config.auto_lang ? languageCacheMap.getOrDefault(lang.replace("-", "_"), languageCacheMap.get(config.default_lang.toString().toLowerCase())) : languageCacheMap.get(config.default_lang.toString().toLowerCase());
}
public void reloadPlugin() {
reloadLang(false);
reloadConfiguration();
}
private void reloadConfiguration() {
try {
config = new Config();
villagerCache = new VillagerCache(config.cache_keep_time_seconds);
VillagerOptimizerCommand.reloadCommands();
VillagerOptimizerModule.reloadModules();
config.saveConfig();
} catch (Exception e) {
logger.severe("Error loading config! - " + e.getLocalizedMessage());
e.printStackTrace();
}
}
private void reloadLang(boolean startup) {
languageCacheMap = new HashMap<>();
ConsoleCommandSender console = getServer().getConsoleSender();
try {
File langDirectory = new File(getDataFolder() + "/lang");
Files.createDirectories(langDirectory.toPath());
for (String fileName : getDefaultLanguageFiles()) {
String localeString = fileName.substring(fileName.lastIndexOf('/') + 1, fileName.lastIndexOf('.'));
if (startup) console.sendMessage(
Component.text("").style(plugin_style)
.append(Component.text(" "+localeString).color(NamedTextColor.WHITE).decorate(TextDecoration.BOLD))
.append(Component.text("").style(plugin_style)));
else logger.info("Found language file for " + localeString);
LanguageCache langCache = new LanguageCache(localeString);
languageCacheMap.put(localeString, langCache);
}
Pattern langPattern = Pattern.compile("([a-z]{1,3}_[a-z]{1,3})(\\.yml)", Pattern.CASE_INSENSITIVE);
for (File langFile : langDirectory.listFiles()) {
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) console.sendMessage(
Component.text("").style(plugin_style)
.append(Component.text(" "+localeString).color(NamedTextColor.WHITE).decorate(TextDecoration.BOLD))
.append(Component.text("").style(plugin_style)));
else logger.info("Found language file for " + localeString);
LanguageCache langCache = new LanguageCache(localeString);
languageCacheMap.put(localeString, langCache);
}
}
}
} catch (Exception e) {
if (startup) console.sendMessage(
Component.text("").style(plugin_style)
.append(Component.text("LANG ERROR").color(NamedTextColor.RED).decorate(TextDecoration.BOLD))
.append(Component.text("").style(plugin_style)));
else logger.severe("Error loading language files! Language files will not reload to avoid errors, make sure to correct this before restarting the server!");
e.printStackTrace();
}
}
private Set<String> getDefaultLanguageFiles() {
Set<String> languageFiles = new HashSet<>();
try (JarFile jarFile = new JarFile(this.getFile())) {
jarFile.entries().asIterator().forEachRemaining(jarFileEntry -> {
final String path = jarFileEntry.getName();
if (path.startsWith("lang/") && path.endsWith(".yml"))
languageFiles.add(path);
});
} catch (IOException e) {
logger.severe("Error while getting default language files! - " + e.getLocalizedMessage());
e.printStackTrace();
}
return languageFiles;
}
}

View File

@ -0,0 +1,192 @@
package me.xginko.villageroptimizer;
import me.xginko.villageroptimizer.enums.Keys;
import me.xginko.villageroptimizer.enums.OptimizationType;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.entity.Villager;
import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.persistence.PersistentDataType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public final class WrappedVillager {
private final @NotNull Villager villager;
private final @NotNull PersistentDataContainer dataContainer;
WrappedVillager(@NotNull Villager villager) {
this.villager = villager;
this.dataContainer = villager.getPersistentDataContainer();
}
/**
* @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 this plugin, otherwise false.
*/
public boolean isOptimized() {
return dataContainer.has(Keys.OPTIMIZATION_TYPE.key());
}
/**
* @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) {
return getLastOptimize() + cooldown_millis <= System.currentTimeMillis();
}
/**
* @param type OptimizationType the villager should be set to.
*/
public void setOptimization(OptimizationType type) {
if (type.equals(OptimizationType.NONE) && isOptimized()) {
dataContainer.remove(Keys.OPTIMIZATION_TYPE.key());
villager.getScheduler().run(VillagerOptimizer.getInstance(), enableAI -> {
villager.setAware(true);
villager.setAI(true);
}, null);
} else {
dataContainer.set(Keys.OPTIMIZATION_TYPE.key(), PersistentDataType.STRING, type.name());
villager.getScheduler().run(VillagerOptimizer.getInstance(), disableAI -> {
villager.setAware(false);
}, null);
}
}
/**
* @return The current OptimizationType of the villager.
*/
public @NotNull OptimizationType getOptimizationType() {
return isOptimized() ? OptimizationType.valueOf(dataContainer.get(Keys.OPTIMIZATION_TYPE.key(), PersistentDataType.STRING)) : OptimizationType.NONE;
}
/**
* Saves the system time in millis when the villager was last optimized.
*/
public void saveOptimizeTime() {
dataContainer.set(Keys.LAST_OPTIMIZE.key(), 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() {
return dataContainer.has(Keys.LAST_OPTIMIZE.key(), PersistentDataType.LONG) ? dataContainer.get(Keys.LAST_OPTIMIZE.key(), PersistentDataType.LONG) : 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) {
return dataContainer.has(Keys.LAST_OPTIMIZE.key(), PersistentDataType.LONG) ? (System.currentTimeMillis() - (dataContainer.get(Keys.LAST_OPTIMIZE.key(), PersistentDataType.LONG) + cooldown_millis)) : 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() {
villager.getRecipes().forEach(recipe -> recipe.setUses(0));
}
/**
* Saves the time of the in-game world when the entity was last restocked.
*/
public void saveRestockTime() {
dataContainer.set(Keys.LAST_RESTOCK.key(), PersistentDataType.LONG, villager.getWorld().getFullTime());
}
/**
* @return The time of the in-game world when the entity was last restocked.
*/
public long getLastRestock() {
return dataContainer.has(Keys.LAST_RESTOCK.key(), PersistentDataType.LONG) ? dataContainer.get(Keys.LAST_RESTOCK.key(), PersistentDataType.LONG) : 0L;
}
public long getRestockCooldownMillis(final long cooldown_millis) {
return dataContainer.has(Keys.LAST_RESTOCK.key(), PersistentDataType.LONG) ? (villager.getWorld().getFullTime() - (dataContainer.get(Keys.LAST_RESTOCK.key(), PersistentDataType.LONG) + cooldown_millis)) : 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) {
return getLastLevelUpTime() + cooldown_millis <= villager.getWorld().getFullTime();
}
/**
* Saves the time of the in-game world when the entity was last leveled up.
*/
public void saveLastLevelUp() {
dataContainer.set(Keys.LAST_LEVELUP.key(), 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() {
return dataContainer.has(Keys.LAST_LEVELUP.key(), PersistentDataType.LONG) ? dataContainer.get(Keys.LAST_LEVELUP.key(), PersistentDataType.LONG) : 0L;
}
public long getLevelCooldownMillis(final long cooldown_millis) {
return dataContainer.has(Keys.LAST_LEVELUP.key(), PersistentDataType.LONG) ? (villager.getWorld().getFullTime() - (dataContainer.get(Keys.LAST_LEVELUP.key(), PersistentDataType.LONG) + cooldown_millis)) : cooldown_millis;
}
public void memorizeName(final Component customName) {
dataContainer.set(Keys.LAST_OPTIMIZE_NAME.key(), PersistentDataType.STRING, MiniMessage.miniMessage().serialize(customName));
}
public @Nullable Component getMemorizedName() {
return dataContainer.has(Keys.LAST_OPTIMIZE_NAME.key()) ? MiniMessage.miniMessage().deserialize(dataContainer.get(Keys.LAST_OPTIMIZE_NAME.key(), PersistentDataType.STRING)) : null;
}
public void forgetName() {
dataContainer.remove(Keys.LAST_OPTIMIZE_NAME.key());
}
}

View File

@ -0,0 +1,11 @@
package me.xginko.villageroptimizer.commands;
import net.kyori.adventure.text.TextComponent;
import org.bukkit.command.CommandSender;
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

@ -0,0 +1,35 @@
package me.xginko.villageroptimizer.commands;
import me.xginko.villageroptimizer.VillagerOptimizer;
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.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandMap;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import java.util.HashSet;
public interface VillagerOptimizerCommand extends CommandExecutor {
String label();
HashSet<VillagerOptimizerCommand> commands = new HashSet<>();
static void reloadCommands() {
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
CommandMap commandMap = plugin.getServer().getCommandMap();
commands.forEach(command -> plugin.getCommand(command.label()).unregister(commandMap));
commands.clear();
commands.add(new VillagerOptimizerCmd());
commands.add(new OptVillagersRadius());
commands.add(new UnOptVillagersRadius());
commands.forEach(command -> plugin.getCommand(command.label()).setExecutor(command));
}
@Override
boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args);
}

View File

@ -0,0 +1,142 @@
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.enums.OptimizationType;
import me.xginko.villageroptimizer.enums.Permissions;
import me.xginko.villageroptimizer.events.VillagerOptimizeEvent;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextReplacementConfig;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextDecoration;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.entity.Villager;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
public class OptVillagersRadius implements VillagerOptimizerCommand, TabCompleter {
private final List<String> tabCompletes = List.of("5", "10", "25", "50");
private final long cooldown;
private final int max_radius;
public OptVillagersRadius() {
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.\s
Here for configuration freedom. Recommended to leave as is to not enable any exploitable behavior.""") * 1000L;
}
@Override
public String label() {
return "optimizevillagers";
}
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
return args.length == 1 ? tabCompletes : null;
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
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;
}
if (!sender.hasPermission(Permissions.Commands.OPTIMIZE_RADIUS.get())) {
sender.sendMessage(VillagerOptimizer.getLang(sender).no_permission);
return true;
}
if (args.length != 1) {
VillagerOptimizer.getLang(player.locale()).command_specify_radius.forEach(player::sendMessage);
return true;
}
try {
int specifiedRadius = Integer.parseInt(args[0]);
if (specifiedRadius > max_radius) {
final TextReplacementConfig limit = TextReplacementConfig.builder()
.matchLiteral("%distance%")
.replacement(Integer.toString(max_radius))
.build();
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());
for (Entity entity : player.getNearbyEntities(specifiedRadius, specifiedRadius, specifiedRadius)) {
if (!entity.getType().equals(EntityType.VILLAGER)) continue;
Villager villager = (Villager) entity;
Villager.Profession profession = villager.getProfession();
if (profession.equals(Villager.Profession.NITWIT) || profession.equals(Villager.Profession.NONE)) continue;
WrappedVillager wVillager = villagerCache.getOrAdd(villager);
if (player_has_cooldown_bypass || wVillager.canOptimize(cooldown)) {
VillagerOptimizeEvent optimizeEvent = new VillagerOptimizeEvent(wVillager, OptimizationType.COMMAND, player);
if (optimizeEvent.callEvent()) {
wVillager.setOptimization(optimizeEvent.getOptimizationType());
wVillager.saveOptimizeTime();
successCount++;
}
} else {
failCount++;
}
}
if (successCount <= 0 && failCount <= 0) {
final TextReplacementConfig radius = TextReplacementConfig.builder()
.matchLiteral("%radius%")
.replacement(Integer.toString(specifiedRadius))
.build();
VillagerOptimizer.getLang(player.locale()).command_no_villagers_nearby.forEach(line -> player.sendMessage(line.replaceText(radius)));
return true;
}
if (successCount > 0) {
final TextReplacementConfig success_amount = TextReplacementConfig.builder()
.matchLiteral("%amount%")
.replacement(Integer.toString(successCount))
.build();
final TextReplacementConfig radius = TextReplacementConfig.builder()
.matchLiteral("%radius%")
.replacement(Integer.toString(specifiedRadius))
.build();
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 -> player.sendMessage(line.replaceText(alreadyOptimized)));
}
} catch (NumberFormatException e) {
VillagerOptimizer.getLang(player.locale()).command_radius_invalid.forEach(player::sendMessage);
}
return true;
}
}

View File

@ -0,0 +1,121 @@
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.enums.OptimizationType;
import me.xginko.villageroptimizer.enums.Permissions;
import me.xginko.villageroptimizer.events.VillagerUnoptimizeEvent;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextReplacementConfig;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextDecoration;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.entity.Villager;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
public class UnOptVillagersRadius implements VillagerOptimizerCommand, TabCompleter {
private final List<String> tabCompletes = List.of("5", "10", "25", "50");
private final int max_radius;
public UnOptVillagersRadius() {
this.max_radius = VillagerOptimizer.getConfiguration().getInt("optimization-methods.commands.unoptimizevillagers.max-block-radius", 100);
}
@Override
public String label() {
return "unoptimizevillagers";
}
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
return args.length == 1 ? tabCompletes : null;
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
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;
}
if (!sender.hasPermission(Permissions.Commands.UNOPTIMIZE_RADIUS.get())) {
sender.sendMessage(VillagerOptimizer.getLang(sender).no_permission);
return true;
}
if (args.length != 1) {
VillagerOptimizer.getLang(player.locale()).command_specify_radius.forEach(player::sendMessage);
return true;
}
try {
int specifiedRadius = Integer.parseInt(args[0]);
if (specifiedRadius > max_radius) {
final TextReplacementConfig limit = TextReplacementConfig.builder()
.matchLiteral("%distance%")
.replacement(Integer.toString(max_radius))
.build();
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(specifiedRadius, specifiedRadius, specifiedRadius)) {
if (!entity.getType().equals(EntityType.VILLAGER)) continue;
Villager villager = (Villager) entity;
Villager.Profession profession = villager.getProfession();
if (profession.equals(Villager.Profession.NITWIT) || profession.equals(Villager.Profession.NONE)) continue;
WrappedVillager wVillager = villagerCache.getOrAdd(villager);
if (wVillager.isOptimized()) {
VillagerUnoptimizeEvent unOptimizeEvent = new VillagerUnoptimizeEvent(wVillager, player, OptimizationType.COMMAND);
if (unOptimizeEvent.callEvent()) {
wVillager.setOptimization(OptimizationType.NONE);
successCount++;
}
}
}
if (successCount <= 0) {
final TextReplacementConfig radius = TextReplacementConfig.builder()
.matchLiteral("%radius%")
.replacement(Integer.toString(specifiedRadius))
.build();
VillagerOptimizer.getLang(player.locale()).command_no_villagers_nearby.forEach(line -> player.sendMessage(line.replaceText(radius)));
} else {
final TextReplacementConfig success_amount = TextReplacementConfig.builder()
.matchLiteral("%amount%")
.replacement(Integer.toString(successCount))
.build();
final TextReplacementConfig radius = TextReplacementConfig.builder()
.matchLiteral("%radius%")
.replacement(Integer.toString(specifiedRadius))
.build();
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(player::sendMessage);
}
return true;
}
}

View File

@ -0,0 +1,79 @@
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.enums.Permissions;
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.bukkit.command.TabCompleter;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
public class VillagerOptimizerCmd implements TabCompleter, VillagerOptimizerCommand {
private final List<SubCommand> subCommands = new ArrayList<>(3);
private final List<String> tabCompleter = new ArrayList<>(3);
public VillagerOptimizerCmd() {
subCommands.add(new ReloadSubCmd());
subCommands.add(new VersionSubCmd());
subCommands.add(new DisableSubCmd());
subCommands.forEach(subCommand -> tabCompleter.add(subCommand.getLabel()));
}
@Override
public String label() {
return "villageroptimizer";
}
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, String[] args) {
return args.length == 1 ? tabCompleter : null;
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
if (args.length > 0) {
boolean cmdExists = false;
for (SubCommand subCommand : subCommands) {
if (args[0].equalsIgnoreCase(subCommand.getLabel())) {
subCommand.perform(sender, args);
cmdExists = true;
break;
}
}
if (!cmdExists) sendCommandOverview(sender);
} else {
sendCommandOverview(sender);
}
return true;
}
private void sendCommandOverview(CommandSender sender) {
if (!sender.hasPermission(Permissions.Commands.RELOAD.get()) && !sender.hasPermission(Permissions.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))
);
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))
);
sender.sendMessage(Component.text("-----------------------------------------------------").color(NamedTextColor.GRAY));
}
}

View File

@ -0,0 +1,42 @@
package me.xginko.villageroptimizer.commands.villageroptimizer.subcommands;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.commands.SubCommand;
import me.xginko.villageroptimizer.enums.Permissions;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.CommandSender;
public class DisableSubCmd extends SubCommand {
@Override
public String getLabel() {
return "disable";
}
@Override
public TextComponent getDescription() {
return Component.text("Disable all plugin tasks and listeners.").color(NamedTextColor.GRAY);
}
@Override
public TextComponent getSyntax() {
return Component.text("/villageroptimizer disable").color(VillagerOptimizer.plugin_style.color());
}
@Override
public void perform(CommandSender sender, String[] args) {
if (sender.hasPermission(Permissions.Commands.DISABLE.get())) {
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));
} else {
sender.sendMessage(VillagerOptimizer.getLang(sender).no_permission);
}
}
}

View File

@ -0,0 +1,41 @@
package me.xginko.villageroptimizer.commands.villageroptimizer.subcommands;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.commands.SubCommand;
import me.xginko.villageroptimizer.enums.Permissions;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.CommandSender;
public class ReloadSubCmd extends SubCommand {
@Override
public String getLabel() {
return "reload";
}
@Override
public TextComponent getDescription() {
return Component.text("Reload the plugin configuration.").color(NamedTextColor.GRAY);
}
@Override
public TextComponent getSyntax() {
return Component.text("/villageroptimizer reload").color(VillagerOptimizer.plugin_style.color());
}
@Override
public void perform(CommandSender sender, String[] args) {
if (sender.hasPermission(Permissions.Commands.RELOAD.get())) {
sender.sendMessage(Component.text("Reloading VillagerOptimizer...").color(NamedTextColor.WHITE));
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
plugin.getServer().getAsyncScheduler().runNow(plugin, reloadPlugin -> {
plugin.reloadPlugin();
sender.sendMessage(Component.text("Reload complete.").color(NamedTextColor.GREEN));
});
} else {
sender.sendMessage(VillagerOptimizer.getLang(sender).no_permission);
}
}
}

View File

@ -0,0 +1,53 @@
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.enums.Permissions;
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.CommandSender;
public class VersionSubCmd extends SubCommand {
@Override
public String getLabel() {
return "version";
}
@Override
public TextComponent getDescription() {
return Component.text("Show the plugin version.").color(NamedTextColor.GRAY);
}
@Override
public TextComponent getSyntax() {
return Component.text("/villageroptimizer version").color(VillagerOptimizer.plugin_style.color());
}
@Override
public void perform(CommandSender sender, String[] args) {
if (sender.hasPermission(Permissions.Commands.VERSION.get())) {
final PluginMeta pluginMeta = VillagerOptimizer.getInstance().getPluginMeta();
sender.sendMessage(
Component.newline()
.append(
Component.text(pluginMeta.getName()+" "+pluginMeta.getVersion())
.style(VillagerOptimizer.plugin_style)
.clickEvent(ClickEvent.openUrl(pluginMeta.getWebsite()))
)
.append(Component.text(" by ").color(NamedTextColor.GRAY))
.append(
Component.text(pluginMeta.getAuthors().get(0))
.color(NamedTextColor.WHITE)
.clickEvent(ClickEvent.openUrl("https://github.com/xGinko"))
)
.append(Component.newline())
);
} else {
sender.sendMessage(VillagerOptimizer.getLang(sender).no_permission);
}
}
}

View File

@ -0,0 +1,146 @@
package me.xginko.villageroptimizer.config;
import io.github.thatsmusic99.configurationmaster.api.ConfigFile;
import io.github.thatsmusic99.configurationmaster.api.ConfigSection;
import me.xginko.villageroptimizer.VillagerOptimizer;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.util.List;
import java.util.Locale;
import java.util.Map;
public class Config {
private final @NotNull ConfigFile config;
public final @NotNull Locale default_lang;
public final boolean auto_lang;
public final long cache_keep_time_seconds;
public Config() throws Exception {
this.config = loadConfig(new File(VillagerOptimizer.getInstance().getDataFolder(), "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 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_seconds = getInt("general.cache-keep-time-seconds", 30,
"The amount of time in seconds a villager will be kept in the plugin's cache.");
}
private ConfigFile loadConfig(File ymlFile) throws Exception {
File parent = ymlFile.getParentFile();
if (!parent.exists() && !parent.mkdir())
VillagerOptimizer.getLog().severe("Unable to create plugin config directory.");
return ConfigFile.loadConfig(ymlFile);
}
public void saveConfig() {
try {
config.save();
} catch (Exception e) {
VillagerOptimizer.getLog().severe("Failed to save config file! - " + e.getLocalizedMessage());
}
}
private void structureConfig() {
config.addDefault("config-version", 1.00);
createTitledSection("General", "general");
createTitledSection("Optimization", "optimization-methods");
config.addDefault("optimization-methods.commands.unoptimizevillagers", null);
config.addComment("optimization-methods.commands", """
If you want to disable commands, negate the following permissions:\s
villageroptimizer.cmd.optimize\s
villageroptimizer.cmd.unoptimize
""");
config.addDefault("optimization-methods.nametag-optimization.enable", true);
createTitledSection("Villager Chunk Limit", "villager-chunk-limit");
createTitledSection("Gameplay", "gameplay");
config.addDefault("gameplay.restock-optimized-trades", null);
config.addDefault("gameplay.level-optimized-profession", null);
config.addDefault("gameplay.rename-optimized-villagers.enable", true);
config.addDefault("gameplay.villagers-spawn-as-adults.enable", false);
config.addDefault("gameplay.prevent-trading-with-unoptimized.enable", false);
config.addDefault("gameplay.prevent-entities-from-targeting-optimized.enable", true);
config.addDefault("gameplay.prevent-damage-to-optimized.enable", true);
}
public void createTitledSection(@NotNull String title, @NotNull String path) {
config.addSection(title);
config.addDefault(path, null);
}
public @NotNull ConfigFile master() {
return config;
}
public boolean getBoolean(@NotNull String path, boolean def, @NotNull String comment) {
config.addDefault(path, def, comment);
return config.getBoolean(path, def);
}
public boolean getBoolean(@NotNull String path, boolean def) {
config.addDefault(path, def);
return config.getBoolean(path, def);
}
public @NotNull String getString(@NotNull String path, @NotNull String def, @NotNull String comment) {
config.addDefault(path, def, comment);
return config.getString(path, def);
}
public @NotNull String getString(@NotNull String path, @NotNull String def) {
config.addDefault(path, def);
return config.getString(path, def);
}
public double getDouble(@NotNull String path, @NotNull Double def, @NotNull String comment) {
config.addDefault(path, def, comment);
return config.getDouble(path, def);
}
public double getDouble(@NotNull String path, @NotNull Double def) {
config.addDefault(path, def);
return config.getDouble(path, def);
}
public int getInt(@NotNull String path, int def, @NotNull String comment) {
config.addDefault(path, def, comment);
return config.getInteger(path, def);
}
public int getInt(@NotNull String path, int def) {
config.addDefault(path, def);
return config.getInteger(path, def);
}
public @NotNull List<String> getList(@NotNull String path, @NotNull List<String> def, @NotNull String comment) {
config.addDefault(path, def, comment);
return config.getStringList(path);
}
public @NotNull List<String> getList(@NotNull String path, @NotNull List<String> def) {
config.addDefault(path, def);
return config.getStringList(path);
}
public @NotNull ConfigSection getConfigSection(@NotNull String path, @NotNull Map<String, Object> defaultKeyValue) {
config.addDefault(path, null);
config.makeSectionLenient(path);
defaultKeyValue.forEach((string, object) -> config.addExample(path+"."+string, object));
return config.getConfigSection(path);
}
public @NotNull ConfigSection getConfigSection(@NotNull String path, @NotNull Map<String, Object> defaultKeyValue, @NotNull String comment) {
config.addDefault(path, null, comment);
config.makeSectionLenient(path);
defaultKeyValue.forEach((string, object) -> config.addExample(path+"."+string, object));
return config.getConfigSection(path);
}
public void addComment(@NotNull String path, @NotNull String comment) {
config.addComment(path, comment);
}
}

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().severe("Unable 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
// 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);
lang.save();
} catch (Exception e) {
VillagerOptimizer.getLog().severe("Failed to save language file: "+ lang.getFile().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)));
lang.addDefault(path, defaultTranslation);
return MiniMessage.miniMessage().deserialize(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) {
lang.addDefault(path, defaultTranslation, comment);
return MiniMessage.miniMessage().deserialize(lang.getString(path, defaultTranslation));
}
public @NotNull List<Component> getListTranslation(@NotNull String path, @NotNull List<String> defaultTranslation) {
lang.addDefault(path, defaultTranslation);
return lang.getStringList(path).stream().map(MiniMessage.miniMessage()::deserialize).toList();
}
public @NotNull List<Component> getListTranslation(@NotNull String path, @NotNull List<String> defaultTranslation, @NotNull String comment) {
lang.addDefault(path, defaultTranslation, comment);
return lang.getStringList(path).stream().map(MiniMessage.miniMessage()::deserialize).toList();
}
}

View File

@ -0,0 +1,24 @@
package me.xginko.villageroptimizer.enums;
import me.xginko.villageroptimizer.VillagerOptimizer;
import org.bukkit.NamespacedKey;
public enum Keys {
OPTIMIZATION_TYPE(VillagerOptimizer.getKey("optimization-type")),
LAST_OPTIMIZE(VillagerOptimizer.getKey("last-optimize")),
LAST_LEVELUP(VillagerOptimizer.getKey("last-levelup")),
LAST_RESTOCK(VillagerOptimizer.getKey("last-restock")),
LAST_OPTIMIZE_NAME(VillagerOptimizer.getKey("last-optimize-name"));
private final NamespacedKey key;
Keys(NamespacedKey key) {
this.key = key;
}
public NamespacedKey key() {
return key;
}
}

View File

@ -0,0 +1,11 @@
package me.xginko.villageroptimizer.enums;
public enum OptimizationType {
COMMAND,
NAMETAG,
WORKSTATION,
BLOCK,
NONE
}

View File

@ -0,0 +1,45 @@
package me.xginko.villageroptimizer.enums;
public class Permissions {
public enum Commands {
VERSION("villageroptimizer.cmd.version"),
RELOAD("villageroptimizer.cmd.reload"),
DISABLE("villageroptimizer.cmd.disable"),
OPTIMIZE_RADIUS("villageroptimizer.cmd.optimize"),
UNOPTIMIZE_RADIUS("villageroptimizer.cmd.unoptimize");
private final String permission;
Commands(String permission) {
this.permission = permission;
}
public String get() {
return permission;
}
}
public enum Optimize {
NAMETAG("villageroptimizer.optimize.nametag"),
BLOCK("villageroptimizer.optimize.block"),
WORKSTATION("villageroptimizer.optimize.workstation");
private final String permission;
Optimize(String permission) {
this.permission = permission;
}
public String get() {
return permission;
}
}
public enum Bypass {
TRADE_PREVENTION("villageroptimizer.bypass.tradeprevention"),
RESTOCK_COOLDOWN("villageroptimizer.bypass.restockcooldown"),
NAMETAG_COOLDOWN("villageroptimizer.bypass.nametagcooldown"),
BLOCK_COOLDOWN("villageroptimizer.bypass.blockcooldown"),
WORKSTATION_COOLDOWN("villageroptimizer.bypass.workstationcooldown"),
COMMAND_COOLDOWN("villageroptimizer.bypass.commandcooldown");
private final String permission;
Bypass(String permission) {
this.permission = permission;
}
public String get() {
return permission;
}
}
}

View File

@ -0,0 +1,79 @@
package me.xginko.villageroptimizer.events;
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;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class VillagerOptimizeEvent extends Event implements Cancellable {
private static final @NotNull HandlerList handlers = new HandlerList();
private final @NotNull WrappedVillager wrappedVillager;
private @NotNull OptimizationType type;
private final @Nullable Player whoOptimised;
private boolean isCancelled = false;
public VillagerOptimizeEvent(@NotNull WrappedVillager wrappedVillager, @NotNull OptimizationType type, @Nullable Player whoOptimised, boolean isAsync) throws IllegalArgumentException {
super(isAsync);
this.wrappedVillager = wrappedVillager;
this.whoOptimised = whoOptimised;
if (type.equals(OptimizationType.NONE)) {
throw new IllegalArgumentException("OptimizationType can't be NONE.");
} else {
this.type = type;
}
}
public VillagerOptimizeEvent(@NotNull WrappedVillager wrappedVillager, @NotNull OptimizationType type, @Nullable Player whoOptimised) throws IllegalArgumentException {
this.wrappedVillager = wrappedVillager;
this.whoOptimised = whoOptimised;
if (type.equals(OptimizationType.NONE)) {
throw new IllegalArgumentException("OptimizationType can't be NONE.");
} else {
this.type = type;
}
}
public @NotNull WrappedVillager getWrappedVillager() {
return wrappedVillager;
}
public @NotNull OptimizationType getOptimizationType() {
return type;
}
public void setOptimizationType(@NotNull OptimizationType type) throws IllegalArgumentException {
if (type.equals(OptimizationType.NONE)) {
throw new IllegalArgumentException("OptimizationType can't be NONE.");
} else {
this.type = type;
}
}
public @Nullable Player getWhoOptimised() {
return whoOptimised;
}
@Override
public void setCancelled(boolean cancel) {
isCancelled = cancel;
}
@Override
public boolean isCancelled() {
return isCancelled;
}
@Override
public @NotNull HandlerList getHandlers() {
return handlers;
}
public static HandlerList getHandlerList() {
return handlers;
}
}

View File

@ -0,0 +1,63 @@
package me.xginko.villageroptimizer.events;
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;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class VillagerUnoptimizeEvent extends Event implements Cancellable {
private static final @NotNull HandlerList handlers = new HandlerList();
private final @NotNull WrappedVillager wrappedVillager;
private final @NotNull OptimizationType unoptimizeType;
private final @Nullable Player whoUnoptimized;
private boolean isCancelled = false;
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) {
this.wrappedVillager = wrappedVillager;
this.whoUnoptimized = whoUnoptimized;
this.unoptimizeType = unoptimizeType;
}
public @NotNull WrappedVillager getWrappedVillager() {
return wrappedVillager;
}
public @Nullable Player getWhoUnoptimized() {
return whoUnoptimized;
}
public @NotNull OptimizationType getWhichTypeUnoptimized() {
return unoptimizeType;
}
@Override
public boolean isCancelled() {
return isCancelled;
}
@Override
public void setCancelled(boolean cancel) {
this.isCancelled = cancel;
}
@Override
public @NotNull HandlerList getHandlers() {
return handlers;
}
public static HandlerList getHandlerList() {
return handlers;
}
}

View File

@ -0,0 +1,182 @@
package me.xginko.villageroptimizer.modules;
import io.papermc.paper.threadedregions.scheduler.ScheduledTask;
import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.utils.LogUtil;
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;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.CreatureSpawnEvent;
import org.bukkit.event.player.PlayerInteractEntityEvent;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.logging.Level;
public class VillagerChunkLimit implements VillagerOptimizerModule, Listener {
private final VillagerCache villagerCache;
private ScheduledTask periodic_chunk_check;
private final List<Villager.Profession> non_optimized_removal_priority = new ArrayList<>(16);
private final List<Villager.Profession> optimized_removal_priority = new ArrayList<>(16);
private final long check_period;
private final int non_optimized_max_per_chunk, optimized_max_per_chunk;
private final boolean log_enabled, skip_unloaded_entity_chunks;
protected VillagerChunkLimit() {
shouldEnable();
this.villagerCache = VillagerOptimizer.getCache();
Config config = VillagerOptimizer.getConfiguration();
config.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.log_enabled = config.getBoolean("villager-chunk-limit.log-removals", false);
this.non_optimized_max_per_chunk = config.getInt("villager-chunk-limit.unoptimized.max-per-chunk", 20,
"The maximum amount of unoptimized villagers per chunk.");
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"""
).forEach(configuredProfession -> {
try {
Villager.Profession profession = Villager.Profession.valueOf(configuredProfession);
this.non_optimized_removal_priority.add(profession);
} catch (IllegalArgumentException e) {
LogUtil.moduleLog(Level.WARNING, "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.");
}
});
this.optimized_max_per_chunk = config.getInt("villager-chunk-limit.optimized.max-per-chunk", 60,
"The maximum amount of optimized villagers per chunk.");
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"
)).forEach(configuredProfession -> {
try {
Villager.Profession profession = Villager.Profession.valueOf(configuredProfession);
this.optimized_removal_priority.add(profession);
} catch (IllegalArgumentException e) {
LogUtil.moduleLog(Level.WARNING, "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.");
}
});
}
@Override
public void enable() {
final VillagerOptimizer plugin = VillagerOptimizer.getInstance();
final Server server = plugin.getServer();
server.getPluginManager().registerEvents(this, plugin);
this.periodic_chunk_check = server.getGlobalRegionScheduler().runAtFixedRate(plugin, periodic_chunk_check -> {
for (World world : server.getWorlds()) {
for (Chunk chunk : world.getLoadedChunks()) {
plugin.getServer().getRegionScheduler().run(
plugin, world, chunk.getX(), chunk.getZ(), check_chunk -> this.manageVillagerCount(chunk)
);
}
}
}, check_period, check_period);
}
@Override
public boolean shouldEnable() {
return VillagerOptimizer.getConfiguration().getBoolean("villager-chunk-limit.enable", false);
}
@Override
public void disable() {
HandlerList.unregisterAll(this);
if (periodic_chunk_check != null) periodic_chunk_check.cancel();
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onCreatureSpawn(CreatureSpawnEvent event) {
Entity spawned = event.getEntity();
if (spawned.getType().equals(EntityType.VILLAGER)) {
this.manageVillagerCount(spawned.getChunk());
}
}
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
private void onInteract(PlayerInteractEntityEvent event) {
Entity clicked = event.getRightClicked();
if (clicked.getType().equals(EntityType.VILLAGER)) {
this.manageVillagerCount(clicked.getChunk());
}
}
private void manageVillagerCount(@NotNull Chunk chunk) {
if (skip_unloaded_entity_chunks && !chunk.isEntitiesLoaded()) return;
// 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().equals(EntityType.VILLAGER)) {
Villager villager = (Villager) entity;
if (villagerCache.getOrAdd(villager).isOptimized()) {
optimized_villagers.add(villager);
} else {
not_optimized_villagers.add(villager);
}
}
}
// Check if there are more unoptimized villagers in that chunk than allowed
final int not_optimized_villagers_too_many = not_optimized_villagers.size() - non_optimized_max_per_chunk;
if (not_optimized_villagers_too_many > 0) {
// Sort villagers by profession priority
not_optimized_villagers.sort(Comparator.comparingInt(villager -> {
final Villager.Profession profession = villager.getProfession();
return non_optimized_removal_priority.contains(profession) ? non_optimized_removal_priority.indexOf(profession) : Integer.MAX_VALUE;
}));
// 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);
villager.remove();
if (log_enabled) LogUtil.moduleLog(Level.INFO, "villager-chunk-limit",
"Removed unoptimized villager of profession type '"+villager.getProfession().name()+"' at "+villager.getLocation()
);
}
}
// Check if there are more optimized villagers in that chunk than allowed
final int optimized_villagers_too_many = optimized_villagers.size() - optimized_max_per_chunk;
if (optimized_villagers_too_many > 0) {
// Sort villagers by profession priority
optimized_villagers.sort(Comparator.comparingInt(villager -> {
final Villager.Profession profession = villager.getProfession();
return optimized_removal_priority.contains(profession) ? optimized_removal_priority.indexOf(profession) : Integer.MAX_VALUE;
}));
// Remove prioritized villagers that are too many
for (int i = 0; i < optimized_villagers_too_many; i++) {
Villager villager = optimized_villagers.get(i);
villager.remove();
if (log_enabled) LogUtil.moduleLog(Level.INFO, "villager-chunk-limit",
"Removed optimized villager of profession type '"+villager.getProfession().name()+"' at "+villager.getLocation()
);
}
}
}
}

View File

@ -0,0 +1,40 @@
package me.xginko.villageroptimizer.modules;
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.util.HashSet;
public interface VillagerOptimizerModule {
void enable();
void disable();
boolean shouldEnable();
HashSet<VillagerOptimizerModule> modules = new HashSet<>();
static void reloadModules() {
modules.forEach(VillagerOptimizerModule::disable);
modules.clear();
modules.add(new OptimizeByNametag());
modules.add(new OptimizeByBlock());
modules.add(new OptimizeByWorkstation());
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 VillagerChunkLimit());
modules.forEach(module -> {
if (module.shouldEnable()) module.enable();
});
}
}

View File

@ -0,0 +1,91 @@
package me.xginko.villageroptimizer.modules.gameplay;
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.CommonUtil;
import net.kyori.adventure.text.TextReplacementConfig;
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.inventory.InventoryCloseEvent;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
public class LevelOptimizedProfession implements VillagerOptimizerModule, Listener {
private final VillagerOptimizer plugin;
private final VillagerCache villagerCache;
private final boolean notify_player;
private final long cooldown;
public LevelOptimizedProfession() {
shouldEnable();
this.plugin = VillagerOptimizer.getInstance();
this.villagerCache = VillagerOptimizer.getCache();
Config config = VillagerOptimizer.getConfiguration();
config.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 = 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.""") * 1000L;
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);
}
@Override
public void disable() {
HandlerList.unregisterAll(this);
}
@Override
public boolean shouldEnable() {
return true;
}
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
private void onTradeScreenClose(InventoryCloseEvent event) {
if (
event.getInventory().getType().equals(InventoryType.MERCHANT)
&& event.getInventory().getHolder() instanceof Villager villager
) {
WrappedVillager wVillager = villagerCache.getOrAdd(villager);
if (!wVillager.isOptimized()) return;
if (wVillager.canLevelUp(cooldown)) {
if (wVillager.calculateLevel() > villager.getVillagerLevel()) {
villager.getScheduler().run(plugin, enableAI -> {
villager.addPotionEffect(new PotionEffect(PotionEffectType.SLOW, 120, 120, false, false));
villager.setAware(true);
}, null);
villager.getScheduler().runDelayed(plugin, disableAI -> {
villager.setAware(false);
wVillager.saveLastLevelUp();
}, null, 100L);
}
} else {
if (notify_player) {
Player player = (Player) event.getPlayer();
final TextReplacementConfig timeLeft = TextReplacementConfig.builder()
.matchLiteral("%time%")
.replacement(CommonUtil.formatTime(wVillager.getLevelCooldownMillis(cooldown)))
.build();
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().equals(EntityType.VILLAGER)) {
Villager villager = (Villager) event.getEntity();
if (!villager.isAdult()) villager.setAdult();
}
}

View File

@ -0,0 +1,86 @@
package me.xginko.villageroptimizer.modules.gameplay;
import io.papermc.paper.event.entity.EntityPushedByEntityAttackEvent;
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.LogUtil;
import org.bukkit.entity.EntityType;
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.EntityDamageEvent;
import java.util.Arrays;
import java.util.HashSet;
public class PreventOptimizedDamage implements VillagerOptimizerModule, Listener {
private final VillagerCache villagerCache;
private final HashSet<EntityDamageEvent.DamageCause> damage_causes_to_cancel = new HashSet<>();
private final boolean push;
public PreventOptimizedDamage() {
shouldEnable();
this.villagerCache = VillagerOptimizer.getCache();
Config config = VillagerOptimizer.getConfiguration();
config.addComment("gameplay.prevent-damage-to-optimized.enable",
"Configure what kind of damage you want to cancel for optimized villagers here.");
this.push = config.getBoolean("gameplay.prevent-damage-to-optimized.prevent-push-from-attack", true,
"Prevents optimized villagers from getting pushed by an attacking entity");
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"""
).forEach(configuredDamageCause -> {
try {
EntityDamageEvent.DamageCause damageCause = EntityDamageEvent.DamageCause.valueOf(configuredDamageCause);
this.damage_causes_to_cancel.add(damageCause);
} catch (IllegalArgumentException e) {
LogUtil.damageCauseNotRecognized("prevent-damage-to-optimized", configuredDamageCause);
}
});
}
@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.prevent-damage-to-optimized.enable", true);
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onDamageByEntity(EntityDamageEvent event) {
if (
event.getEntityType().equals(EntityType.VILLAGER)
&& damage_causes_to_cancel.contains(event.getCause())
&& villagerCache.getOrAdd((Villager) event.getEntity()).isOptimized()
) {
event.setCancelled(true);
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onPushByEntityAttack(EntityPushedByEntityAttackEvent event) {
if (
push
&& 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().equals(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().equals(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().equals(EntityType.VILLAGER)
&& event.getDamager() instanceof Mob attacker
&& villagerCache.getOrAdd((Villager) event.getEntity()).isOptimized()
) {
((Mob) event.getDamager()).setTarget(null);
attacker.setTarget(null);
}
}
}

View File

@ -0,0 +1,80 @@
package me.xginko.villageroptimizer.modules.gameplay;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.enums.Permissions;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
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.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.event.inventory.TradeSelectEvent;
public class PreventUnoptimizedTrading implements VillagerOptimizerModule, Listener {
private final VillagerCache villagerCache;
private final boolean notify_player;
public PreventUnoptimizedTrading() {
shouldEnable();
this.villagerCache = VillagerOptimizer.getCache();
Config config = VillagerOptimizer.getConfiguration();
config.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);
}
@Override
public void disable() {
HandlerList.unregisterAll(this);
}
@Override
public boolean shouldEnable() {
return VillagerOptimizer.getConfiguration().getBoolean("gameplay.prevent-trading-with-unoptimized.enable", false);
}
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
private void onTradeOpen(TradeSelectEvent event) {
Player player = (Player) event.getWhoClicked();
if (player.hasPermission(Permissions.Bypass.TRADE_PREVENTION.get())) return;
if (
event.getInventory().getType().equals(InventoryType.MERCHANT)
&& event.getInventory().getHolder() instanceof Villager villager
&& !villagerCache.getOrAdd(villager).isOptimized()
) {
event.setCancelled(true);
if (notify_player)
VillagerOptimizer.getLang(player.locale()).optimize_for_trading.forEach(player::sendMessage);
}
}
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
private void onInventoryClick(InventoryClickEvent event) {
Player player = (Player) event.getWhoClicked();
if (player.hasPermission(Permissions.Bypass.TRADE_PREVENTION.get())) return;
if (
event.getInventory().getType().equals(InventoryType.MERCHANT)
&& event.getInventory().getHolder() instanceof Villager villager
&& !villagerCache.getOrAdd(villager).isOptimized()
) {
event.setCancelled(true);
if (notify_player)
VillagerOptimizer.getLang(player.locale()).optimize_for_trading.forEach(player::sendMessage);
}
}
}

View File

@ -0,0 +1,79 @@
package me.xginko.villageroptimizer.modules.gameplay;
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 VillagerOptimizer plugin;
private final Component optimized_name;
private final boolean overwrite_previous_name;
public RenameOptimizedVillagers() {
shouldEnable();
this.plugin = VillagerOptimizer.getInstance();
Config config = VillagerOptimizer.getConfiguration();
config.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() {
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", true);
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onOptimize(VillagerOptimizeEvent event) {
WrappedVillager wVillager = event.getWrappedVillager();
Villager villager = wVillager.villager();
villager.getScheduler().runDelayed(plugin, nameOptimized -> {
if (overwrite_previous_name || villager.customName() == null) {
villager.customName(optimized_name);
wVillager.memorizeName(optimized_name);
}
}, null, 10L);
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onUnOptimize(VillagerUnoptimizeEvent event) {
WrappedVillager wVillager = event.getWrappedVillager();
Villager villager = wVillager.villager();
villager.getScheduler().runDelayed(plugin, unNameOptimized -> {
final Component currentName = villager.customName();
final Component memorizedName = wVillager.getMemorizedName();
if (currentName != null && currentName.equals(memorizedName))
villager.customName(null);
if (memorizedName != null)
wVillager.forgetName();
}, null, 10L);
}
}

View File

@ -0,0 +1,80 @@
package me.xginko.villageroptimizer.modules.gameplay;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.enums.Permissions;
import me.xginko.villageroptimizer.WrappedVillager;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import me.xginko.villageroptimizer.utils.CommonUtil;
import net.kyori.adventure.text.TextReplacementConfig;
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;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerInteractEntityEvent;
public class RestockOptimizedTrades implements VillagerOptimizerModule, Listener {
private final VillagerCache villagerCache;
private final long restock_delay_millis;
private final boolean log_enabled, notify_player;
public RestockOptimizedTrades() {
shouldEnable();
this.villagerCache = VillagerOptimizer.getCache();
Config config = VillagerOptimizer.getConfiguration();
config.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("gameplay.restock-optimized-trades.log", false);
}
@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 true;
}
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
private void onInteract(PlayerInteractEntityEvent event) {
if (!event.getRightClicked().getType().equals(EntityType.VILLAGER)) return;
WrappedVillager wVillager = villagerCache.getOrAdd((Villager) event.getRightClicked());
if (!wVillager.isOptimized()) return;
Player player = event.getPlayer();
final boolean player_bypassing = player.hasPermission(Permissions.Bypass.RESTOCK_COOLDOWN.get());
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.formatTime(wVillager.getRestockCooldownMillis(restock_delay_millis)))
.build();
VillagerOptimizer.getLang(player.locale()).trades_restocked.forEach(line -> player.sendMessage(line.replaceText(timeLeft)));
}
if (log_enabled)
VillagerOptimizer.getLog().info("Restocked optimized villager at "+ wVillager.villager().getLocation());
}
}
}

View File

@ -0,0 +1,200 @@
package me.xginko.villageroptimizer.modules.optimization;
import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.WrappedVillager;
import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.enums.OptimizationType;
import me.xginko.villageroptimizer.enums.Permissions;
import me.xginko.villageroptimizer.events.VillagerOptimizeEvent;
import me.xginko.villageroptimizer.events.VillagerUnoptimizeEvent;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import me.xginko.villageroptimizer.utils.CommonUtil;
import me.xginko.villageroptimizer.utils.LogUtil;
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;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import java.util.HashSet;
import java.util.List;
public class OptimizeByBlock implements VillagerOptimizerModule, Listener {
private final VillagerCache villagerCache;
private final HashSet<Material> blocks_that_disable = new HashSet<>(4);
private final long cooldown;
private final double search_radius;
private final boolean only_while_sneaking, notify_player, log_enabled;
public OptimizeByBlock() {
shouldEnable();
this.villagerCache = VillagerOptimizer.getCache();
Config config = VillagerOptimizer.getConfiguration();
config.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.""");
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."
).forEach(configuredMaterial -> {
try {
Material disableBlock = Material.valueOf(configuredMaterial);
this.blocks_that_disable.add(disableBlock);
} catch (IllegalArgumentException e) {
LogUtil.materialNotRecognized("block-optimization", configuredMaterial);
}
});
this.cooldown = 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.""") * 1000L;
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("optimization-methods.block-optimization.log", false);
}
@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("optimization-methods.block-optimization.enable", false);
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onBlockPlace(BlockPlaceEvent event) {
Block placed = event.getBlock();
if (!blocks_that_disable.contains(placed.getType())) return;
Player player = event.getPlayer();
if (!player.hasPermission(Permissions.Optimize.BLOCK.get())) return;
if (only_while_sneaking && !player.isSneaking()) return;
final Location blockLoc = placed.getLocation();
WrappedVillager closestOptimizableVillager = null;
double closestDistance = Double.MAX_VALUE;
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;
WrappedVillager wVillager = villagerCache.getOrAdd(villager);
final double distance = entity.getLocation().distance(blockLoc);
if (distance < closestDistance && wVillager.canOptimize(cooldown)) {
closestOptimizableVillager = wVillager;
closestDistance = distance;
}
}
if (closestOptimizableVillager == null) return;
if (closestOptimizableVillager.canOptimize(cooldown) || player.hasPermission(Permissions.Bypass.BLOCK_COOLDOWN.get())) {
VillagerOptimizeEvent optimizeEvent = new VillagerOptimizeEvent(closestOptimizableVillager, OptimizationType.BLOCK, player, event.isAsynchronous());
if (!optimizeEvent.callEvent()) return;
closestOptimizableVillager.setOptimization(optimizeEvent.getOptimizationType());
closestOptimizableVillager.saveOptimizeTime();
if (notify_player) {
final TextReplacementConfig vilProfession = TextReplacementConfig.builder()
.matchLiteral("%vil_profession%")
.replacement(closestOptimizableVillager.villager().getProfession().toString().toLowerCase())
.build();
final TextReplacementConfig placedMaterial = TextReplacementConfig.builder()
.matchLiteral("%blocktype%")
.replacement(placed.getType().toString().toLowerCase())
.build();
VillagerOptimizer.getLang(player.locale()).block_optimize_success.forEach(line -> player.sendMessage(line
.replaceText(vilProfession)
.replaceText(placedMaterial)
));
}
if (log_enabled)
VillagerOptimizer.getLog().info("Villager was optimized by block at "+closestOptimizableVillager.villager().getLocation());
} else {
closestOptimizableVillager.villager().shakeHead();
if (notify_player) {
final TextReplacementConfig timeLeft = TextReplacementConfig.builder()
.matchLiteral("%time%")
.replacement(CommonUtil.formatTime(closestOptimizableVillager.getOptimizeCooldownMillis(cooldown)))
.build();
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) {
Block broken = event.getBlock();
if (!blocks_that_disable.contains(broken.getType())) return;
Player player = event.getPlayer();
if (!player.hasPermission(Permissions.Optimize.BLOCK.get())) return;
if (only_while_sneaking && !player.isSneaking()) return;
final Location blockLoc = broken.getLocation();
WrappedVillager closestOptimizedVillager = null;
double closestDistance = Double.MAX_VALUE;
for (Entity entity : blockLoc.getNearbyEntities(search_radius, search_radius, search_radius)) {
if (!entity.getType().equals(EntityType.VILLAGER)) continue;
Villager villager = (Villager) entity;
WrappedVillager wVillager = villagerCache.getOrAdd(villager);
final double distance = entity.getLocation().distance(blockLoc);
if (distance < closestDistance && wVillager.isOptimized()) {
closestOptimizedVillager = wVillager;
closestDistance = distance;
}
}
if (closestOptimizedVillager == null) return;
VillagerUnoptimizeEvent unOptimizeEvent = new VillagerUnoptimizeEvent(closestOptimizedVillager, player, OptimizationType.BLOCK, event.isAsynchronous());
if (!unOptimizeEvent.callEvent()) return;
closestOptimizedVillager.setOptimization(OptimizationType.NONE);
if (notify_player) {
final TextReplacementConfig vilProfession = TextReplacementConfig.builder()
.matchLiteral("%vil_profession%")
.replacement(closestOptimizedVillager.villager().getProfession().toString().toLowerCase())
.build();
final TextReplacementConfig brokenMaterial = TextReplacementConfig.builder()
.matchLiteral("%blocktype%")
.replacement(broken.getType().toString().toLowerCase())
.build();
VillagerOptimizer.getLang(player.locale()).block_unoptimize_success.forEach(line -> player.sendMessage(line
.replaceText(vilProfession)
.replaceText(brokenMaterial)
));
}
if (log_enabled)
VillagerOptimizer.getLog().info("Villager unoptimized because nearby optimization block broken at: "+closestOptimizedVillager.villager().getLocation());
}
}

View File

@ -0,0 +1,133 @@
package me.xginko.villageroptimizer.modules.optimization;
import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.WrappedVillager;
import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.enums.OptimizationType;
import me.xginko.villageroptimizer.enums.Permissions;
import me.xginko.villageroptimizer.events.VillagerOptimizeEvent;
import me.xginko.villageroptimizer.events.VillagerUnoptimizeEvent;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import me.xginko.villageroptimizer.utils.CommonUtil;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextReplacementConfig;
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;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerInteractEntityEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import java.util.HashSet;
import java.util.List;
public class OptimizeByNametag implements VillagerOptimizerModule, Listener {
private final VillagerCache villagerCache;
private final HashSet<String> nametags = new HashSet<>(4);
private final long cooldown;
private final boolean consume_nametag, notify_player, log_enabled;
public OptimizeByNametag() {
shouldEnable();
this.villagerCache = VillagerOptimizer.getCache();
Config config = VillagerOptimizer.getConfiguration();
config.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.addAll(config.getList("optimization-methods.nametag-optimization.names", List.of("Optimize", "DisableAI"),
"Names are case insensitive, capital letters won't matter.").stream().map(String::toLowerCase).toList());
this.consume_nametag = config.getBoolean("optimization-methods.nametag-optimization.nametags-get-consumed", true,
"Enable or disable consumption of the used nametag item.");
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("optimization-methods.nametag-optimization.log", false);
}
@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("optimization-methods.nametag-optimization.enable", true);
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onPlayerInteractEntity(PlayerInteractEntityEvent event) {
if (!event.getRightClicked().getType().equals(EntityType.VILLAGER)) return;
Player player = event.getPlayer();
if (!player.hasPermission(Permissions.Optimize.NAMETAG.get())) return;
ItemStack usedItem = player.getInventory().getItem(event.getHand());
if (!usedItem.getType().equals(Material.NAME_TAG)) return;
ItemMeta meta = usedItem.getItemMeta();
if (!meta.hasDisplayName()) return;
// 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(Permissions.Bypass.NAMETAG_COOLDOWN.get())) {
VillagerOptimizeEvent optimizeEvent = new VillagerOptimizeEvent(wVillager, OptimizationType.NAMETAG, player, event.isAsynchronous());
if (!optimizeEvent.callEvent()) return;
if (!consume_nametag) {
event.setCancelled(true);
villager.customName(newVillagerName);
}
wVillager.setOptimization(optimizeEvent.getOptimizationType());
wVillager.saveOptimizeTime();
if (notify_player)
VillagerOptimizer.getLang(player.locale()).nametag_optimize_success.forEach(player::sendMessage);
if (log_enabled)
VillagerOptimizer.getLog().info(player.getName() + " optimized a villager using nametag: '" + name + "'");
} else {
event.setCancelled(true);
villager.shakeHead();
if (notify_player) {
final TextReplacementConfig timeLeft = TextReplacementConfig.builder()
.matchLiteral("%time%")
.replacement(CommonUtil.formatTime(wVillager.getOptimizeCooldownMillis(cooldown)))
.build();
VillagerOptimizer.getLang(player.locale()).nametag_on_optimize_cooldown.forEach(line -> player.sendMessage(line.replaceText(timeLeft)));
}
}
} else {
if (wVillager.isOptimized()) {
VillagerUnoptimizeEvent unOptimizeEvent = new VillagerUnoptimizeEvent(wVillager, player, OptimizationType.NAMETAG, event.isAsynchronous());
if (!unOptimizeEvent.callEvent()) return;
wVillager.setOptimization(OptimizationType.NONE);
if (notify_player)
VillagerOptimizer.getLang(player.locale()).nametag_unoptimize_success.forEach(player::sendMessage);
if (log_enabled)
VillagerOptimizer.getLog().info(event.getPlayer().getName() + " disabled optimizations for a villager using nametag: '" + name + "'");
}
}
}
}

View File

@ -0,0 +1,207 @@
package me.xginko.villageroptimizer.modules.optimization;
import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.WrappedVillager;
import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.enums.OptimizationType;
import me.xginko.villageroptimizer.enums.Permissions;
import me.xginko.villageroptimizer.events.VillagerOptimizeEvent;
import me.xginko.villageroptimizer.events.VillagerUnoptimizeEvent;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import me.xginko.villageroptimizer.utils.CommonUtil;
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;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent;
public class OptimizeByWorkstation implements VillagerOptimizerModule, Listener {
private final VillagerCache villagerCache;
private final long cooldown;
private final double search_radius;
private final boolean only_while_sneaking, log_enabled, notify_player;
public OptimizeByWorkstation() {
shouldEnable();
this.villagerCache = VillagerOptimizer.getCache();
Config config = VillagerOptimizer.getConfiguration();
config.addComment("optimization-methods.workstation-optimization.enable", """
When enabled, the closest villager near a matching workstation being placed will be optimized.\s
If a nearby matching workstation is broken, the villager will become unoptimized again.""");
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.""") / 2;
this.cooldown = 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.""") * 1000L;
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("optimization-methods.workstation-optimization.log", false);
}
@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("optimization-methods.workstation-optimization.enable", false);
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onBlockPlace(BlockPlaceEvent event) {
Block placed = event.getBlock();
Villager.Profession workstationProfession = getWorkstationProfession(placed.getType());
if (workstationProfession.equals(Villager.Profession.NONE)) return;
Player player = event.getPlayer();
if (!player.hasPermission(Permissions.Optimize.WORKSTATION.get())) return;
if (only_while_sneaking && !player.isSneaking()) return;
final Location workstationLoc = placed.getLocation();
WrappedVillager closestOptimizableVillager = null;
double closestDistance = Double.MAX_VALUE;
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 double distance = entity.getLocation().distance(workstationLoc);
if (distance < closestDistance && wVillager.canOptimize(cooldown)) {
closestOptimizableVillager = wVillager;
closestDistance = distance;
}
}
if (closestOptimizableVillager == null) return;
if (closestOptimizableVillager.canOptimize(cooldown) || player.hasPermission(Permissions.Bypass.WORKSTATION_COOLDOWN.get())) {
VillagerOptimizeEvent optimizeEvent = new VillagerOptimizeEvent(closestOptimizableVillager, OptimizationType.WORKSTATION, player, event.isAsynchronous());
if (!optimizeEvent.callEvent()) return;
closestOptimizableVillager.setOptimization(optimizeEvent.getOptimizationType());
closestOptimizableVillager.saveOptimizeTime();
if (notify_player) {
final TextReplacementConfig vilProfession = TextReplacementConfig.builder()
.matchLiteral("%vil_profession%")
.replacement(closestOptimizableVillager.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)
));
}
if (log_enabled)
VillagerOptimizer.getLog().info(player.getName() + " optimized a villager using workstation: '" + placed.getType().toString().toLowerCase() + "'");
} else {
closestOptimizableVillager.villager().shakeHead();
if (notify_player) {
final TextReplacementConfig timeLeft = TextReplacementConfig.builder()
.matchLiteral("%time%")
.replacement(CommonUtil.formatTime(closestOptimizableVillager.getOptimizeCooldownMillis(cooldown)))
.build();
VillagerOptimizer.getLang(player.locale()).nametag_on_optimize_cooldown.forEach(line -> player.sendMessage(line
.replaceText(timeLeft)
));
}
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onBlockBreak(BlockBreakEvent event) {
Block broken = event.getBlock();
Villager.Profession workstationProfession = getWorkstationProfession(broken.getType());
if (workstationProfession.equals(Villager.Profession.NONE)) return;
Player player = event.getPlayer();
if (!player.hasPermission(Permissions.Optimize.WORKSTATION.get())) return;
if (only_while_sneaking && !player.isSneaking()) return;
final Location workstationLoc = broken.getLocation();
WrappedVillager closestOptimizedVillager = null;
double closestDistance = Double.MAX_VALUE;
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 double distance = entity.getLocation().distance(workstationLoc);
if (distance < closestDistance && wVillager.canOptimize(cooldown)) {
closestOptimizedVillager = wVillager;
closestDistance = distance;
}
}
if (closestOptimizedVillager == null) return;
VillagerUnoptimizeEvent unOptimizeEvent = new VillagerUnoptimizeEvent(closestOptimizedVillager, player, OptimizationType.WORKSTATION, event.isAsynchronous());
if (!unOptimizeEvent.callEvent()) return;
closestOptimizedVillager.setOptimization(OptimizationType.NONE);
if (notify_player) {
final TextReplacementConfig vilProfession = TextReplacementConfig.builder()
.matchLiteral("%vil_profession%")
.replacement(closestOptimizedVillager.villager().getProfession().toString().toLowerCase())
.build();
final TextReplacementConfig brokenWorkstation = TextReplacementConfig.builder()
.matchLiteral("%workstation%")
.replacement(broken.getType().toString().toLowerCase())
.build();
VillagerOptimizer.getLang(player.locale()).workstation_unoptimize_success.forEach(line -> player.sendMessage(line
.replaceText(vilProfession)
.replaceText(brokenWorkstation)
));
}
if (log_enabled)
VillagerOptimizer.getLog().info(player.getName() + " unoptimized a villager by breaking workstation: '" + broken.getType().toString().toLowerCase() + "'");
}
private Villager.Profession getWorkstationProfession(final 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

@ -0,0 +1,24 @@
package me.xginko.villageroptimizer.utils;
import org.jetbrains.annotations.NotNull;
import java.time.Duration;
import static java.lang.String.format;
public class CommonUtil {
public static @NotNull String formatTime(final long millis) {
Duration duration = Duration.ofMillis(millis);
final int seconds = duration.toSecondsPart();
final int minutes = duration.toMinutesPart();
final int hours = duration.toHoursPart();
if (hours > 0) {
return format("%02dh %02dm %02ds", hours, minutes, seconds);
} else if (minutes > 0) {
return format("%02dm %02ds", minutes, seconds);
} else {
return format("%02ds", seconds);
}
}
}

View File

@ -0,0 +1,26 @@
package me.xginko.villageroptimizer.utils;
import me.xginko.villageroptimizer.VillagerOptimizer;
import java.util.logging.Level;
public class LogUtil {
public static void moduleLog(Level logLevel, String path, String logMessage) {
VillagerOptimizer.getLog().log(logLevel, "(" + path + ") " + logMessage);
}
public static void materialNotRecognized(String path, String material) {
moduleLog(Level.WARNING, path, "Material '" + material + "' not recognized. Please use correct Material enums from: " +
"https://jd.papermc.io/paper/1.20/org/bukkit/Material.html");
}
public static void damageCauseNotRecognized(String path, String cause) {
moduleLog(Level.WARNING, path, "DamageCause '" + cause + "' not recognized. Please use correct DamageCause enums from: " +
"https://jd.papermc.io/paper/1.20/org/bukkit/event/entity/EntityDamageEvent.DamageCause.html");
}
public static void entityTypeNotRecognized(String path, String entityType) {
moduleLog(Level.WARNING, path, "EntityType '" + entityType + "' not recognized. Please use correct Spigot EntityType enums for your Minecraft version!");
}
}

View File

@ -0,0 +1,44 @@
messages:
no-permission: "<red>Du hast keine Berechtigung für diesen Befehl."
optimize-to-trade:
- "<red>Optimiere den Dorfbewohner um mit ihm handeln zu können."
trades-restocked:
- "<green>Alle Angebote sind wieder verfügbar! Nächste Auffrischung in %time%."
villager-leveling-up:
- "<yellow>Dorfbewohner steigt gerade im Level auf und kann in %time% wieder handeln."
nametag:
optimize-success:
- "<green>Dorfbewohner erfolgreich mit einem Namensschild optimiert."
optimize-on-cooldown:
- "<gray>Du musst %time% warten, bevor du diesen Dorfbewohner erneut optimieren kannst."
unoptimize-success:
- "<green>Optimierung erfolgreich durch eine Namensschildänderung aufgehoben."
block:
optimize-success:
- "<green>%vil_profession% Dorfbewohner erfolgreich mit %blocktype% optimiert."
optimize-on-cooldown:
- "<gray>Du musst %time% warten, bevor du diesen Dorfbewohner erneut optimieren kannst."
unoptimize-success:
- "<green>Optimierung des %vil_profession% Dorfbewohners erfolgreich durch Abbauen von %blocktype% aufgehoben."
workstation:
optimize-success:
- "<green>%vil_profession% Dorfbewohner erfolgreich mit Arbeitsplatz-Block %blocktype% optimiert."
optimize-on-cooldown:
- "<gray>Du musst %time% warten, bevor du diesen Dorfbewohner erneut optimieren kannst."
unoptimize-success:
- "<green>Optimierung des %vil_profession% Dorfbewohners erfolgreich durch Abbauen von %blocktype% aufgehoben."
command:
optimize-success:
- "<green>Erfolgreich %amount% Dorfbewohner in einem Radius von %radius% Blöcken optimiert."
optimize-fail:
- "<gray>%amount% Dorfbewohner konnten nicht optimiert werden, da sie erst vor Kurzem optimiert wurden."
radius-limit-exceed:
- "<red>Der eingegebene Radius überschreitet das Limit von %distance% Blöcken."
unoptimize-success:
- "<green>Optimierung von %amount% Dorfbewohnern in einem Radius von %radius% Blöcken erfolgreich aufgehoben."
specify-radius:
- "<red>Bitte gib einen Radius an."
radius-invalid:
- "<red>Der eingegebene Radius ist keine gültige Zahl. Versuche es erneut."
no-villagers-nearby:
- "<gray>Es wurden keine beschäftigten Dorfbewohner innerhalb eines Radius von %radius% Blöcken gefunden."

View File

@ -0,0 +1,44 @@
messages:
no-permission: "<red>You don't have permission to use this command."
optimize-to-trade:
- "<red>You need to optimize this villager before you can trade with it."
trades-restocked:
- "<green>All trades have been restocked! Next restock in %time%."
villager-leveling-up:
- "<yellow>Villager is currently leveling up! You can use the villager again in %time%."
nametag:
optimize-success:
- "<green>Successfully optimized villager by using a nametag."
optimize-on-cooldown:
- "<gray>You need to wait %time% until you can optimize this villager again."
unoptimize-success:
- "<green>Successfully unoptimized villager by using a nametag."
block:
optimize-success:
- "<green>%vil_profession% villager successfully optimized using block %blocktype%."
optimize-on-cooldown:
- "<gray>You need to wait %time% until you can optimize this villager again."
unoptimize-success:
- "<green>Successfully unoptimized %vil_profession% villager by removing %blocktype%."
workstation:
optimize-success:
- "<green>%vil_profession% villager successfully optimized using workstation block %blocktype%."
optimize-on-cooldown:
- "<gray>You need to wait %time% until you can optimize this villager again."
unoptimize-success:
- "<green>Successfully unoptimized villager by removing workstation block %blocktype%."
command:
optimize-success:
- "<green>Successfully optimized %amount% villager(s) in a radius of %radius% blocks."
optimize-fail:
- "<gray>%amount% villagers couldn't be optimized because they have recently been optimized."
radius-limit-exceed:
- "<red>The radius you entered exceeds the limit of %distance% blocks."
unoptimize-success:
- "<green>Successfully unoptimized %amount% villager(s) in a radius of %radius% blocks."
specify-radius:
- "<red>Please specify a radius."
radius-invalid:
- "<red>The radius you entered is not a valid number. Try again."
no-villagers-nearby:
- "<gray>Couldn't find any employed villagers within a radius of %radius%."

View File

@ -0,0 +1,44 @@
messages:
no-permission: "<red>Non hai il permesso per usare questo comando."
optimize-to-trade:
- "<red>Devi ottimizzare il villager prima di poterci commerciare."
trades-restocked:
- "<green>Tutti i commerci sono stati riavviati! Prossimo riavvio in %time%."
villager-leveling-up:
- "<yellow>Il villager si sta ancora ottimizzando! Potrai utilizzarlo di nuovo tra %time%."
nametag:
optimize-success:
- "<green>Villager ottimizzato correttamente utilizzando il name-tag."
optimize-on-cooldown:
- "<gray>Devi attendere %time% prima di poter ottimizzare di nuovo questo villager."
unoptimize-success:
- "<green>Ottimizzazione rimossa con successo dal villager usando il name-tag."
block:
optimize-success:
- "<green>%vil_profession% ottimizzato con successo usando il blocco: %blocktype%."
optimize-on-cooldown:
- "<gray>Devi attendere %time% prima di poter ottimizzare di nuovo questo villager."
unoptimize-success:
- "<green>Ottimizzazione rimossa al villager %vil_profession% rimuovendo il blocco: %blocktype%."
workstation:
optimize-success:
- "<green>%vil_profession% ottimizzato con successo usando la workstation: %blocktype%."
optimize-on-cooldown:
- "<gray>Devi aspettare %time% prima di poter ottimizzare il villager."
unoptimize-success:
- "<green>Ottimizzazione rimossa al villager %vil_profession% rimuovendo la workstation: %blocktype%."
command:
optimize-success:
- "<green>Ottimizzato con successo %amount% villager in un raggio di %radius% blocchi."
optimize-fail:
- "<gray>%amount% villager non possono essere ottimizzati poiché sono stati già ottimizzati di recente."
radius-limit-exceed:
- "<red>Il raggio inserito supera il limite di %distance% blocchi."
unoptimize-success:
- "<green>Ottimizzazione rimossa con successo da %amount% villager in un raggio di %radius% blocchi."
specify-radius:
- "<red>Per favore, specifica un raggio."
radius-invalid:
- "<red>Il raggio inserito non è un numero valido, riprova."
no-villagers-nearby:
- "<gray>Non è stato possibile trovare villager entro un raggio di %radius% blocchi."

View File

@ -0,0 +1,44 @@
messages:
no-permission: "<red>У вас нет разрешения на использование данной команды."
optimize-to-trade:
- "<red>Вам необходимо оптимизировать данного жителя перед торговлей."
trades-restocked:
- "<green>Товары были обновлены. Следующее обновление через %time%."
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>Оптимизация была успешно снята с %vil_profession% удалением %blocktype%."
workstation:
optimize-success:
- "<green>%vil_profession% был успешно оптимизирован используя %blocktype%."
optimize-on-cooldown:
- "<gray>Подождите еще %time% для повторной оптимизации данного жителя."
unoptimize-success:
- "<green>Оптимизация была успешно снята с %vil_profession% удалением %blocktype%."
command:
optimize-success:
- "<green>Успешно оптимизинованы %amount% жителей в радиусе %radius% блоков."
optimize-fail:
- "<gray>%amount% жителей не могут быть оптимизированы снова так как они уже были оптимизированы недавно."
radius-limit-exceed:
- "<red>Укажите радиус меньше %distance% блоков."
unoptimize-success:
- "<green>Оптимизация была успешно снята с %amount% жителей в радиусе %radius% блоков."
specify-radius:
- "<red>Пожалуйста, укажите радиус."
radius-invalid:
- "<red>Указанный вами радиус не является действительным значением."
no-villagers-nearby:
- "<gray>Не найдено работающих жителей в радиусе %radius% блоков."

View File

@ -0,0 +1,98 @@
name: VillagerOptimizer
version: '${project.version}'
main: me.xginko.villageroptimizer.VillagerOptimizer
authors: [ xGinko ]
description: ${project.description}
website: ${project.url}
api-version: '1.19'
folia-supported: true
commands:
villageroptimizer:
usage: /villageroptimizer [ reload, version, disable ]
description: VillagerOptimizer admin commands
aliases:
- voptimizer
- vo
optimizevillagers:
usage: /optimizevillagers <blockradius>
description: Optmize villagers in a radius around you
aliases:
- optvils
- noai
unoptimizevillagers:
usage: /unoptimizevillagers <blockradius>
description: Unoptmize villagers in a radius around you
aliases:
- unoptvils
- noaiundo
permissions:
villageroptimizer.ignore:
description: Players with this permission won't be able to use the plugin features
children:
villageroptimizer.optimize.nametag: false
villageroptimizer.optimize.block: false
villageroptimizer.optimize.workstation: false
villageroptimizer.playerdefaults:
description: Default permissions for players
default: true
children:
villageroptimizer.cmd.optimize: true
villageroptimizer.cmd.unoptimize: true
villageroptimizer.optimize.*: true
villageroptimizer.*:
description: All plugin permissions
children:
villageroptimizer.cmd.*: true
villageroptimizer.bypass.*: true
villageroptimizer.optimize.*: true
villageroptimizer.optimize.*:
description: Optimization type permissions
children:
villageroptimizer.optimize.nametag: true
villageroptimizer.optimize.block: true
villageroptimizer.optimize.workstation: true
villageroptimizer.optimize.nametag:
description: Optimize/Unoptimize villagers using nametags
villageroptimizer.optimize.block:
description: Optimize/Unoptimize villagers using specific blocks
villageroptimizer.optimize.workstation:
description: Optimize/Unoptimize villagers using workstations
villageroptimizer.cmd.*:
description: All command permissions
children:
villageroptimizer.cmd.reload: true
villageroptimizer.cmd.disable: true
villageroptimizer.cmd.version: true
villageroptimizer.cmd.optimize: true
villageroptimizer.cmd.unoptimize: true
villageroptimizer.cmd.disable:
description: Disable the plugin
villageroptimizer.cmd.reload:
description: Reload the plugin configuration
villageroptimizer.cmd.version:
description: Show the plugin version
villageroptimizer.cmd.optimize:
description: Optimize villagers in a radius
villageroptimizer.cmd.unoptimize:
description: Unoptimize villagers in a radius
villageroptimizer.bypass.*:
description: All bypass permissions
children:
villageroptimizer.bypass.tradeprevention: true
villageroptimizer.bypass.restockcooldown: true
villageroptimizer.bypass.nametagcooldown: true
villageroptimizer.bypass.blockcooldown: true
villageroptimizer.bypass.workstationcooldown: true
villageroptimizer.bypass.commandcooldown: true
villageroptimizer.bypass.tradeprevention:
description: Bypass unoptimized trading prevention if enabled
villageroptimizer.bypass.restockcooldown:
description: Bypass permission for optimized trade restock cooldown
villageroptimizer.bypass.nametagcooldown:
description: Bypass permission for nametag optimization cooldown
villageroptimizer.bypass.blockcooldown:
description: Bypass permission for block optimization cooldown
villageroptimizer.bypass.workstationcooldown:
description: Bypass permission for workstation optimization cooldown
villageroptimizer.bypass.commandcooldown:
description: Bypass permission for command optimization cooldown

125
pom.xml
View File

@ -4,17 +4,20 @@
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>
<packaging>jar</packaging>
<version>1.0.1</version>
<modules>
<module>VillagerOptimizer-1.20.2</module>
<module>VillagerOptimizer-1.16.5</module>
</modules>
<packaging>pom</packaging>
<name>VillagerOptimizer</name>
<description>Combat heavy villager lag by letting players optimize their trading halls.</description>
<url>https://github.com/xGinko/VillagerOptimizer</url>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
@ -23,7 +26,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>
@ -40,51 +43,17 @@
<goal>shade</goal>
</goals>
<configuration>
<relocations>
<relocation>
<pattern>com.github.benmanes.caffeine</pattern>
<shadedPattern>me.xginko.villageroptimizer.libs.caffeine</shadedPattern>
</relocation>
<relocation>
<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>
<createDependencyReducedPom>false</createDependencyReducedPom>
<finalName>${project.parent.artifactId}-${project.parent.version}--${project.artifactId}</finalName>
<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>
<createDependencyReducedPom>false</createDependencyReducedPom>
</configuration>
</execution>
</executions>
@ -100,94 +69,36 @@
<repositories>
<repository>
<id>papermc-repo</id>
<url>https://repo.papermc.io/repository/maven-public/</url>
</repository>
<repository>
<id>sonatype</id>
<url>https://oss.sonatype.org/content/groups/public/</url>
<id>papermc</id>
<url>https://papermc.io/repo/repository/maven-public/</url>
</repository>
<repository>
<id>configmaster-repo</id>
<url>https://ci.pluginwiki.us/plugin/repository/everything/</url>
</repository>
<repository>
<id>morepaperlib-repo</id>
<url>https://mvn-repo.arim.space/lesser-gpl3/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>io.papermc.paper</groupId>
<artifactId>paper-api</artifactId>
<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>
</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>
</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) -->
<!-- Caching -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.9.3</version>
</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>
<version>3.1.8</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

View File

@ -1,259 +0,0 @@
package me.xginko.villageroptimizer;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
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.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.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.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 {
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 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);
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(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();
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)));
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));
}
@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() {
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() {
return config;
}
public static @NotNull ComponentLogger logger() {
return logger;
}
public static @NotNull BukkitAudiences audiences() {
return audiences;
}
public static @NotNull 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 @NotNull 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()));
}
public void reloadPlugin() {
reloadLang(false);
reloadConfiguration();
}
private void reloadConfiguration() {
try {
config = new Config();
if (wrapperCache != null) wrapperCache.cleanUp();
wrapperCache = Caffeine.newBuilder().expireAfterWrite(config.cache_keep_time).build();
VillagerOptimizerCommand.reloadCommands();
VillagerOptimizerModule.reloadModules();
config.saveConfig();
} catch (Exception exception) {
logger.error("Error during config reload!", exception);
}
}
private void reloadLang(boolean logFancy) {
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)));
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);
}
}
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();
}
}
}

View File

@ -1,36 +0,0 @@
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 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;
}
}

View File

@ -1,62 +0,0 @@
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 org.bukkit.command.CommandExecutor;
import org.bukkit.command.PluginCommand;
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.HashSet;
import java.util.List;
import java.util.Set;
public abstract class VillagerOptimizerCommand implements Enableable, Disableable, 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());
public final PluginCommand pluginCommand;
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.");
}
public static void reloadCommands() {
COMMANDS.forEach(VillagerOptimizerCommand::disable);
COMMANDS.clear();
for (Class<?> clazz : COMMANDS_PACKAGE.get(Scanners.SubTypes.of(VillagerOptimizerCommand.class).asClass())) {
if (clazz.isInterface() || Modifier.isAbstract(clazz.getModifiers())) continue;
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());
}
}

View File

@ -1,88 +0,0 @@
package me.xginko.villageroptimizer.commands.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 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 {
private final List<SubCommand> subCommands;
private final List<String> tabCompletes;
public VillagerOptimizerCmd() {
super("villageroptimizer");
subCommands = Arrays.asList(new ReloadSubCmd(), new VersionSubCmd(), new DisableSubCmd());
tabCompletes = subCommands.stream().map(SubCommand::label).collect(Collectors.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();
}
@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);
}
}
}
overview(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)
.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)
.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));
}
}

View File

@ -1,52 +0,0 @@
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.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.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 @Nullable List<String> onTabComplete(
@NotNull CommandSender sender, @NotNull Command command, @NotNull String commandLabel, @NotNull String[] args
) {
return Collections.emptyList();
}
@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;
}
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;
}
}

View File

@ -1,50 +0,0 @@
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 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.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 @Nullable List<String> onTabComplete(
@NotNull CommandSender sender, @NotNull Command command, @NotNull String commandLabel, @NotNull String[] args
) {
return Collections.emptyList();
}
@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;
}
KyoriUtil.sendMessage(sender, Component.text("Reloading VillagerOptimizer...").color(NamedTextColor.WHITE));
VillagerOptimizer.scheduling().asyncScheduler().run(reload -> {
VillagerOptimizer.getInstance().reloadPlugin();
KyoriUtil.sendMessage(sender, Component.text("Reload complete.").color(NamedTextColor.GREEN));
});
return true;
}
}

View File

@ -1,80 +0,0 @@
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 net.kyori.adventure.text.Component;
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 @Nullable List<String> onTabComplete(
@NotNull CommandSender sender, @NotNull Command command, @NotNull String commandLabel, @NotNull String[] args
) {
return Collections.emptyList();
}
@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;
}
String name, version, website, author;
try {
final PluginMeta pluginMeta = VillagerOptimizer.getInstance().getPluginMeta();
name = pluginMeta.getName();
version = pluginMeta.getVersion();
website = pluginMeta.getWebsite();
author = pluginMeta.getAuthors().get(0);
} catch (Throwable versionIncompatible) {
final PluginDescriptionFile pluginYML = VillagerOptimizer.getInstance().getDescription();
name = pluginYML.getName();
version = pluginYML.getVersion();
website = pluginYML.getWebsite();
author = pluginYML.getAuthors().get(0);
}
KyoriUtil.sendMessage(sender, Component.newline()
.append(
Component.text(name + " " + version)
.style(Util.PL_STYLE)
.clickEvent(ClickEvent.openUrl(website))
)
.append(Component.text(" by ").color(NamedTextColor.GRAY))
.append(
Component.text(author)
.color(NamedTextColor.WHITE)
.clickEvent(ClickEvent.openUrl("https://github.com/xGinko"))
)
.append(Component.newline())
);
return true;
}
}

View File

@ -1,130 +0,0 @@
package me.xginko.villageroptimizer.config;
import io.github.thatsmusic99.configurationmaster.api.ConfigFile;
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;
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 Config() throws Exception {
// Load config.yml with ConfigMaster
this.config = ConfigFile.loadConfig(new File(VillagerOptimizer.getInstance().getDataFolder(), "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.")
.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.");
}
public void saveConfig() {
try {
this.config.save();
} catch (Throwable throwable) {
VillagerOptimizer.logger().error("Failed to save config file!", throwable);
}
}
private void structureConfig() {
this.config.addDefault("config-version", 1.00);
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.addDefault("optimization-methods.nametag-optimization.enable", true);
this.createTitledSection("Villager Chunk Limit", "villager-chunk-limit");
this.createTitledSection("Gameplay", "gameplay");
this.config.addDefault("gameplay.prevent-trading-with-unoptimized.enable", false);
this.config.addDefault("gameplay.restock-optimized-trades", null);
this.config.addDefault("gameplay.level-optimized-profession", null);
this.config.addDefault("gameplay.unoptimize-on-job-loose.enable", true);
this.config.addDefault("gameplay.villagers-can-be-leashed.enable", true);
this.config.addDefault("gameplay.villagers-spawn-as-adults.enable", false);
this.config.addDefault("gameplay.rename-optimized-villagers.enable", false);
this.config.addDefault("gameplay.prevent-entities-from-targeting-optimized.enable", true);
this.config.addDefault("gameplay.prevent-damage-to-optimized.enable", true);
}
public void createTitledSection(@NotNull String title, @NotNull String path) {
this.config.addSection(title);
this.config.addDefault(path, null);
}
public @NotNull ConfigFile master() {
return this.config;
}
public boolean getBoolean(@NotNull String path, boolean def, @NotNull String comment) {
this.config.addDefault(path, def, comment);
return this.config.getBoolean(path, def);
}
public boolean getBoolean(@NotNull String path, boolean def) {
this.config.addDefault(path, def);
return this.config.getBoolean(path, def);
}
public @NotNull String getString(@NotNull String path, @NotNull String def, @NotNull String comment) {
this.config.addDefault(path, def, comment);
return this.config.getString(path, def);
}
public @NotNull String getString(@NotNull String path, @NotNull String def) {
this.config.addDefault(path, def);
return this.config.getString(path, def);
}
public double getDouble(@NotNull String path, @NotNull Double def, @NotNull String comment) {
this.config.addDefault(path, def, comment);
return this.config.getDouble(path, def);
}
public double getDouble(@NotNull String path, @NotNull Double def) {
this.config.addDefault(path, def);
return this.config.getDouble(path, def);
}
public int getInt(@NotNull String path, int def, @NotNull String comment) {
this.config.addDefault(path, def, comment);
return this.config.getInteger(path, def);
}
public int getInt(@NotNull String path, int def) {
this.config.addDefault(path, def);
return this.config.getInteger(path, def);
}
public @NotNull <T> List<T> getList(@NotNull String path, @NotNull List<T> def, @NotNull String comment) {
this.config.addDefault(path, def, comment);
return this.config.getList(path);
}
public @NotNull <T> List<T> getList(@NotNull String path, @NotNull List<T> def) {
this.config.addDefault(path, def);
return this.config.getList(path);
}
}

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

@ -1,232 +0,0 @@
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 org.bukkit.Chunk;
import org.bukkit.World;
import org.bukkit.entity.Entity;
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.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 {
private ScheduledTask 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;
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,
"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,
"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,
"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());
}
@Override
public void enable() {
plugin.getServer().getPluginManager().registerEvents(this, plugin);
periodic_chunk_check = scheduling.globalRegionalScheduler().runAtFixedRate(this, check_period, check_period);
}
@Override
public boolean shouldEnable() {
return config.getBoolean(configPath + ".enable", false);
}
@Override
public void disable() {
HandlerList.unregisterAll(this);
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());
});
}
}
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
private void onInteract(PlayerInteractEntityEvent event) {
if (event.getRightClicked().getType() == XEntityType.VILLAGER.get()) {
scheduling.regionSpecificScheduler(event.getRightClicked().getLocation()).run(() -> {
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);
}
}
// Check if there are more unoptimized villagers in that chunk than allowed
final int not_optimized_villagers_too_many = not_optimized_villagers.size() - non_optimized_max_per_chunk;
if (not_optimized_villagers_too_many > 0) {
// Sort villagers by profession priority
not_optimized_villagers.sort(Comparator.comparingInt(villager -> {
final Villager.Profession profession = villager.getProfession();
return non_optimized_removal_priority.contains(profession) ? non_optimized_removal_priority.indexOf(profession) : Integer.MAX_VALUE;
}));
// 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 -> {
villager.remove();
if (log_enabled) info("Removed unoptimized villager with profession '" +
Util.toNiceString(villager.getProfession()) + "' at " + LocationUtil.toString(villager.getLocation()));
}, null);
}
}
// Check if there are more optimized villagers in that chunk than allowed
final int optimized_villagers_too_many = optimized_villagers.size() - optimized_max_per_chunk;
if (optimized_villagers_too_many > 0) {
// Sort villagers by profession priority
optimized_villagers.sort(Comparator.comparingInt(villager -> {
final Villager.Profession profession = villager.getProfession();
return optimized_removal_priority.contains(profession) ? optimized_removal_priority.indexOf(profession) : Integer.MAX_VALUE;
}));
// 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 -> {
villager.remove();
if (log_enabled) info("Removed unoptimized villager with profession '" +
Util.toNiceString(villager.getProfession()) + "' at " + LocationUtil.toString(villager.getLocation()));
}, null);
}
}
}
}

View File

@ -1,87 +0,0 @@
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 java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.HashSet;
import java.util.Set;
public abstract class VillagerOptimizerModule implements Enableable, Disableable {
private static final Reflections MODULES_PACKAGE = new Reflections(VillagerOptimizerModule.class.getPackage().getName());
public static final Set<VillagerOptimizerModule> ENABLED_MODULES = new HashSet<>();
public abstract boolean shouldEnable();
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;
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] + "> {}";
}
}
public static void reloadModules() {
ENABLED_MODULES.forEach(VillagerOptimizerModule::disable);
ENABLED_MODULES.clear();
for (Class<?> clazz : MODULES_PACKAGE.get(Scanners.SubTypes.of(VillagerOptimizerModule.class).asClass())) {
if (clazz.isInterface() || Modifier.isAbstract(clazz.getModifiers())) continue;
try {
VillagerOptimizerModule module = (VillagerOptimizerModule) clazz.getDeclaredConstructor().newInstance();
if (module.shouldEnable()) {
ENABLED_MODULES.add(module);
}
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
VillagerOptimizer.logger().error("Failed initialising module class '{}'.", clazz.getSimpleName(), e);
}
}
ENABLED_MODULES.forEach(VillagerOptimizerModule::enable);
}
protected void error(String message, Throwable throwable) {
VillagerOptimizer.logger().error(logFormat, message, throwable);
}
protected void error(String message) {
VillagerOptimizer.logger().error(logFormat, message);
}
protected void warn(String message) {
VillagerOptimizer.logger().warn(logFormat, message);
}
protected void info(String message) {
VillagerOptimizer.logger().info(logFormat, message);
}
protected void notRecognized(Class<?> clazz, String unrecognized) {
warn("Unable to parse " + clazz.getSimpleName() + " at '" + unrecognized + "'. Please check your configuration.");
}
}

View File

@ -1,83 +0,0 @@
package me.xginko.villageroptimizer.modules.gameplay;
import com.cryptomorin.xseries.XEntityType;
import com.cryptomorin.xseries.XMaterial;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import me.xginko.villageroptimizer.utils.LocationUtil;
import me.xginko.villageroptimizer.wrapper.WrappedVillager;
import org.bukkit.GameMode;
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.PlayerLeashEntityEvent;
import org.bukkit.event.player.PlayerInteractEntityEvent;
import org.bukkit.inventory.ItemStack;
public class EnableLeashingVillagers extends VillagerOptimizerModule implements Listener {
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,
"If set to true, only optimized villagers can be leashed.");
this.log_enabled = 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);
}
@SuppressWarnings("deprecation")
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onLeash(PlayerInteractEntityEvent event) {
if (event.getRightClicked().getType() != XEntityType.VILLAGER.get()) return;
final Player player = event.getPlayer();
final ItemStack handItem = player.getInventory().getItem(event.getHand());
if (handItem == null || handItem.getType() != XMaterial.LEAD.parseMaterial()) return;
final 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
// Call event for compatibility with other plugins, constructing non deprecated if available
PlayerLeashEntityEvent leashEvent;
try {
leashEvent = new PlayerLeashEntityEvent(villager, player, player, event.getHand());
} catch (Throwable versionIncompatible) {
leashEvent = new PlayerLeashEntityEvent(villager, player, player);
}
// If canceled by any plugin, do nothing
if (!leashEvent.callEvent()) return;
scheduling.entitySpecificScheduler(villager).run(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()));
}
}, null);
}
}

View File

@ -1,47 +0,0 @@
package me.xginko.villageroptimizer.modules.gameplay;
import com.cryptomorin.xseries.XEntityType;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import me.xginko.villageroptimizer.wrapper.WrappedVillager;
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.EntityTransformEvent;
public class FixOptimisationAfterCure extends VillagerOptimizerModule implements Listener {
public FixOptimisationAfterCure() {
super("post-cure-optimization-fix");
}
@Override
public void enable() {
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@Override
public void disable() {
HandlerList.unregisterAll(this);
}
@Override
public boolean shouldEnable() {
return true;
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onTransform(EntityTransformEvent event) {
if (
event.getTransformReason() == EntityTransformEvent.TransformReason.CURED
&& event.getTransformedEntity().getType() == XEntityType.VILLAGER.get()
) {
Villager villager = (Villager) event.getTransformedEntity();
scheduling.entitySpecificScheduler(villager).runDelayed(() -> {
WrappedVillager wVillager = wrapperCache.get(villager, WrappedVillager::new);
wVillager.setOptimizationType(wVillager.getOptimizationType());
}, null, 40L);
}
}
}

View File

@ -1,95 +0,0 @@
package me.xginko.villageroptimizer.modules.gameplay;
import com.cryptomorin.xseries.XPotion;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.config.Config;
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 net.kyori.adventure.text.TextReplacementConfig;
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.inventory.InventoryCloseEvent;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.potion.PotionEffect;
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);
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.");
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,
"Tell players to wait when a villager is leveling up.");
}
@Override
public void enable() {
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@Override
public void disable() {
HandlerList.unregisterAll(this);
}
@Override
public boolean shouldEnable() {
return true;
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onTradeScreenClose(InventoryCloseEvent event) {
if (
event.getInventory().getType() == InventoryType.MERCHANT
&& event.getInventory().getHolder() instanceof Villager
) {
final Villager villager = (Villager) event.getInventory().getHolder();
final WrappedVillager wVillager = wrapperCache.get(villager, WrappedVillager::new);
if (!wVillager.isOptimized()) return;
if (wVillager.canLevelUp(cooldown_millis)) {
if (wVillager.calculateLevel() <= villager.getVillagerLevel()) return;
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);
} 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))))
.build();
VillagerOptimizer.getLang(player.locale()).villager_leveling_up
.forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(timeLeft)));
}
}
}
}
}

View File

@ -1,85 +0,0 @@
package me.xginko.villageroptimizer.modules.gameplay;
import com.cryptomorin.xseries.XEntityType;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import me.xginko.villageroptimizer.wrapper.WrappedVillager;
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.EntityDamageEvent;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
public class PreventOptimizedDamage extends VillagerOptimizerModule implements Listener {
private final Set<EntityDamageEvent.DamageCause> damage_causes_to_cancel;
private final boolean cancel_knockback;
public PreventOptimizedDamage() {
super("gameplay.prevent-damage-to-optimized");
config.master().addComment(configPath + ".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,
"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)));
}
@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", true);
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onDamageByEntity(EntityDamageEvent event) {
if (
event.getEntityType() == XEntityType.VILLAGER.get()
&& damage_causes_to_cancel.contains(event.getCause())
&& wrapperCache.get((Villager) event.getEntity(), WrappedVillager::new).isOptimized()
) {
event.setCancelled(true);
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onKnockbackByEntity(com.destroystokyo.paper.event.entity.EntityKnockbackByEntityEvent event) {
if (
cancel_knockback
&& event.getEntityType() == XEntityType.VILLAGER.get()
&& wrapperCache.get((Villager) event.getEntity(), WrappedVillager::new).isOptimized()
) {
event.setCancelled(true);
}
}
}

View File

@ -1,76 +0,0 @@
package me.xginko.villageroptimizer.modules.gameplay;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.struct.enums.Permissions;
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;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
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 {
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,
"Sends players a message when they try to trade with an unoptimized villager.");
}
@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);
}
@EventHandler(priority = EventPriority.HIGHEST, 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;
event.setCancelled(true);
if (notify_player) {
Player player = (Player) event.getWhoClicked();
VillagerOptimizer.getLang(player.locale()).optimize_for_trading.forEach(line -> KyoriUtil.sendMessage(player, line));
}
}
@EventHandler(priority = EventPriority.HIGHEST, 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;
event.setCancelled(true);
if (notify_player) {
Player player = (Player) event.getWhoClicked();
VillagerOptimizer.getLang(player.locale()).optimize_for_trading.forEach(line -> KyoriUtil.sendMessage(player, line));
}
}
}

View File

@ -1,110 +0,0 @@
package me.xginko.villageroptimizer.modules.gameplay;
import com.cryptomorin.xseries.XEntityType;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import me.xginko.villageroptimizer.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 net.kyori.adventure.text.TextReplacementConfig;
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.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 {
private final SortedSet<Long> restockDayTimes;
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,
"Sends the player a message when the trades were restocked on a clicked villager.");
this.log_enabled = 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 true;
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onPlayerInteractEntity(PlayerInteractEntityEvent event) {
if (event.getRightClicked().getType() != XEntityType.VILLAGER.get()) return;
WrappedVillager wrapped = wrapperCache.get((Villager) event.getRightClicked(), WrappedVillager::new);
if (!wrapped.isOptimized()) return;
if (event.getPlayer().hasPermission(Permissions.Bypass.RESTOCK_COOLDOWN.get())) {
wrapped.restock();
return;
}
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 (!restocked) {
wrapped.restock();
wrapped.saveRestockTime();
restocked = true;
}
}
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()));
}
}
}

Some files were not shown because too many files have changed in this diff Show More