Compare commits
105 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
15fcd14e16 | ||
![]() |
4019afe89b | ||
![]() |
6f1077ad2d | ||
![]() |
72e2673248 | ||
![]() |
04d6cf604c | ||
![]() |
d4ba859176 | ||
![]() |
56d6baf9de | ||
![]() |
ca3c486d8b | ||
![]() |
9dd32b1207 | ||
![]() |
da300b2958 | ||
![]() |
f90d3ac3f2 | ||
![]() |
1360be9917 | ||
![]() |
42a1bdec57 | ||
![]() |
6fe1330038 | ||
![]() |
0a3cd80f9c | ||
![]() |
0ff37cbd7d | ||
![]() |
07d105e30e | ||
![]() |
aecd669638 | ||
![]() |
7969f75a3a | ||
![]() |
849d6fbaf5 | ||
![]() |
b0d1c42955 | ||
![]() |
159c03f3cd | ||
![]() |
106e1625cc | ||
![]() |
c5e274daf9 | ||
![]() |
d0af45fc21 | ||
![]() |
4a775d5e7e | ||
![]() |
98b392d528 | ||
![]() |
f652cb1d1c | ||
![]() |
b94e158465 | ||
![]() |
c05ec30330 | ||
![]() |
5b3687a062 | ||
![]() |
665e23ec58 | ||
![]() |
d5de576591 | ||
![]() |
ca563700b3 | ||
![]() |
20d426e315 | ||
![]() |
7c56dfdb17 | ||
![]() |
2cd6d0576a | ||
![]() |
65322c6caa | ||
![]() |
2fec1bcbd4 | ||
![]() |
d547628a55 | ||
![]() |
77ff0a8921 | ||
![]() |
9cc91619dc | ||
![]() |
3b4c6dc32e | ||
![]() |
0d24169283 | ||
![]() |
5d4e9e4021 | ||
![]() |
7cf9e7d2de | ||
![]() |
ef8b6c884a | ||
![]() |
8f6fe7fa07 | ||
![]() |
493a3d7fe7 | ||
![]() |
28f9f13a5b | ||
![]() |
620a0b5d48 | ||
![]() |
bc7cffd77e | ||
![]() |
37636e5332 | ||
![]() |
e01b8b0462 | ||
![]() |
5fecedf658 | ||
![]() |
b29f4afa65 | ||
![]() |
11ef3eedac | ||
![]() |
9525a2b3ba | ||
![]() |
aeb7367b0e | ||
![]() |
00daca70a7 | ||
![]() |
034a270cfc | ||
![]() |
2ab9dbeaf1 | ||
![]() |
c7ee42f150 | ||
![]() |
02b4675fa0 | ||
![]() |
9421da491b | ||
![]() |
f43f9898f1 | ||
![]() |
4582cabc31 | ||
![]() |
c7a0513589 | ||
![]() |
7f002a5c97 | ||
![]() |
59bb34685f | ||
![]() |
67d3fcf373 | ||
![]() |
a569505f1b | ||
![]() |
41d2bc4935 | ||
![]() |
2d1a9a4fec | ||
![]() |
ee866c70f2 | ||
![]() |
b1994806c2 | ||
![]() |
2f95176cf0 | ||
![]() |
09ab1e6477 | ||
![]() |
b5073d0361 | ||
![]() |
f4d6fac8c9 | ||
![]() |
0afb51b54d | ||
![]() |
6d2307ae8d | ||
![]() |
5068e59175 | ||
![]() |
005f26b2d3 | ||
![]() |
123c0d0787 | ||
![]() |
533031863f | ||
![]() |
6272383dd4 | ||
![]() |
69881d7128 | ||
![]() |
3d86639d4d | ||
![]() |
cb9e5e9553 | ||
![]() |
42a4ac9455 | ||
![]() |
1ef3a5e43c | ||
![]() |
9448161aec | ||
![]() |
93c7cb4a31 | ||
![]() |
c80f2c2eff | ||
![]() |
28fb8b86ba | ||
![]() |
aadb1a00ba | ||
![]() |
4024614f85 | ||
![]() |
b30f0856c3 | ||
![]() |
39c71155f8 | ||
![]() |
acdb97685d | ||
![]() |
6a099ddfe3 | ||
![]() |
f4a5d2b9fd | ||
![]() |
0ff288ce84 | ||
![]() |
74bb0b0e9e |
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@ -1 +1 @@
|
||||
custom: https://ko-fi.com/xginko
|
||||
ko_fi: xginko
|
||||
|
100
pom.xml
100
pom.xml
@ -4,9 +4,9 @@
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>me.xginko.VillagerOptimizer</groupId>
|
||||
<groupId>me.xginko</groupId>
|
||||
<artifactId>VillagerOptimizer</artifactId>
|
||||
<version>1.4.0</version>
|
||||
<version>1.7.0</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>VillagerOptimizer</name>
|
||||
@ -14,7 +14,7 @@
|
||||
<url>https://github.com/xGinko/VillagerOptimizer</url>
|
||||
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
<java.version>1.8</java.version>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
@ -23,7 +23,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.11.0</version>
|
||||
<version>3.12.1</version>
|
||||
<configuration>
|
||||
<source>${java.version}</source>
|
||||
<target>${java.version}</target>
|
||||
@ -41,10 +41,6 @@
|
||||
</goals>
|
||||
<configuration>
|
||||
<relocations>
|
||||
<relocation>
|
||||
<pattern>com.tcoded.folialib</pattern>
|
||||
<shadedPattern>me.xginko.villageroptimizer.libs.folialib</shadedPattern>
|
||||
</relocation>
|
||||
<relocation>
|
||||
<pattern>com.github.benmanes.caffeine</pattern>
|
||||
<shadedPattern>me.xginko.villageroptimizer.libs.caffeine</shadedPattern>
|
||||
@ -53,13 +49,38 @@
|
||||
<pattern>org.bstats</pattern>
|
||||
<shadedPattern>me.xginko.villageroptimizer.libs.bstats</shadedPattern>
|
||||
</relocation>
|
||||
<relocation>
|
||||
<pattern>net.kyori</pattern>
|
||||
<shadedPattern>me.xginko.villageroptimizer.libs.kyori</shadedPattern>
|
||||
</relocation>
|
||||
<relocation>
|
||||
<pattern>io.github.thatsmusic99.configurationmaster</pattern>
|
||||
<shadedPattern>me.xginko.villageroptimizer.libs.configmaster</shadedPattern>
|
||||
</relocation>
|
||||
<relocation>
|
||||
<pattern>org.reflections</pattern>
|
||||
<shadedPattern>me.xginko.villageroptimizer.libs.reflections</shadedPattern>
|
||||
</relocation>
|
||||
<relocation>
|
||||
<pattern>com.cryptomorin.xseries</pattern>
|
||||
<shadedPattern>me.xginko.villageroptimizer.libs.xseries</shadedPattern>
|
||||
</relocation>
|
||||
<relocation>
|
||||
<pattern>space.arim.morepaperlib</pattern>
|
||||
<shadedPattern>me.xginko.villageroptimizer.libs.morepaperlib</shadedPattern>
|
||||
</relocation>
|
||||
</relocations>
|
||||
<filters>
|
||||
<filter>
|
||||
<artifact>*:*</artifact>
|
||||
<excludes>
|
||||
<exclude>module-info.class</exclude>
|
||||
<exclude>com/cryptomorin/xseries/XBiome*</exclude>
|
||||
<exclude>com/cryptomorin/xseries/NMSExtras*</exclude>
|
||||
<exclude>com/cryptomorin/xseries/NoteBlockMusic*</exclude>
|
||||
<exclude>com/cryptomorin/xseries/SkullCacheListener*</exclude>
|
||||
<exclude>META-INF/MANIFEST.MF</exclude>
|
||||
<exclude>META-INF/LICENSE</exclude>
|
||||
<exclude>META-INF/LICENSE.txt</exclude>
|
||||
</excludes>
|
||||
</filter>
|
||||
</filters>
|
||||
@ -91,8 +112,8 @@
|
||||
<url>https://ci.pluginwiki.us/plugin/repository/everything/</url>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>folialib-repo</id>
|
||||
<url>https://nexuslite.gcnt.net/repos/other/</url>
|
||||
<id>morepaperlib-repo</id>
|
||||
<url>https://mvn-repo.arim.space/lesser-gpl3/</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
@ -103,51 +124,70 @@
|
||||
<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.15.0</version>
|
||||
<scope>compile</scope>
|
||||
<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.15.0</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.kyori</groupId>
|
||||
<artifactId>adventure-text-serializer-plain</artifactId>
|
||||
<version>4.15.0</version>
|
||||
<scope>compile</scope>
|
||||
<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 -->
|
||||
<!-- Fast Caching (Needs to be 2.9.3 for java 8 support) -->
|
||||
<dependency>
|
||||
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||
<artifactId>caffeine</artifactId>
|
||||
<version>3.1.8</version>
|
||||
<scope>compile</scope>
|
||||
<version>2.9.3</version>
|
||||
</dependency>
|
||||
<!-- Folia Support -->
|
||||
<dependency>
|
||||
<groupId>com.tcoded</groupId>
|
||||
<artifactId>FoliaLib</artifactId>
|
||||
<version>0.3.1</version>
|
||||
<scope>compile</scope>
|
||||
<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>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
@ -1,56 +0,0 @@
|
||||
package me.xginko.villageroptimizer;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Villager;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
public final class VillagerCache {
|
||||
|
||||
private final @NotNull Cache<UUID, WrappedVillager> villagerCache;
|
||||
|
||||
public VillagerCache(long expireAfterWriteSeconds) {
|
||||
this.villagerCache = Caffeine.newBuilder().expireAfterWrite(Duration.ofSeconds(expireAfterWriteSeconds)).build();
|
||||
}
|
||||
|
||||
public @NotNull ConcurrentMap<UUID, WrappedVillager> cacheMap() {
|
||||
return this.villagerCache.asMap();
|
||||
}
|
||||
|
||||
public @Nullable WrappedVillager get(@NotNull UUID uuid) {
|
||||
WrappedVillager wrappedVillager = this.villagerCache.getIfPresent(uuid);
|
||||
return wrappedVillager == null && Bukkit.getEntity(uuid) instanceof Villager villager ? this.add(villager) : wrappedVillager;
|
||||
}
|
||||
|
||||
public @NotNull WrappedVillager getOrAdd(@NotNull Villager villager) {
|
||||
WrappedVillager wrappedVillager = this.villagerCache.getIfPresent(villager.getUniqueId());
|
||||
return wrappedVillager == null ? this.add(new WrappedVillager(villager)) : this.add(wrappedVillager);
|
||||
}
|
||||
|
||||
public @NotNull WrappedVillager add(@NotNull WrappedVillager villager) {
|
||||
this.villagerCache.put(villager.villager().getUniqueId(), villager);
|
||||
return villager;
|
||||
}
|
||||
|
||||
public @NotNull WrappedVillager add(@NotNull Villager villager) {
|
||||
return this.add(new WrappedVillager(villager));
|
||||
}
|
||||
|
||||
public boolean contains(@NotNull UUID uuid) {
|
||||
return this.villagerCache.getIfPresent(uuid) != null;
|
||||
}
|
||||
|
||||
public boolean contains(@NotNull WrappedVillager villager) {
|
||||
return this.contains(villager.villager().getUniqueId());
|
||||
}
|
||||
|
||||
public boolean contains(@NotNull Villager villager) {
|
||||
return this.contains(villager.getUniqueId());
|
||||
}
|
||||
}
|
@ -1,109 +1,199 @@
|
||||
package me.xginko.villageroptimizer;
|
||||
|
||||
import com.tcoded.folialib.FoliaLib;
|
||||
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.Style;
|
||||
import net.kyori.adventure.text.format.TextColor;
|
||||
import net.kyori.adventure.text.format.TextDecoration;
|
||||
import net.kyori.adventure.text.logger.slf4j.ComponentLogger;
|
||||
import org.apache.logging.log4j.Level;
|
||||
import org.apache.logging.log4j.core.config.Configurator;
|
||||
import org.bstats.bukkit.Metrics;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.entity.Villager;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import space.arim.morepaperlib.MorePaperLib;
|
||||
import space.arim.morepaperlib.commands.CommandRegistration;
|
||||
import space.arim.morepaperlib.scheduling.GracefulScheduling;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
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.Set;
|
||||
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 {
|
||||
public static final Style plugin_style = Style.style(TextColor.color(102,255,230), TextDecoration.BOLD);
|
||||
|
||||
private static VillagerOptimizer instance;
|
||||
private static VillagerCache villagerCache;
|
||||
private static FoliaLib foliaLib;
|
||||
private static HashMap<String, LanguageCache> languageCacheMap;
|
||||
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;
|
||||
logger = ComponentLogger.logger(this.getName());
|
||||
foliaLib = new FoliaLib(this);
|
||||
MorePaperLib morePaperLib = new MorePaperLib(this);
|
||||
commandRegistration = morePaperLib.commandRegistration();
|
||||
scheduling = morePaperLib.scheduling();
|
||||
audiences = BukkitAudiences.create(this);
|
||||
logger = ComponentLogger.logger(getLogger().getName());
|
||||
bStats = new Metrics(this, 19954);
|
||||
|
||||
logger.info(Component.text("╭────────────────────────────────────────────────────────────╮").style(plugin_style));
|
||||
logger.info(Component.text("│ │").style(plugin_style));
|
||||
logger.info(Component.text("│ │").style(plugin_style));
|
||||
logger.info(Component.text("│ _ __ _ __ __ │").style(plugin_style));
|
||||
logger.info(Component.text("│ | | / /(_)/ // /___ _ ___ _ ___ ____ │").style(plugin_style));
|
||||
logger.info(Component.text("│ | |/ // // // // _ `// _ `// -_)/ __/ │").style(plugin_style));
|
||||
logger.info(Component.text("│ |___//_//_//_/ \\_,_/ \\_, / \\__//_/ │").style(plugin_style));
|
||||
logger.info(Component.text("│ ____ __ _ /___/_ │").style(plugin_style));
|
||||
logger.info(Component.text("│ / __ \\ ___ / /_ (_)__ _ (_)___ ___ ____ │").style(plugin_style));
|
||||
logger.info(Component.text("│ / /_/ // _ \\/ __// // ' \\ / //_ // -_)/ __/ │").style(plugin_style));
|
||||
logger.info(Component.text("│ \\____// .__/\\__//_//_/_/_//_/ /__/\\__//_/ │").style(plugin_style));
|
||||
logger.info(Component.text("│ /_/ by xGinko │").style(plugin_style));
|
||||
logger.info(Component.text("│ │").style(plugin_style));
|
||||
logger.info(Component.text("│ │").style(plugin_style));
|
||||
if (getServer().getPluginManager().getPlugin("AntiVillagerLag") != null) {
|
||||
logger.warn("While VillagerOptimizer can read data previously created by AVL, running");
|
||||
logger.warn("both plugins at the same time is unsafe and definitely will cause issues.");
|
||||
logger.warn("To protect your game from corruption, VillagerOptimizer will now disable!");
|
||||
logger.warn("Please decide for one of the plugins!");
|
||||
getServer().getPluginManager().disablePlugin(this);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
getDataFolder().mkdirs();
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to create plugin directory! Cannot enable!", e);
|
||||
getServer().getPluginManager().disablePlugin(this);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(Component.text("╭────────────────────────────────────────────────────────────╮").style(Util.PL_STYLE));
|
||||
logger.info(Component.text("│ │").style(Util.PL_STYLE));
|
||||
logger.info(Component.text("│ │").style(Util.PL_STYLE));
|
||||
logger.info(Component.text("│ _ __ _ __ __ │").style(Util.PL_STYLE));
|
||||
logger.info(Component.text("│ | | / /(_)/ // /___ _ ___ _ ___ ____ │").style(Util.PL_STYLE));
|
||||
logger.info(Component.text("│ | |/ // // // // _ `// _ `// -_)/ __/ │").style(Util.PL_STYLE));
|
||||
logger.info(Component.text("│ |___//_//_//_/ \\_,_/ \\_, / \\__//_/ │").style(Util.PL_STYLE));
|
||||
logger.info(Component.text("│ ____ __ _ /___/_ │").style(Util.PL_STYLE));
|
||||
logger.info(Component.text("│ / __ \\ ___ / /_ (_)__ _ (_)___ ___ ____ │").style(Util.PL_STYLE));
|
||||
logger.info(Component.text("│ / /_/ // _ \\/ __// // ' \\ / //_ // -_)/ __/ │").style(Util.PL_STYLE));
|
||||
logger.info(Component.text("│ \\____// .__/\\__//_//_/_/_//_/ /__/\\__//_/ │").style(Util.PL_STYLE));
|
||||
logger.info(Component.text("│ /_/ by xGinko │").style(Util.PL_STYLE));
|
||||
logger.info(Component.text("│ │").style(Util.PL_STYLE));
|
||||
logger.info(Component.text("│ │").style(Util.PL_STYLE));
|
||||
logger.info(Component.text("│ ")
|
||||
.style(plugin_style).append(Component.text("https://github.com/xGinko/VillagerOptimizer")
|
||||
.color(NamedTextColor.GRAY)).append(Component.text(" │").style(plugin_style)));
|
||||
logger.info(Component.text("│ │").style(plugin_style));
|
||||
logger.info(Component.text("│ │").style(plugin_style));
|
||||
logger.info(Component.text("│ ")
|
||||
.style(plugin_style).append(Component.text(" ➤ Loading Translations...").style(plugin_style))
|
||||
.append(Component.text(" │").style(plugin_style)));
|
||||
reloadLang(true);
|
||||
logger.info(Component.text("│ ")
|
||||
.style(plugin_style).append(Component.text(" ➤ Loading Config...").style(plugin_style))
|
||||
.append(Component.text(" │").style(plugin_style)));
|
||||
reloadConfiguration();
|
||||
logger.info(Component.text("│ ")
|
||||
.style(plugin_style).append(Component.text(" ✓ Done.").color(NamedTextColor.WHITE).decorate(TextDecoration.BOLD))
|
||||
.append(Component.text(" │").style(plugin_style)));
|
||||
logger.info(Component.text("│ │").style(plugin_style));
|
||||
logger.info(Component.text("│ │").style(plugin_style));
|
||||
logger.info(Component.text("╰────────────────────────────────────────────────────────────╯").style(plugin_style));
|
||||
.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();
|
||||
|
||||
new Metrics(this, 19954);
|
||||
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));
|
||||
}
|
||||
|
||||
public static VillagerOptimizer getInstance() {
|
||||
@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 Config getConfiguration() {
|
||||
|
||||
public static @NotNull GracefulScheduling scheduling() {
|
||||
return scheduling;
|
||||
}
|
||||
|
||||
public static @NotNull CommandRegistration commandRegistration() {
|
||||
return commandRegistration;
|
||||
}
|
||||
|
||||
public static @NotNull Cache<Villager, WrappedVillager> wrappers() {
|
||||
return wrapperCache;
|
||||
}
|
||||
|
||||
public static @NotNull Config config() {
|
||||
return config;
|
||||
}
|
||||
public static VillagerCache getCache() {
|
||||
return villagerCache;
|
||||
}
|
||||
public static FoliaLib getFoliaLib() {
|
||||
return foliaLib;
|
||||
}
|
||||
public static ComponentLogger getLog() {
|
||||
|
||||
public static @NotNull ComponentLogger logger() {
|
||||
return logger;
|
||||
}
|
||||
public static LanguageCache getLang(Locale locale) {
|
||||
|
||||
public static @NotNull BukkitAudiences audiences() {
|
||||
return audiences;
|
||||
}
|
||||
|
||||
public static @NotNull 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 @NotNull LanguageCache getLang(CommandSender commandSender) {
|
||||
return commandSender instanceof Player ? getLang(((Player) commandSender).locale()) : getLang(config.default_lang);
|
||||
}
|
||||
public static LanguageCache getLang(String 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()));
|
||||
}
|
||||
@ -116,64 +206,54 @@ public final class VillagerOptimizer extends JavaPlugin {
|
||||
private void reloadConfiguration() {
|
||||
try {
|
||||
config = new Config();
|
||||
villagerCache = new VillagerCache(config.cache_keep_time_seconds);
|
||||
if (wrapperCache != null) wrapperCache.cleanUp();
|
||||
wrapperCache = Caffeine.newBuilder().expireAfterWrite(config.cache_keep_time).build();
|
||||
VillagerOptimizerCommand.reloadCommands();
|
||||
VillagerOptimizerModule.reloadModules();
|
||||
config.saveConfig();
|
||||
} catch (Exception e) {
|
||||
logger.error("Error loading config! - " + e.getLocalizedMessage());
|
||||
e.printStackTrace();
|
||||
} catch (Exception exception) {
|
||||
logger.error("Error during config reload!", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private void reloadLang(boolean startup) {
|
||||
languageCacheMap = new HashMap<>();
|
||||
private void reloadLang(boolean logFancy) {
|
||||
try {
|
||||
File langDirectory = new File(getDataFolder() + File.separator + "lang");
|
||||
Files.createDirectories(langDirectory.toPath());
|
||||
for (String fileName : getDefaultLanguageFiles()) {
|
||||
final String localeString = fileName.substring(fileName.lastIndexOf(File.separator) + 1, fileName.lastIndexOf('.'));
|
||||
if (startup) logger.info(
|
||||
Component.text("│ ").style(plugin_style)
|
||||
.append(Component.text(" "+localeString).color(NamedTextColor.WHITE).decorate(TextDecoration.BOLD))
|
||||
.append(Component.text(" │").style(plugin_style)));
|
||||
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));
|
||||
}
|
||||
final Pattern langPattern = Pattern.compile("([a-z]{1,3}_[a-z]{1,3})(\\.yml)", Pattern.CASE_INSENSITIVE);
|
||||
for (File langFile : langDirectory.listFiles()) {
|
||||
final Matcher langMatcher = langPattern.matcher(langFile.getName());
|
||||
if (langMatcher.find()) {
|
||||
String localeString = langMatcher.group(1).toLowerCase();
|
||||
if (!languageCacheMap.containsKey(localeString)) { // make sure it wasn't a default file that we already loaded
|
||||
if (startup) logger.info(
|
||||
Component.text("│ ").style(plugin_style)
|
||||
.append(Component.text(" "+localeString).color(NamedTextColor.WHITE).decorate(TextDecoration.BOLD))
|
||||
.append(Component.text(" │").style(plugin_style)));
|
||||
else logger.info(String.format("Found language file for %s", localeString));
|
||||
languageCacheMap.put(localeString, new LanguageCache(localeString));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
if (startup) logger.error(
|
||||
Component.text("│ ").style(plugin_style)
|
||||
.append(Component.text("LANG ERROR").color(NamedTextColor.RED).decorate(TextDecoration.BOLD))
|
||||
.append(Component.text(" │").style(plugin_style)));
|
||||
else logger.error("Error loading language files! Language files will not reload to avoid errors, make sure to correct this before restarting the server!");
|
||||
e.printStackTrace();
|
||||
} 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 Set<String> getDefaultLanguageFiles() {
|
||||
try (final JarFile pluginJarFile = new JarFile(this.getFile())) {
|
||||
return pluginJarFile.stream()
|
||||
.map(ZipEntry::getName)
|
||||
.filter(name -> name.startsWith("lang" + File.separator) && name.endsWith(".yml"))
|
||||
.collect(Collectors.toSet());
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed getting default lang files! - "+e.getLocalizedMessage());
|
||||
return Collections.emptySet();
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,350 +0,0 @@
|
||||
package me.xginko.villageroptimizer;
|
||||
|
||||
import me.xginko.villageroptimizer.enums.Keyring;
|
||||
import me.xginko.villageroptimizer.enums.OptimizationType;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.Villager;
|
||||
import org.bukkit.entity.memory.MemoryKey;
|
||||
import org.bukkit.inventory.MerchantRecipe;
|
||||
import org.bukkit.persistence.PersistentDataContainer;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@SuppressWarnings("ALL")
|
||||
public final class WrappedVillager {
|
||||
|
||||
private final @NotNull Villager villager;
|
||||
private final @NotNull PersistentDataContainer dataContainer;
|
||||
private @Nullable CachedJobSite cachedJobSite;
|
||||
private final boolean parseOther;
|
||||
|
||||
WrappedVillager(@NotNull Villager villager) {
|
||||
this.villager = villager;
|
||||
this.dataContainer = villager.getPersistentDataContainer();
|
||||
this.parseOther = VillagerOptimizer.getConfiguration().support_other_plugins;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The villager inside the wrapper.
|
||||
*/
|
||||
public @NotNull Villager villager() {
|
||||
return villager;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The data container inside the wrapper.
|
||||
*/
|
||||
public @NotNull PersistentDataContainer dataContainer() {
|
||||
return dataContainer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if the villager is optimized by either this plugin or a supported alternative, otherwise false.
|
||||
*/
|
||||
public boolean isOptimized() {
|
||||
if (!parseOther) {
|
||||
return isOptimized(Keyring.Spaces.VillagerOptimizer);
|
||||
}
|
||||
for (Keyring.Spaces pluginNamespaces : Keyring.Spaces.values()) {
|
||||
if (isOptimized(pluginNamespaces)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if the villager is optimized by the supported plugin, otherwise false.
|
||||
*/
|
||||
public boolean isOptimized(Keyring.Spaces namespaces) {
|
||||
return switch (namespaces) {
|
||||
case VillagerOptimizer -> dataContainer.has(Keyring.VillagerOptimizer.OPTIMIZATION_TYPE.getKey(), PersistentDataType.STRING);
|
||||
case AntiVillagerLag -> dataContainer.has(Keyring.AntiVillagerLag.OPTIMIZED_ANY.getKey(), PersistentDataType.STRING)
|
||||
|| dataContainer.has(Keyring.AntiVillagerLag.OPTIMIZED_WORKSTATION.getKey(), PersistentDataType.STRING)
|
||||
|| dataContainer.has(Keyring.AntiVillagerLag.OPTIMIZED_BLOCK.getKey(), PersistentDataType.STRING);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param cooldown_millis The configured cooldown in millis until the next optimization is allowed to occur.
|
||||
* @return True if villager can be optimized again, otherwise false.
|
||||
*/
|
||||
public boolean canOptimize(final long cooldown_millis) {
|
||||
if (parseOther) {
|
||||
if (
|
||||
dataContainer.has(Keyring.AntiVillagerLag.NEXT_OPTIMIZATION_SYSTIME_SECONDS.getKey(), PersistentDataType.LONG)
|
||||
&& System.currentTimeMillis() <= 1000 * dataContainer.get(Keyring.AntiVillagerLag.NEXT_OPTIMIZATION_SYSTIME_SECONDS.getKey(), PersistentDataType.LONG)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return System.currentTimeMillis() > getLastOptimize() + cooldown_millis;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param type OptimizationType the villager should be set to.
|
||||
*/
|
||||
public void setOptimizationType(final OptimizationType type) {
|
||||
VillagerOptimizer.getFoliaLib().getImpl().runAtEntityTimer(villager, setOptimization -> {
|
||||
// Keep repeating task until villager is no longer trading with a player
|
||||
if (villager.isTrading()) return;
|
||||
|
||||
switch (type) {
|
||||
case NAMETAG, COMMAND, BLOCK, WORKSTATION -> {
|
||||
dataContainer.set(Keyring.VillagerOptimizer.OPTIMIZATION_TYPE.getKey(), PersistentDataType.STRING, type.name());
|
||||
villager.setAware(false);
|
||||
}
|
||||
case NONE -> {
|
||||
if (isOptimized(Keyring.Spaces.VillagerOptimizer)) {
|
||||
dataContainer.remove(Keyring.VillagerOptimizer.OPTIMIZATION_TYPE.getKey());
|
||||
}
|
||||
if (parseOther) {
|
||||
if (dataContainer.has(Keyring.AntiVillagerLag.OPTIMIZED_ANY.getKey(), PersistentDataType.STRING))
|
||||
dataContainer.remove(Keyring.AntiVillagerLag.OPTIMIZED_ANY.getKey());
|
||||
if (dataContainer.has(Keyring.AntiVillagerLag.OPTIMIZED_WORKSTATION.getKey(), PersistentDataType.STRING))
|
||||
dataContainer.remove(Keyring.AntiVillagerLag.OPTIMIZED_WORKSTATION.getKey());
|
||||
if (dataContainer.has(Keyring.AntiVillagerLag.OPTIMIZED_BLOCK.getKey(), PersistentDataType.STRING))
|
||||
dataContainer.remove(Keyring.AntiVillagerLag.OPTIMIZED_BLOCK.getKey());
|
||||
}
|
||||
villager.setAware(true);
|
||||
villager.setAI(true);
|
||||
}
|
||||
}
|
||||
|
||||
// End repeating task once logic is finished
|
||||
setOptimization.cancel();
|
||||
}, 0L, 1L, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The current OptimizationType of the villager.
|
||||
*/
|
||||
public @NotNull OptimizationType getOptimizationType() {
|
||||
if (!parseOther) {
|
||||
return getOptimizationType(Keyring.Spaces.VillagerOptimizer);
|
||||
}
|
||||
OptimizationType optimizationType = getOptimizationType(Keyring.Spaces.VillagerOptimizer);
|
||||
if (optimizationType != OptimizationType.NONE) {
|
||||
return optimizationType;
|
||||
}
|
||||
return getOptimizationType(Keyring.Spaces.AntiVillagerLag);
|
||||
}
|
||||
|
||||
public @NotNull OptimizationType getOptimizationType(Keyring.Spaces namespaces) {
|
||||
return switch (namespaces) {
|
||||
case VillagerOptimizer -> {
|
||||
if (isOptimized(Keyring.Spaces.VillagerOptimizer)) {
|
||||
yield OptimizationType.valueOf(dataContainer.get(Keyring.VillagerOptimizer.OPTIMIZATION_TYPE.getKey(), PersistentDataType.STRING));
|
||||
}
|
||||
yield OptimizationType.NONE;
|
||||
}
|
||||
case AntiVillagerLag -> {
|
||||
if (dataContainer.has(Keyring.AntiVillagerLag.OPTIMIZED_BLOCK.getKey(), PersistentDataType.STRING)) {
|
||||
yield OptimizationType.BLOCK;
|
||||
}
|
||||
if (dataContainer.has(Keyring.AntiVillagerLag.OPTIMIZED_WORKSTATION.getKey(), PersistentDataType.STRING)) {
|
||||
yield OptimizationType.WORKSTATION;
|
||||
}
|
||||
if (dataContainer.has(Keyring.AntiVillagerLag.OPTIMIZED_ANY.getKey(), PersistentDataType.STRING)) {
|
||||
yield OptimizationType.COMMAND; // Best we can do
|
||||
}
|
||||
yield OptimizationType.NONE;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the system time in millis when the villager was last optimized.
|
||||
*/
|
||||
public void saveOptimizeTime() {
|
||||
dataContainer.set(Keyring.VillagerOptimizer.LAST_OPTIMIZE.getKey(), PersistentDataType.LONG, System.currentTimeMillis());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The system time in millis when the villager was last optimized, 0L if the villager was never optimized.
|
||||
*/
|
||||
public long getLastOptimize() {
|
||||
if (dataContainer.has(Keyring.VillagerOptimizer.LAST_OPTIMIZE.getKey(), PersistentDataType.LONG)) {
|
||||
return dataContainer.get(Keyring.VillagerOptimizer.LAST_OPTIMIZE.getKey(), PersistentDataType.LONG);
|
||||
}
|
||||
return 0L;
|
||||
}
|
||||
|
||||
/**
|
||||
* Here for convenience so the remaining millis since the last stored optimize time
|
||||
* can be easily calculated.
|
||||
* This enables new configured cooldowns to instantly apply instead of them being persistent.
|
||||
*
|
||||
* @param cooldown_millis The configured cooldown in milliseconds you want to check against.
|
||||
* @return The time left in millis until the villager can be optimized again.
|
||||
*/
|
||||
public long getOptimizeCooldownMillis(final long cooldown_millis) {
|
||||
long remainingMillis = 0L;
|
||||
|
||||
if (parseOther) {
|
||||
if (dataContainer.has(Keyring.AntiVillagerLag.NEXT_OPTIMIZATION_SYSTIME_SECONDS.getKey(), PersistentDataType.LONG)) {
|
||||
remainingMillis = System.currentTimeMillis() - dataContainer.get(Keyring.AntiVillagerLag.NEXT_OPTIMIZATION_SYSTIME_SECONDS.getKey(), PersistentDataType.LONG);
|
||||
}
|
||||
}
|
||||
|
||||
if (remainingMillis > 0) return remainingMillis;
|
||||
|
||||
if (dataContainer.has(Keyring.VillagerOptimizer.LAST_OPTIMIZE.getKey(), PersistentDataType.LONG)) {
|
||||
return System.currentTimeMillis() - (dataContainer.get(Keyring.VillagerOptimizer.LAST_OPTIMIZE.getKey(), PersistentDataType.LONG) + cooldown_millis);
|
||||
}
|
||||
|
||||
return cooldown_millis;
|
||||
}
|
||||
|
||||
/**
|
||||
* Here for convenience so the remaining millis since the last stored restock time
|
||||
* can be easily calculated.
|
||||
*
|
||||
* @param cooldown_millis The configured cooldown in milliseconds you want to check against.
|
||||
* @return True if the villager has been loaded long enough.
|
||||
*/
|
||||
public boolean canRestock(final long cooldown_millis) {
|
||||
return getLastRestock() + cooldown_millis <= villager.getWorld().getFullTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* Restock all trading recipes.
|
||||
*/
|
||||
public void restock() {
|
||||
for (MerchantRecipe recipe : villager.getRecipes()) {
|
||||
recipe.setUses(0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the time of the in-game world when the entity was last restocked.
|
||||
*/
|
||||
public void saveRestockTime() {
|
||||
dataContainer.set(Keyring.VillagerOptimizer.LAST_RESTOCK.getKey(), PersistentDataType.LONG, villager.getWorld().getFullTime());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The time of the in-game world when the entity was last restocked.
|
||||
*/
|
||||
public long getLastRestock() {
|
||||
long lastRestock = 0L;
|
||||
if (dataContainer.has(Keyring.VillagerOptimizer.LAST_RESTOCK.getKey(), PersistentDataType.LONG)) {
|
||||
lastRestock = dataContainer.get(Keyring.VillagerOptimizer.LAST_RESTOCK.getKey(), PersistentDataType.LONG);
|
||||
}
|
||||
if (parseOther) {
|
||||
if (dataContainer.has(Keyring.AntiVillagerLag.LAST_RESTOCK_WORLDFULLTIME.getKey(), PersistentDataType.LONG)) {
|
||||
long lastAVLRestock = dataContainer.get(Keyring.AntiVillagerLag.LAST_RESTOCK_WORLDFULLTIME.getKey(), PersistentDataType.LONG);
|
||||
if (lastRestock < lastAVLRestock) {
|
||||
lastRestock = lastAVLRestock;
|
||||
}
|
||||
}
|
||||
}
|
||||
return lastRestock;
|
||||
}
|
||||
|
||||
public long getRestockCooldownMillis(final long cooldown_millis) {
|
||||
if (dataContainer.has(Keyring.VillagerOptimizer.LAST_RESTOCK.getKey(), PersistentDataType.LONG))
|
||||
return villager.getWorld().getFullTime() - (dataContainer.get(Keyring.VillagerOptimizer.LAST_RESTOCK.getKey(), PersistentDataType.LONG) + cooldown_millis);
|
||||
return cooldown_millis;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The level between 1-5 calculated from the villagers experience.
|
||||
*/
|
||||
public int calculateLevel() {
|
||||
// https://minecraft.fandom.com/wiki/Trading#Mechanics
|
||||
int vilEXP = villager.getVillagerExperience();
|
||||
if (vilEXP >= 250) return 5;
|
||||
if (vilEXP >= 150) return 4;
|
||||
if (vilEXP >= 70) return 3;
|
||||
if (vilEXP >= 10) return 2;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param cooldown_millis The configured cooldown in milliseconds you want to check against.
|
||||
* @return Whether the villager can be leveled up or not with the checked milliseconds
|
||||
*/
|
||||
public boolean canLevelUp(final long cooldown_millis) {
|
||||
if (villager.getWorld().getFullTime() < getLastLevelUpTime() + cooldown_millis) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parseOther) {
|
||||
return !dataContainer.has(Keyring.AntiVillagerLag.NEXT_LEVELUP_SYSTIME_SECONDS.getKey(), PersistentDataType.LONG)
|
||||
|| System.currentTimeMillis() > dataContainer.get(Keyring.AntiVillagerLag.NEXT_LEVELUP_SYSTIME_SECONDS.getKey(), PersistentDataType.LONG) * 1000;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the time of the in-game world when the entity was last leveled up.
|
||||
*/
|
||||
public void saveLastLevelUp() {
|
||||
dataContainer.set(Keyring.VillagerOptimizer.LAST_LEVELUP.getKey(), PersistentDataType.LONG, villager.getWorld().getFullTime());
|
||||
}
|
||||
|
||||
/**
|
||||
* Here for convenience so the remaining millis since the last stored level-up time
|
||||
* can be easily calculated.
|
||||
*
|
||||
* @return The time of the in-game world when the entity was last leveled up.
|
||||
*/
|
||||
public long getLastLevelUpTime() {
|
||||
if (dataContainer.has(Keyring.VillagerOptimizer.LAST_LEVELUP.getKey(), PersistentDataType.LONG))
|
||||
return dataContainer.get(Keyring.VillagerOptimizer.LAST_LEVELUP.getKey(), PersistentDataType.LONG);
|
||||
return 0L;
|
||||
}
|
||||
|
||||
public long getLevelCooldownMillis(final long cooldown_millis) {
|
||||
if (dataContainer.has(Keyring.VillagerOptimizer.LAST_LEVELUP.getKey(), PersistentDataType.LONG))
|
||||
return villager.getWorld().getFullTime() - (dataContainer.get(Keyring.VillagerOptimizer.LAST_LEVELUP.getKey(), PersistentDataType.LONG) + cooldown_millis);
|
||||
return cooldown_millis;
|
||||
}
|
||||
|
||||
public void memorizeName(final Component customName) {
|
||||
dataContainer.set(Keyring.VillagerOptimizer.LAST_OPTIMIZE_NAME.getKey(), PersistentDataType.STRING, MiniMessage.miniMessage().serialize(customName));
|
||||
}
|
||||
|
||||
public @Nullable Component getMemorizedName() {
|
||||
if (dataContainer.has(Keyring.VillagerOptimizer.LAST_OPTIMIZE_NAME.getKey(), PersistentDataType.STRING))
|
||||
return MiniMessage.miniMessage().deserialize(dataContainer.get(Keyring.VillagerOptimizer.LAST_OPTIMIZE_NAME.getKey(), PersistentDataType.STRING));
|
||||
return null;
|
||||
}
|
||||
|
||||
public void forgetName() {
|
||||
dataContainer.remove(Keyring.VillagerOptimizer.LAST_OPTIMIZE_NAME.getKey());
|
||||
}
|
||||
|
||||
private static class CachedJobSite {
|
||||
private @Nullable Location jobSite;
|
||||
private long lastRefresh;
|
||||
private CachedJobSite(Villager villager) {
|
||||
this.jobSite = villager.getMemory(MemoryKey.JOB_SITE);
|
||||
this.lastRefresh = System.currentTimeMillis();
|
||||
}
|
||||
private @Nullable Location getJobSite(Villager villager) {
|
||||
final long now = System.currentTimeMillis();
|
||||
if (now - lastRefresh > 1000L) {
|
||||
this.jobSite = villager.getMemory(MemoryKey.JOB_SITE);
|
||||
this.lastRefresh = now;
|
||||
}
|
||||
return jobSite;
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable Location getJobSite() {
|
||||
if (cachedJobSite == null)
|
||||
cachedJobSite = new CachedJobSite(villager);
|
||||
return cachedJobSite.getJobSite(villager);
|
||||
}
|
||||
|
||||
public boolean canLooseProfession() {
|
||||
// A villager with a level of 1 and no trading experience is liable to lose its profession.
|
||||
return villager.getVillagerLevel() <= 1 && villager.getVillagerExperience() <= 0;
|
||||
}
|
||||
}
|
@ -1,11 +1,36 @@
|
||||
package me.xginko.villageroptimizer.commands;
|
||||
|
||||
import net.kyori.adventure.text.TextComponent;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.TabCompleter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public abstract class SubCommand {
|
||||
public abstract String getLabel();
|
||||
public abstract TextComponent getDescription();
|
||||
public abstract TextComponent getSyntax();
|
||||
public abstract void perform(CommandSender sender, String[] args);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -1,36 +1,62 @@
|
||||
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 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.CommandMap;
|
||||
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.util.Collections;
|
||||
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 interface VillagerOptimizerCommand extends CommandExecutor, TabCompleter {
|
||||
public abstract class VillagerOptimizerCommand implements Enableable, Disableable, CommandExecutor, TabCompleter {
|
||||
|
||||
String label();
|
||||
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());
|
||||
|
||||
List<String> NO_TABCOMPLETES = Collections.emptyList();
|
||||
List<String> RADIUS_TABCOMPLETES = List.of("5", "10", "25", "50");
|
||||
public final PluginCommand pluginCommand;
|
||||
|
||||
HashSet<VillagerOptimizerCommand> commands = new HashSet<>();
|
||||
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.");
|
||||
}
|
||||
|
||||
static void reloadCommands() {
|
||||
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
|
||||
CommandMap commandMap = plugin.getServer().getCommandMap();
|
||||
commands.forEach(command -> plugin.getCommand(command.label()).unregister(commandMap));
|
||||
commands.clear();
|
||||
public static void reloadCommands() {
|
||||
COMMANDS.forEach(VillagerOptimizerCommand::disable);
|
||||
COMMANDS.clear();
|
||||
|
||||
commands.add(new VillagerOptimizerCmd());
|
||||
commands.add(new OptVillagersRadius());
|
||||
commands.add(new UnOptVillagersRadius());
|
||||
for (Class<?> clazz : COMMANDS_PACKAGE.get(Scanners.SubTypes.of(VillagerOptimizerCommand.class).asClass())) {
|
||||
if (clazz.isInterface() || Modifier.isAbstract(clazz.getModifiers())) continue;
|
||||
|
||||
commands.forEach(command -> plugin.getCommand(command.label()).setExecutor(command));
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,13 @@
|
||||
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.Bypass;
|
||||
import me.xginko.villageroptimizer.enums.permissions.Commands;
|
||||
import me.xginko.villageroptimizer.struct.enums.OptimizationType;
|
||||
import me.xginko.villageroptimizer.struct.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;
|
||||
@ -22,46 +21,50 @@ 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 implements VillagerOptimizerCommand {
|
||||
public class OptVillagersRadius extends VillagerOptimizerCommand {
|
||||
|
||||
private final long cooldown;
|
||||
private final int max_radius;
|
||||
|
||||
public OptVillagersRadius() {
|
||||
Config config = VillagerOptimizer.getConfiguration();
|
||||
super("optimizevillagers");
|
||||
Config config = VillagerOptimizer.config();
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String label() {
|
||||
return "optimizevillagers";
|
||||
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();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
|
||||
return args.length == 1 ? RADIUS_TABCOMPLETES : NO_TABCOMPLETES;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
|
||||
if (!sender.hasPermission(Commands.OPTIMIZE_RADIUS.get())) {
|
||||
sender.sendMessage(VillagerOptimizer.getLang(sender).no_permission);
|
||||
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;
|
||||
}
|
||||
|
||||
if (!(sender instanceof Player player)) {
|
||||
sender.sendMessage(Component.text("This command can only be executed by a player.")
|
||||
if (!(sender instanceof Player)) {
|
||||
KyoriUtil.sendMessage(sender, Component.text("This command can only be executed by a player.")
|
||||
.color(NamedTextColor.RED).decorate(TextDecoration.BOLD));
|
||||
return true;
|
||||
}
|
||||
|
||||
Player player = (Player) sender;
|
||||
|
||||
if (args.length != 1) {
|
||||
VillagerOptimizer.getLang(player.locale()).command_specify_radius.forEach(player::sendMessage);
|
||||
VillagerOptimizer.getLang(player.locale()).command_specify_radius
|
||||
.forEach(line -> KyoriUtil.sendMessage(sender, line));
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -71,7 +74,8 @@ public class OptVillagersRadius implements VillagerOptimizerCommand {
|
||||
final int safeRadius = (int) Math.sqrt(specifiedRadius * specifiedRadius);
|
||||
|
||||
if (safeRadius == 0) {
|
||||
VillagerOptimizer.getLang(player.locale()).command_radius_invalid.forEach(player::sendMessage);
|
||||
VillagerOptimizer.getLang(player.locale()).command_radius_invalid
|
||||
.forEach(line -> KyoriUtil.sendMessage(sender, line));
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -80,14 +84,14 @@ public class OptVillagersRadius implements VillagerOptimizerCommand {
|
||||
.matchLiteral("%distance%")
|
||||
.replacement(Integer.toString(max_radius))
|
||||
.build();
|
||||
VillagerOptimizer.getLang(player.locale()).command_radius_limit_exceed.forEach(line -> player.sendMessage(line.replaceText(limit)));
|
||||
VillagerOptimizer.getLang(player.locale()).command_radius_limit_exceed
|
||||
.forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(limit)));
|
||||
return true;
|
||||
}
|
||||
|
||||
VillagerCache villagerCache = VillagerOptimizer.getCache();
|
||||
int successCount = 0;
|
||||
int failCount = 0;
|
||||
final boolean player_has_cooldown_bypass = player.hasPermission(Bypass.COMMAND_COOLDOWN.get());
|
||||
final boolean player_has_cooldown_bypass = player.hasPermission(Permissions.Bypass.COMMAND_COOLDOWN.get());
|
||||
|
||||
for (Entity entity : player.getNearbyEntities(safeRadius, safeRadius, safeRadius)) {
|
||||
if (!entity.getType().equals(EntityType.VILLAGER)) continue;
|
||||
@ -95,7 +99,7 @@ public class OptVillagersRadius implements VillagerOptimizerCommand {
|
||||
Villager.Profession profession = villager.getProfession();
|
||||
if (profession.equals(Villager.Profession.NITWIT) || profession.equals(Villager.Profession.NONE)) continue;
|
||||
|
||||
WrappedVillager wVillager = villagerCache.getOrAdd(villager);
|
||||
WrappedVillager wVillager = VillagerOptimizer.wrappers().get(villager, WrappedVillager::new);
|
||||
|
||||
if (player_has_cooldown_bypass || wVillager.canOptimize(cooldown)) {
|
||||
VillagerOptimizeEvent optimizeEvent = new VillagerOptimizeEvent(wVillager, OptimizationType.COMMAND, player);
|
||||
@ -114,7 +118,8 @@ public class OptVillagersRadius implements VillagerOptimizerCommand {
|
||||
.matchLiteral("%radius%")
|
||||
.replacement(Integer.toString(safeRadius))
|
||||
.build();
|
||||
VillagerOptimizer.getLang(player.locale()).command_no_villagers_nearby.forEach(line -> player.sendMessage(line.replaceText(radius)));
|
||||
VillagerOptimizer.getLang(player.locale()).command_no_villagers_nearby
|
||||
.forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(radius)));
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -127,20 +132,20 @@ public class OptVillagersRadius implements VillagerOptimizerCommand {
|
||||
.matchLiteral("%radius%")
|
||||
.replacement(Integer.toString(safeRadius))
|
||||
.build();
|
||||
VillagerOptimizer.getLang(player.locale()).command_optimize_success.forEach(line -> player.sendMessage(line
|
||||
.replaceText(success_amount)
|
||||
.replaceText(radius)
|
||||
));
|
||||
VillagerOptimizer.getLang(player.locale()).command_optimize_success
|
||||
.forEach(line -> KyoriUtil.sendMessage(player, 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)));
|
||||
VillagerOptimizer.getLang(player.locale()).command_optimize_fail
|
||||
.forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(alreadyOptimized)));
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
VillagerOptimizer.getLang(player.locale()).command_radius_invalid.forEach(player::sendMessage);
|
||||
VillagerOptimizer.getLang(player.locale()).command_radius_invalid
|
||||
.forEach(line -> KyoriUtil.sendMessage(player, line));
|
||||
}
|
||||
|
||||
return true;
|
||||
|
@ -1,12 +1,12 @@
|
||||
package me.xginko.villageroptimizer.commands.unoptimizevillagers;
|
||||
|
||||
import me.xginko.villageroptimizer.VillagerCache;
|
||||
import me.xginko.villageroptimizer.VillagerOptimizer;
|
||||
import me.xginko.villageroptimizer.WrappedVillager;
|
||||
import me.xginko.villageroptimizer.commands.VillagerOptimizerCommand;
|
||||
import me.xginko.villageroptimizer.enums.OptimizationType;
|
||||
import me.xginko.villageroptimizer.enums.permissions.Commands;
|
||||
import me.xginko.villageroptimizer.struct.enums.OptimizationType;
|
||||
import me.xginko.villageroptimizer.struct.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;
|
||||
@ -20,41 +20,46 @@ import org.bukkit.entity.Villager;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class UnOptVillagersRadius implements VillagerOptimizerCommand {
|
||||
public class UnOptVillagersRadius extends VillagerOptimizerCommand {
|
||||
|
||||
private final int max_radius;
|
||||
|
||||
public UnOptVillagersRadius() {
|
||||
this.max_radius = VillagerOptimizer.getConfiguration().getInt("optimization-methods.commands.unoptimizevillagers.max-block-radius", 100);
|
||||
super("unoptimizevillagers");
|
||||
this.max_radius = VillagerOptimizer.config()
|
||||
.getInt("optimization-methods.commands.unoptimizevillagers.max-block-radius", 100);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String label() {
|
||||
return "unoptimizevillagers";
|
||||
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();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
|
||||
return args.length == 1 ? RADIUS_TABCOMPLETES : NO_TABCOMPLETES;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
|
||||
if (!sender.hasPermission(Commands.UNOPTIMIZE_RADIUS.get())) {
|
||||
sender.sendMessage(VillagerOptimizer.getLang(sender).no_permission);
|
||||
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;
|
||||
}
|
||||
|
||||
if (!(sender instanceof Player player)) {
|
||||
sender.sendMessage(Component.text("This command can only be executed by a player.")
|
||||
if (!(sender instanceof Player)) {
|
||||
KyoriUtil.sendMessage(sender, Component.text("This command can only be executed by a player.")
|
||||
.color(NamedTextColor.RED).decorate(TextDecoration.BOLD));
|
||||
return true;
|
||||
}
|
||||
|
||||
Player player = (Player) sender;
|
||||
|
||||
if (args.length != 1) {
|
||||
VillagerOptimizer.getLang(player.locale()).command_specify_radius.forEach(player::sendMessage);
|
||||
VillagerOptimizer.getLang(player.locale()).command_specify_radius
|
||||
.forEach(line -> KyoriUtil.sendMessage(sender, line));
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -64,7 +69,8 @@ public class UnOptVillagersRadius implements VillagerOptimizerCommand {
|
||||
final int safeRadius = (int) Math.sqrt(specifiedRadius * specifiedRadius);
|
||||
|
||||
if (safeRadius == 0) {
|
||||
VillagerOptimizer.getLang(player.locale()).command_radius_invalid.forEach(player::sendMessage);
|
||||
VillagerOptimizer.getLang(player.locale()).command_radius_invalid
|
||||
.forEach(line -> KyoriUtil.sendMessage(sender, line));
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -73,11 +79,11 @@ public class UnOptVillagersRadius implements VillagerOptimizerCommand {
|
||||
.matchLiteral("%distance%")
|
||||
.replacement(Integer.toString(max_radius))
|
||||
.build();
|
||||
VillagerOptimizer.getLang(player.locale()).command_radius_limit_exceed.forEach(line -> player.sendMessage(line.replaceText(limit)));
|
||||
VillagerOptimizer.getLang(player.locale()).command_radius_limit_exceed
|
||||
.forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(limit)));
|
||||
return true;
|
||||
}
|
||||
|
||||
VillagerCache villagerCache = VillagerOptimizer.getCache();
|
||||
int successCount = 0;
|
||||
|
||||
for (Entity entity : player.getNearbyEntities(safeRadius, safeRadius, safeRadius)) {
|
||||
@ -86,7 +92,7 @@ public class UnOptVillagersRadius implements VillagerOptimizerCommand {
|
||||
Villager.Profession profession = villager.getProfession();
|
||||
if (profession.equals(Villager.Profession.NITWIT) || profession.equals(Villager.Profession.NONE)) continue;
|
||||
|
||||
WrappedVillager wVillager = villagerCache.getOrAdd(villager);
|
||||
WrappedVillager wVillager = VillagerOptimizer.wrappers().get(villager, WrappedVillager::new);
|
||||
|
||||
if (wVillager.isOptimized()) {
|
||||
VillagerUnoptimizeEvent unOptimizeEvent = new VillagerUnoptimizeEvent(wVillager, player, OptimizationType.COMMAND);
|
||||
@ -102,7 +108,8 @@ public class UnOptVillagersRadius implements VillagerOptimizerCommand {
|
||||
.matchLiteral("%radius%")
|
||||
.replacement(Integer.toString(safeRadius))
|
||||
.build();
|
||||
VillagerOptimizer.getLang(player.locale()).command_no_villagers_nearby.forEach(line -> player.sendMessage(line.replaceText(radius)));
|
||||
VillagerOptimizer.getLang(player.locale()).command_no_villagers_nearby
|
||||
.forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(radius)));
|
||||
} else {
|
||||
final TextReplacementConfig success_amount = TextReplacementConfig.builder()
|
||||
.matchLiteral("%amount%")
|
||||
@ -112,13 +119,12 @@ public class UnOptVillagersRadius implements VillagerOptimizerCommand {
|
||||
.matchLiteral("%radius%")
|
||||
.replacement(Integer.toString(safeRadius))
|
||||
.build();
|
||||
VillagerOptimizer.getLang(player.locale()).command_unoptimize_success.forEach(line -> player.sendMessage(line
|
||||
.replaceText(success_amount)
|
||||
.replaceText(radius)
|
||||
));
|
||||
VillagerOptimizer.getLang(player.locale()).command_unoptimize_success
|
||||
.forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(success_amount).replaceText(radius)));
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
VillagerOptimizer.getLang(player.locale()).command_radius_invalid.forEach(player::sendMessage);
|
||||
VillagerOptimizer.getLang(player.locale()).command_radius_invalid
|
||||
.forEach(line -> KyoriUtil.sendMessage(player, line));
|
||||
}
|
||||
|
||||
return true;
|
||||
|
@ -1,75 +1,88 @@
|
||||
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.Commands;
|
||||
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 implements VillagerOptimizerCommand {
|
||||
public class VillagerOptimizerCmd extends VillagerOptimizerCommand {
|
||||
|
||||
private final List<SubCommand> subCommands;
|
||||
private final List<String> tabCompleter;
|
||||
private final List<String> tabCompletes;
|
||||
|
||||
public VillagerOptimizerCmd() {
|
||||
subCommands = List.of(new ReloadSubCmd(), new VersionSubCmd(), new DisableSubCmd());
|
||||
tabCompleter = subCommands.stream().map(SubCommand::getLabel).toList();
|
||||
super("villageroptimizer");
|
||||
subCommands = Arrays.asList(new ReloadSubCmd(), new VersionSubCmd(), new DisableSubCmd());
|
||||
tabCompletes = subCommands.stream().map(SubCommand::label).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@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 : NO_TABCOMPLETES;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
|
||||
if (args.length == 0) {
|
||||
sendCommandOverview(sender);
|
||||
return true;
|
||||
public @Nullable List<String> onTabComplete(
|
||||
@NotNull CommandSender sender, @NotNull Command command, @NotNull String commandLabel, @NotNull String[] args
|
||||
) {
|
||||
if (args.length == 1) {
|
||||
return tabCompletes;
|
||||
}
|
||||
|
||||
for (final SubCommand subCommand : subCommands) {
|
||||
if (args[0].equalsIgnoreCase(subCommand.getLabel())) {
|
||||
subCommand.perform(sender, args);
|
||||
return true;
|
||||
if (args.length >= 2) {
|
||||
for (SubCommand subCommand : subCommands) {
|
||||
if (args[0].equalsIgnoreCase(subCommand.label())) {
|
||||
return subCommand.onTabComplete(sender, command, commandLabel, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sendCommandOverview(sender);
|
||||
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 sendCommandOverview(CommandSender sender) {
|
||||
if (!sender.hasPermission(Commands.RELOAD.get()) && !sender.hasPermission(Commands.VERSION.get())) return;
|
||||
sender.sendMessage(Component.text("-----------------------------------------------------").color(NamedTextColor.GRAY));
|
||||
sender.sendMessage(Component.text("VillagerOptimizer Commands").color(VillagerOptimizer.plugin_style.color()));
|
||||
sender.sendMessage(Component.text("-----------------------------------------------------").color(NamedTextColor.GRAY));
|
||||
subCommands.forEach(subCommand -> sender.sendMessage(
|
||||
subCommand.getSyntax().append(Component.text(" - ").color(NamedTextColor.DARK_GRAY)).append(subCommand.getDescription())));
|
||||
sender.sendMessage(
|
||||
Component.text("/optimizevillagers <blockradius>").color(VillagerOptimizer.plugin_style.color())
|
||||
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))
|
||||
);
|
||||
sender.sendMessage(
|
||||
Component.text("/unoptmizevillagers <blockradius>").color(VillagerOptimizer.plugin_style.color())
|
||||
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))
|
||||
);
|
||||
sender.sendMessage(Component.text("-----------------------------------------------------").color(NamedTextColor.GRAY));
|
||||
KyoriUtil.sendMessage(sender, Component.text("-----------------------------------------------------").color(NamedTextColor.GRAY));
|
||||
}
|
||||
}
|
@ -2,42 +2,51 @@ package me.xginko.villageroptimizer.commands.villageroptimizer.subcommands;
|
||||
|
||||
import me.xginko.villageroptimizer.VillagerOptimizer;
|
||||
import me.xginko.villageroptimizer.commands.SubCommand;
|
||||
import me.xginko.villageroptimizer.enums.permissions.Commands;
|
||||
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.TextComponent;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class DisableSubCmd extends SubCommand {
|
||||
|
||||
@Override
|
||||
public String getLabel() {
|
||||
return "disable";
|
||||
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 TextComponent getDescription() {
|
||||
return Component.text("Disable all plugin tasks and listeners.").color(NamedTextColor.GRAY);
|
||||
public @Nullable List<String> onTabComplete(
|
||||
@NotNull CommandSender sender, @NotNull Command command, @NotNull String commandLabel, @NotNull String[] args
|
||||
) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@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(Commands.DISABLE.get())) {
|
||||
sender.sendMessage(VillagerOptimizer.getLang(sender).no_permission);
|
||||
return;
|
||||
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;
|
||||
}
|
||||
|
||||
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));
|
||||
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;
|
||||
}
|
||||
}
|
@ -2,40 +2,49 @@ package me.xginko.villageroptimizer.commands.villageroptimizer.subcommands;
|
||||
|
||||
import me.xginko.villageroptimizer.VillagerOptimizer;
|
||||
import me.xginko.villageroptimizer.commands.SubCommand;
|
||||
import me.xginko.villageroptimizer.enums.permissions.Commands;
|
||||
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.TextComponent;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class ReloadSubCmd extends SubCommand {
|
||||
|
||||
@Override
|
||||
public String getLabel() {
|
||||
return "reload";
|
||||
public ReloadSubCmd() {
|
||||
super(
|
||||
"reload",
|
||||
Component.text("/villageroptimizer reload").color(Util.PL_COLOR),
|
||||
Component.text("Reload the plugin configuration.").color(NamedTextColor.GRAY));
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextComponent getDescription() {
|
||||
return Component.text("Reload the plugin configuration.").color(NamedTextColor.GRAY);
|
||||
public @Nullable List<String> onTabComplete(
|
||||
@NotNull CommandSender sender, @NotNull Command command, @NotNull String commandLabel, @NotNull String[] args
|
||||
) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@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(Commands.RELOAD.get())) {
|
||||
sender.sendMessage(VillagerOptimizer.getLang(sender).no_permission);
|
||||
return;
|
||||
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;
|
||||
}
|
||||
|
||||
sender.sendMessage(Component.text("Reloading VillagerOptimizer...").color(NamedTextColor.WHITE));
|
||||
VillagerOptimizer.getFoliaLib().getImpl().runNextTick(reload -> { // Reload in sync with the server
|
||||
KyoriUtil.sendMessage(sender, Component.text("Reloading VillagerOptimizer...").color(NamedTextColor.WHITE));
|
||||
VillagerOptimizer.scheduling().asyncScheduler().run(reload -> {
|
||||
VillagerOptimizer.getInstance().reloadPlugin();
|
||||
sender.sendMessage(Component.text("Reload complete.").color(NamedTextColor.GREEN));
|
||||
KyoriUtil.sendMessage(sender, Component.text("Reload complete.").color(NamedTextColor.GREEN));
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
@ -3,37 +3,45 @@ 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.Commands;
|
||||
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.TextComponent;
|
||||
import net.kyori.adventure.text.event.ClickEvent;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.plugin.PluginDescriptionFile;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class VersionSubCmd extends SubCommand {
|
||||
|
||||
@Override
|
||||
public String getLabel() {
|
||||
return "version";
|
||||
public VersionSubCmd() {
|
||||
super(
|
||||
"version",
|
||||
Component.text("/villageroptimizer version").color(Util.PL_COLOR),
|
||||
Component.text("Show the plugin version.").color(NamedTextColor.GRAY)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextComponent getDescription() {
|
||||
return Component.text("Show the plugin version.").color(NamedTextColor.GRAY);
|
||||
public @Nullable List<String> onTabComplete(
|
||||
@NotNull CommandSender sender, @NotNull Command command, @NotNull String commandLabel, @NotNull String[] args
|
||||
) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextComponent getSyntax() {
|
||||
return Component.text("/villageroptimizer version").color(VillagerOptimizer.plugin_style.color());
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings({"deprecation", "UnstableApiUsage"})
|
||||
public void perform(CommandSender sender, String[] args) {
|
||||
if (!sender.hasPermission(Commands.VERSION.get())) {
|
||||
sender.sendMessage(VillagerOptimizer.getLang(sender).no_permission);
|
||||
return;
|
||||
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;
|
||||
@ -52,10 +60,10 @@ public class VersionSubCmd extends SubCommand {
|
||||
author = pluginYML.getAuthors().get(0);
|
||||
}
|
||||
|
||||
sender.sendMessage(Component.newline()
|
||||
KyoriUtil.sendMessage(sender, Component.newline()
|
||||
.append(
|
||||
Component.text(name + " " + version)
|
||||
.style(VillagerOptimizer.plugin_style)
|
||||
.style(Util.PL_STYLE)
|
||||
.clickEvent(ClickEvent.openUrl(website))
|
||||
)
|
||||
.append(Component.text(" by ").color(NamedTextColor.GRAY))
|
||||
@ -66,5 +74,7 @@ public class VersionSubCmd extends SubCommand {
|
||||
)
|
||||
.append(Component.newline())
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ 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;
|
||||
|
||||
@ -12,37 +13,36 @@ public class Config {
|
||||
|
||||
private final @NotNull ConfigFile config;
|
||||
public final @NotNull Locale default_lang;
|
||||
public final @NotNull Duration cache_keep_time;
|
||||
public final boolean auto_lang, support_other_plugins;
|
||||
public final long cache_keep_time_seconds;
|
||||
|
||||
public Config() throws Exception {
|
||||
// Create plugin folder first if it does not exist yet
|
||||
File pluginFolder = VillagerOptimizer.getInstance().getDataFolder();
|
||||
if (!pluginFolder.exists() && !pluginFolder.mkdir())
|
||||
VillagerOptimizer.getLog().error("Failed to create plugin directory.");
|
||||
// Load config.yml with ConfigMaster
|
||||
this.config = ConfigFile.loadConfig(new File(pluginFolder, "config.yml"));
|
||||
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 or no matching language file was found.")
|
||||
"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_seconds = getInt("general.cache-keep-time-seconds", 30,
|
||||
"The amount of time in seconds a villager will be kept in the plugin's cache.");
|
||||
this.support_other_plugins = getBoolean("general.support-avl-villagers", false, """
|
||||
Enable if you have previously used AntiVillagerLag (https://www.spigotmc.org/resources/antivillagerlag.102949/).\s
|
||||
Tries to read pre-existing info like optimization state so players don't need to reoptimize their villagers.""");
|
||||
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 (Exception e) {
|
||||
VillagerOptimizer.getLog().error("Failed to save config file! - " + e.getLocalizedMessage());
|
||||
} catch (Throwable throwable) {
|
||||
VillagerOptimizer.logger().error("Failed to save config file!", throwable);
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,11 +51,10 @@ public class Config {
|
||||
this.createTitledSection("General", "general");
|
||||
this.createTitledSection("Optimization", "optimization-methods");
|
||||
this.config.addDefault("optimization-methods.commands.unoptimizevillagers", null);
|
||||
this.config.addComment("optimization-methods.commands", """
|
||||
If you want to disable commands, negate the following permissions:\s
|
||||
villageroptimizer.cmd.optimize\s
|
||||
villageroptimizer.cmd.unoptimize
|
||||
""");
|
||||
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");
|
||||
@ -119,13 +118,13 @@ public class Config {
|
||||
return this.config.getInteger(path, def);
|
||||
}
|
||||
|
||||
public @NotNull List<String> getList(@NotNull String path, @NotNull List<String> def, @NotNull String comment) {
|
||||
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.getStringList(path);
|
||||
return this.config.getList(path);
|
||||
}
|
||||
|
||||
public @NotNull List<String> getList(@NotNull String path, @NotNull List<String> def) {
|
||||
public @NotNull <T> List<T> getList(@NotNull String path, @NotNull List<T> def) {
|
||||
this.config.addDefault(path, def);
|
||||
return this.config.getStringList(path);
|
||||
return this.config.getList(path);
|
||||
}
|
||||
}
|
||||
|
@ -2,12 +2,15 @@ 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 {
|
||||
|
||||
@ -17,6 +20,7 @@ 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;
|
||||
@ -27,83 +31,77 @@ public class LanguageCache {
|
||||
// Check if the lang folder has already been created
|
||||
File parent = langYML.getParentFile();
|
||||
if (!parent.exists() && !parent.mkdir())
|
||||
VillagerOptimizer.getLog().error("Failed to create lang directory.");
|
||||
// Check if the file already exists and save the one from the plugins resources folder if it does not
|
||||
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
|
||||
if (!langYML.exists())
|
||||
plugin.saveResource("lang" + File.separator + locale + ".yml", false);
|
||||
// Finally load the lang file with configmaster
|
||||
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%"));
|
||||
"<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."));
|
||||
"<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%."));
|
||||
"<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."));
|
||||
"<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."));
|
||||
"<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."));
|
||||
"<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%."));
|
||||
"<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."));
|
||||
"<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%."));
|
||||
"<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%."));
|
||||
"<green>%villagertype% villager successfully optimized using workstation %blocktype%.");
|
||||
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."));
|
||||
"<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%."));
|
||||
"<green>Successfully unoptimized %villagertype% villager by removing workstation block %blocktype%.");
|
||||
// Activity
|
||||
this.activity_optimize_success = getListTranslation("messages.activity.optimized-near-you",
|
||||
"<gray>%amount% villagers close to you were automatically optimized due to high activity.");
|
||||
|
||||
// Command
|
||||
this.command_optimize_success = getListTranslation("messages.command.optimize-success",
|
||||
List.of("<green>Successfully optimized %amount% villager(s) in a radius of %radius% blocks."));
|
||||
"<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."));
|
||||
"<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."));
|
||||
"<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."));
|
||||
"<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."));
|
||||
"<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."));
|
||||
"<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%."));
|
||||
"<gray>Couldn't find any employed villagers within a radius of %radius%.");
|
||||
|
||||
try {
|
||||
this.lang.save();
|
||||
} catch (Exception e) {
|
||||
VillagerOptimizer.getLog().error("Failed to save language file: "+ langYML.getName() +" - " + e.getLocalizedMessage());
|
||||
} catch (Throwable throwable) {
|
||||
VillagerOptimizer.logger().error("Failed to save language file: " + langYML.getName(), throwable);
|
||||
}
|
||||
}
|
||||
|
||||
public @NotNull Component getTranslation(@NotNull String path, @NotNull String defaultTranslation) {
|
||||
this.lang.addDefault(path, defaultTranslation);
|
||||
return MiniMessage.miniMessage().deserialize(this.lang.getString(path, defaultTranslation));
|
||||
return MiniMessage.miniMessage().deserialize(KyoriUtil.translateChatColor(this.lang.getString(path, defaultTranslation)));
|
||||
}
|
||||
|
||||
public @NotNull Component getTranslation(@NotNull String path, @NotNull String defaultTranslation, @NotNull String comment) {
|
||||
this.lang.addDefault(path, defaultTranslation, comment);
|
||||
return MiniMessage.miniMessage().deserialize(this.lang.getString(path, defaultTranslation));
|
||||
}
|
||||
|
||||
public @NotNull List<Component> getListTranslation(@NotNull String path, @NotNull List<String> defaultTranslation) {
|
||||
this.lang.addDefault(path, defaultTranslation);
|
||||
return this.lang.getStringList(path).stream().map(MiniMessage.miniMessage()::deserialize).toList();
|
||||
}
|
||||
|
||||
public @NotNull List<Component> getListTranslation(@NotNull String path, @NotNull List<String> defaultTranslation, @NotNull String comment) {
|
||||
this.lang.addDefault(path, defaultTranslation, comment);
|
||||
return this.lang.getStringList(path).stream().map(MiniMessage.miniMessage()::deserialize).toList();
|
||||
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());
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
package me.xginko.villageroptimizer.enums.permissions;
|
||||
|
||||
import org.bukkit.permissions.Permission;
|
||||
import org.bukkit.permissions.PermissionDefault;
|
||||
|
||||
public enum Bypass {
|
||||
TRADE_PREVENTION(new Permission("villageroptimizer.bypass.tradeprevention",
|
||||
"Permission to bypass unoptimized trade prevention", PermissionDefault.FALSE)),
|
||||
RESTOCK_COOLDOWN(new Permission("villageroptimizer.bypass.restockcooldown",
|
||||
"Permission to bypass restock cooldown on optimized villagers", PermissionDefault.FALSE)),
|
||||
NAMETAG_COOLDOWN(new Permission("villageroptimizer.bypass.nametagcooldown",
|
||||
"Permission to bypass Nametag optimization cooldown", PermissionDefault.FALSE)),
|
||||
BLOCK_COOLDOWN(new Permission("villageroptimizer.bypass.blockcooldown",
|
||||
"Permission to bypass Block optimization cooldown", PermissionDefault.FALSE)),
|
||||
WORKSTATION_COOLDOWN(new Permission("villageroptimizer.bypass.workstationcooldown",
|
||||
"Permission to bypass Workstation optimization cooldown", PermissionDefault.FALSE)),
|
||||
COMMAND_COOLDOWN(new Permission("villageroptimizer.bypass.commandcooldown",
|
||||
"Permission to bypass command optimization cooldown", PermissionDefault.FALSE));
|
||||
|
||||
private final Permission permission;
|
||||
Bypass(Permission permission) {
|
||||
this.permission = permission;
|
||||
}
|
||||
|
||||
public Permission get() {
|
||||
return permission;
|
||||
}
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
package me.xginko.villageroptimizer.enums.permissions;
|
||||
|
||||
import org.bukkit.permissions.Permission;
|
||||
import org.bukkit.permissions.PermissionDefault;
|
||||
|
||||
public enum Commands {
|
||||
VERSION(new Permission("villageroptimizer.cmd.version",
|
||||
"Permission get the plugin version", PermissionDefault.OP)),
|
||||
RELOAD(new Permission("villageroptimizer.cmd.reload",
|
||||
"Permission to reload the plugin config", PermissionDefault.OP)),
|
||||
DISABLE(new Permission("villageroptimizer.cmd.disable",
|
||||
"Permission to disable the plugin", PermissionDefault.OP)),
|
||||
OPTIMIZE_RADIUS(new Permission("villageroptimizer.cmd.optimize",
|
||||
"Permission to optimize villagers in a radius", PermissionDefault.TRUE)),
|
||||
UNOPTIMIZE_RADIUS(new Permission("villageroptimizer.cmd.unoptimize",
|
||||
"Permission to unoptimize villagers in a radius", PermissionDefault.TRUE));
|
||||
|
||||
private final Permission permission;
|
||||
Commands(Permission permission) {
|
||||
this.permission = permission;
|
||||
}
|
||||
|
||||
public Permission get() {
|
||||
return permission;
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
package me.xginko.villageroptimizer.enums.permissions;
|
||||
|
||||
import org.bukkit.permissions.Permission;
|
||||
import org.bukkit.permissions.PermissionDefault;
|
||||
|
||||
public enum Optimize {
|
||||
NAMETAG(new Permission("villageroptimizer.optimize.nametag",
|
||||
"Permission to optimize / unoptimize using Nametags", PermissionDefault.TRUE)),
|
||||
BLOCK(new Permission("villageroptimizer.optimize.block",
|
||||
"Permission to optimize / unoptimize using Blocks", PermissionDefault.TRUE)),
|
||||
WORKSTATION(new Permission("villageroptimizer.optimize.workstation",
|
||||
"Permission to optimize / unoptimize using Workstations", PermissionDefault.TRUE));
|
||||
|
||||
private final Permission permission;
|
||||
Optimize(Permission permission) {
|
||||
this.permission = permission;
|
||||
}
|
||||
|
||||
public Permission get() {
|
||||
return permission;
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
package me.xginko.villageroptimizer.events;
|
||||
|
||||
import me.xginko.villageroptimizer.WrappedVillager;
|
||||
import me.xginko.villageroptimizer.enums.OptimizationType;
|
||||
import me.xginko.villageroptimizer.wrapper.WrappedVillager;
|
||||
import me.xginko.villageroptimizer.struct.enums.OptimizationType;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.Cancellable;
|
||||
import org.bukkit.event.Event;
|
||||
@ -17,7 +17,12 @@ public class VillagerOptimizeEvent extends Event implements Cancellable {
|
||||
private final @Nullable Player whoOptimised;
|
||||
private boolean isCancelled = false;
|
||||
|
||||
public VillagerOptimizeEvent(@NotNull WrappedVillager wrappedVillager, @NotNull OptimizationType optimizationType, @Nullable Player whoOptimised, boolean isAsync) throws IllegalArgumentException {
|
||||
public VillagerOptimizeEvent(
|
||||
@NotNull WrappedVillager wrappedVillager,
|
||||
@NotNull OptimizationType optimizationType,
|
||||
@Nullable Player whoOptimised,
|
||||
boolean isAsync
|
||||
) throws IllegalArgumentException {
|
||||
super(isAsync);
|
||||
this.wrappedVillager = wrappedVillager;
|
||||
this.whoOptimised = whoOptimised;
|
||||
@ -28,7 +33,11 @@ public class VillagerOptimizeEvent extends Event implements Cancellable {
|
||||
}
|
||||
}
|
||||
|
||||
public VillagerOptimizeEvent(@NotNull WrappedVillager wrappedVillager, @NotNull OptimizationType optimizationType, @Nullable Player whoOptimised) throws IllegalArgumentException {
|
||||
public VillagerOptimizeEvent(
|
||||
@NotNull WrappedVillager wrappedVillager,
|
||||
@NotNull OptimizationType optimizationType,
|
||||
@Nullable Player whoOptimised
|
||||
) throws IllegalArgumentException {
|
||||
this.wrappedVillager = wrappedVillager;
|
||||
this.whoOptimised = whoOptimised;
|
||||
if (optimizationType.equals(OptimizationType.NONE)) {
|
||||
|
@ -1,7 +1,7 @@
|
||||
package me.xginko.villageroptimizer.events;
|
||||
|
||||
import me.xginko.villageroptimizer.WrappedVillager;
|
||||
import me.xginko.villageroptimizer.enums.OptimizationType;
|
||||
import me.xginko.villageroptimizer.wrapper.WrappedVillager;
|
||||
import me.xginko.villageroptimizer.struct.enums.OptimizationType;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.Cancellable;
|
||||
import org.bukkit.event.Event;
|
||||
@ -17,14 +17,23 @@ public class VillagerUnoptimizeEvent extends Event implements Cancellable {
|
||||
private final @Nullable Player whoUnoptimized;
|
||||
private boolean isCancelled = false;
|
||||
|
||||
public VillagerUnoptimizeEvent(@NotNull WrappedVillager wrappedVillager, @Nullable Player whoUnoptimized, @NotNull OptimizationType unOptimizeType, boolean isAsync) {
|
||||
public VillagerUnoptimizeEvent(
|
||||
@NotNull WrappedVillager wrappedVillager,
|
||||
@Nullable Player whoUnoptimized,
|
||||
@NotNull OptimizationType unOptimizeType,
|
||||
boolean isAsync
|
||||
) {
|
||||
super(isAsync);
|
||||
this.wrappedVillager = wrappedVillager;
|
||||
this.whoUnoptimized = whoUnoptimized;
|
||||
this.unOptimizeType = unOptimizeType;
|
||||
}
|
||||
|
||||
public VillagerUnoptimizeEvent(@NotNull WrappedVillager wrappedVillager, @Nullable Player whoUnoptimized, @NotNull OptimizationType unOptimizeType) {
|
||||
public VillagerUnoptimizeEvent(
|
||||
@NotNull WrappedVillager wrappedVillager,
|
||||
@Nullable Player whoUnoptimized,
|
||||
@NotNull OptimizationType unOptimizeType
|
||||
) {
|
||||
this.wrappedVillager = wrappedVillager;
|
||||
this.whoUnoptimized = whoUnoptimized;
|
||||
this.unOptimizeType = unOptimizeType;
|
||||
|
@ -0,0 +1,22 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
package me.xginko.villageroptimizer.models;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public final class ExpiringSet<E> {
|
||||
|
||||
private final Cache<E, Object> cache;
|
||||
private static final Object PRESENT = new Object(); // Dummy value to associate with an Object in the backing Cache
|
||||
|
||||
public ExpiringSet(long duration, TimeUnit unit) {
|
||||
this.cache = Caffeine.newBuilder().expireAfterWrite(duration, unit).build();
|
||||
}
|
||||
|
||||
public ExpiringSet(Duration duration) {
|
||||
this.cache = Caffeine.newBuilder().expireAfterWrite(duration).build();
|
||||
}
|
||||
|
||||
public void add(E item) {
|
||||
this.cache.put(item, PRESENT);
|
||||
}
|
||||
|
||||
public boolean contains(E item) {
|
||||
return this.cache.getIfPresent(item) != null;
|
||||
}
|
||||
}
|
@ -1,17 +1,13 @@
|
||||
package me.xginko.villageroptimizer.modules;
|
||||
|
||||
import com.tcoded.folialib.impl.ServerImplementation;
|
||||
import com.tcoded.folialib.wrapper.task.WrappedTask;
|
||||
import me.xginko.villageroptimizer.VillagerCache;
|
||||
import me.xginko.villageroptimizer.VillagerOptimizer;
|
||||
import me.xginko.villageroptimizer.config.Config;
|
||||
import me.xginko.villageroptimizer.utils.CommonUtil;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import 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.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;
|
||||
@ -20,91 +16,118 @@ 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 implements VillagerOptimizerModule, Listener {
|
||||
public class VillagerChunkLimit extends VillagerOptimizerModule implements Runnable, Listener {
|
||||
|
||||
private final ServerImplementation scheduler;
|
||||
private final VillagerCache villagerCache;
|
||||
private WrappedTask periodic_chunk_check;
|
||||
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_entity_chunks;
|
||||
private final boolean log_enabled, skip_unloaded_chunks, use_whitelist;
|
||||
|
||||
protected VillagerChunkLimit() {
|
||||
shouldEnable();
|
||||
this.scheduler = VillagerOptimizer.getFoliaLib().getImpl();
|
||||
this.villagerCache = VillagerOptimizer.getCache();
|
||||
Config config = VillagerOptimizer.getConfiguration();
|
||||
config.master().addComment("villager-chunk-limit.enable", """
|
||||
Checks chunks for too many villagers and removes excess villagers based on priority.""");
|
||||
this.check_period = config.getInt("villager-chunk-limit.check-period-in-ticks", 600, """
|
||||
Check all loaded chunks every X ticks. 1 second = 20 ticks\s
|
||||
A shorter delay in between checks is more efficient but is also more resource intense.\s
|
||||
A larger delay is less resource intense but could become inefficient.""");
|
||||
this.skip_unloaded_entity_chunks = config.getBoolean("villager-chunk-limit.skip-if-chunk-has-not-loaded-entities", true,
|
||||
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.log_enabled = config.getBoolean("villager-chunk-limit.log-removals", true);
|
||||
this.non_optimized_max_per_chunk = config.getInt("villager-chunk-limit.unoptimized.max-per-chunk", 20,
|
||||
"The maximum amount of unoptimized villagers per chunk.");
|
||||
this.non_optimized_removal_priority = config.getList("villager-chunk-limit.unoptimized.removal-priority", List.of(
|
||||
"NONE", "NITWIT", "SHEPHERD", "FISHERMAN", "BUTCHER", "CARTOGRAPHER", "LEATHERWORKER",
|
||||
"FLETCHER", "MASON", "FARMER", "ARMORER", "TOOLSMITH", "WEAPONSMITH", "CLERIC", "LIBRARIAN"
|
||||
), """
|
||||
Professions that are in the top of the list are going to be scheduled for removal first.\s
|
||||
Use enums from https://jd.papermc.io/paper/1.20/org/bukkit/entity/Villager.Profession.html"""
|
||||
).stream().map(configuredProfession -> {
|
||||
try {
|
||||
return Villager.Profession.valueOf(configuredProfession);
|
||||
} catch (IllegalArgumentException e) {
|
||||
VillagerOptimizer.getLog().warn("(villager-chunk-limit.unoptimized) Villager profession '"+configuredProfession +
|
||||
"' not recognized. Make sure you're using the correct profession enums from " +
|
||||
"https://jd.papermc.io/paper/1.20/org/bukkit/entity/Villager.Profession.html.");
|
||||
return null;
|
||||
}
|
||||
}).filter(Objects::nonNull).toList();
|
||||
this.optimized_max_per_chunk = config.getInt("villager-chunk-limit.optimized.max-per-chunk", 60,
|
||||
"The maximum amount of optimized villagers per chunk.");
|
||||
this.optimized_removal_priority = config.getList("villager-chunk-limit.optimized.removal-priority", List.of(
|
||||
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"
|
||||
)).stream().map(configuredProfession -> {
|
||||
try {
|
||||
return Villager.Profession.valueOf(configuredProfession);
|
||||
} catch (IllegalArgumentException e) {
|
||||
VillagerOptimizer.getLog().warn("(villager-chunk-limit.optimized) Villager profession '"+configuredProfession +
|
||||
"' not recognized. Make sure you're using the correct profession enums from " +
|
||||
"https://jd.papermc.io/paper/1.20/org/bukkit/entity/Villager.Profession.html.");
|
||||
return null;
|
||||
}
|
||||
}).filter(Objects::nonNull).toList();
|
||||
"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() {
|
||||
final VillagerOptimizer plugin = VillagerOptimizer.getInstance();
|
||||
final Server server = plugin.getServer();
|
||||
server.getPluginManager().registerEvents(this, plugin);
|
||||
|
||||
this.periodic_chunk_check = scheduler.runTimer(() -> {
|
||||
for (World world : server.getWorlds()) {
|
||||
for (Chunk chunk : world.getLoadedChunks()) {
|
||||
if (!skip_unloaded_entity_chunks || CommonUtil.isEntitiesLoaded(chunk)) {
|
||||
this.manageVillagerCount(chunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, check_period, check_period);
|
||||
plugin.getServer().getPluginManager().registerEvents(this, plugin);
|
||||
periodic_chunk_check = scheduling.globalRegionalScheduler().runAtFixedRate(this, check_period, check_period);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldEnable() {
|
||||
return VillagerOptimizer.getConfiguration().getBoolean("villager-chunk-limit.enable", false);
|
||||
return config.getBoolean(configPath + ".enable", false);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -113,33 +136,58 @@ public class VillagerChunkLimit implements VillagerOptimizerModule, Listener {
|
||||
if (periodic_chunk_check != null) periodic_chunk_check.cancel();
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
private void onCreatureSpawn(CreatureSpawnEvent event) {
|
||||
if (event.getEntityType() == EntityType.VILLAGER) {
|
||||
this.manageVillagerCount(event.getEntity().getChunk());
|
||||
@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.LOW, ignoreCancelled = true)
|
||||
@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() == EntityType.VILLAGER) {
|
||||
this.manageVillagerCount(event.getRightClicked().getChunk());
|
||||
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().equals(EntityType.VILLAGER)) {
|
||||
Villager villager = (Villager) entity;
|
||||
if (villagerCache.getOrAdd(villager).isOptimized()) {
|
||||
optimized_villagers.add(villager);
|
||||
} else {
|
||||
not_optimized_villagers.add(villager);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -154,14 +202,11 @@ public class VillagerChunkLimit implements VillagerOptimizerModule, Listener {
|
||||
// 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);
|
||||
scheduler.runAtEntity(villager, kill -> {
|
||||
scheduling.entitySpecificScheduler(villager).run(kill -> {
|
||||
villager.remove();
|
||||
if (log_enabled) {
|
||||
VillagerOptimizer.getLog().info(Component.text(
|
||||
"Removed unoptimized villager with profession '" + villager.getProfession().name() + "' at " +
|
||||
CommonUtil.formatLocation(villager.getLocation())).color(VillagerOptimizer.plugin_style.color()));
|
||||
}
|
||||
});
|
||||
if (log_enabled) info("Removed unoptimized villager with profession '" +
|
||||
Util.toNiceString(villager.getProfession()) + "' at " + LocationUtil.toString(villager.getLocation()));
|
||||
}, null);
|
||||
}
|
||||
}
|
||||
|
||||
@ -176,15 +221,11 @@ public class VillagerChunkLimit implements VillagerOptimizerModule, Listener {
|
||||
// Remove prioritized villagers that are too many
|
||||
for (int i = 0; i < optimized_villagers_too_many; i++) {
|
||||
Villager villager = optimized_villagers.get(i);
|
||||
scheduler.runAtEntity(villager, kill -> {
|
||||
scheduling.entitySpecificScheduler(villager).run(kill -> {
|
||||
villager.remove();
|
||||
|
||||
if (log_enabled) {
|
||||
VillagerOptimizer.getLog().info(Component.text("Removed optimized villager with profession '" +
|
||||
villager.getProfession().name() + "' at " +
|
||||
CommonUtil.formatLocation(villager.getLocation())).color(VillagerOptimizer.plugin_style.color()));
|
||||
}
|
||||
});
|
||||
if (log_enabled) info("Removed unoptimized villager with profession '" +
|
||||
Util.toNiceString(villager.getProfession()) + "' at " + LocationUtil.toString(villager.getLocation()));
|
||||
}, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,43 +1,87 @@
|
||||
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 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 interface VillagerOptimizerModule {
|
||||
public abstract class VillagerOptimizerModule implements Enableable, Disableable {
|
||||
|
||||
void enable();
|
||||
void disable();
|
||||
boolean shouldEnable();
|
||||
private static final Reflections MODULES_PACKAGE = new Reflections(VillagerOptimizerModule.class.getPackage().getName());
|
||||
public static final Set<VillagerOptimizerModule> ENABLED_MODULES = new HashSet<>();
|
||||
|
||||
HashSet<VillagerOptimizerModule> modules = new HashSet<>();
|
||||
public abstract boolean shouldEnable();
|
||||
|
||||
static void reloadModules() {
|
||||
modules.forEach(VillagerOptimizerModule::disable);
|
||||
modules.clear();
|
||||
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;
|
||||
|
||||
modules.add(new OptimizeByNametag());
|
||||
modules.add(new OptimizeByBlock());
|
||||
modules.add(new OptimizeByWorkstation());
|
||||
public VillagerOptimizerModule(String configPath) {
|
||||
this.plugin = VillagerOptimizer.getInstance();
|
||||
this.config = VillagerOptimizer.config();
|
||||
this.wrapperCache = VillagerOptimizer.wrappers();
|
||||
this.scheduling = VillagerOptimizer.scheduling();
|
||||
this.configPath = configPath;
|
||||
shouldEnable(); // Ensure enable option is always first
|
||||
String[] paths = configPath.split("\\.");
|
||||
if (paths.length <= 2) {
|
||||
this.logFormat = "<" + configPath + "> {}";
|
||||
} else {
|
||||
this.logFormat = "<" + paths[paths.length - 2] + "." + paths[paths.length - 1] + "> {}";
|
||||
}
|
||||
}
|
||||
|
||||
modules.add(new EnableLeashingVillagers());
|
||||
modules.add(new FixOptimisationAfterCure());
|
||||
modules.add(new RestockOptimizedTrades());
|
||||
modules.add(new LevelOptimizedProfession());
|
||||
modules.add(new RenameOptimizedVillagers());
|
||||
modules.add(new MakeVillagersSpawnAdult());
|
||||
modules.add(new PreventUnoptimizedTrading());
|
||||
modules.add(new PreventOptimizedTargeting());
|
||||
modules.add(new PreventOptimizedDamage());
|
||||
modules.add(new UnoptimizeOnJobLoose());
|
||||
public static void reloadModules() {
|
||||
ENABLED_MODULES.forEach(VillagerOptimizerModule::disable);
|
||||
ENABLED_MODULES.clear();
|
||||
|
||||
modules.add(new VillagerChunkLimit());
|
||||
for (Class<?> clazz : MODULES_PACKAGE.get(Scanners.SubTypes.of(VillagerOptimizerModule.class).asClass())) {
|
||||
if (clazz.isInterface() || Modifier.isAbstract(clazz.getModifiers())) continue;
|
||||
|
||||
modules.forEach(module -> {
|
||||
if (module.shouldEnable()) module.enable();
|
||||
});
|
||||
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.");
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,11 @@
|
||||
package me.xginko.villageroptimizer.modules.gameplay;
|
||||
|
||||
import com.tcoded.folialib.impl.ServerImplementation;
|
||||
import me.xginko.villageroptimizer.VillagerCache;
|
||||
import me.xginko.villageroptimizer.VillagerOptimizer;
|
||||
import me.xginko.villageroptimizer.config.Config;
|
||||
import com.cryptomorin.xseries.XEntityType;
|
||||
import com.cryptomorin.xseries.XMaterial;
|
||||
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
|
||||
import me.xginko.villageroptimizer.utils.CommonUtil;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import me.xginko.villageroptimizer.utils.LocationUtil;
|
||||
import me.xginko.villageroptimizer.wrapper.WrappedVillager;
|
||||
import org.bukkit.GameMode;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.entity.Villager;
|
||||
import org.bukkit.event.EventHandler;
|
||||
@ -20,27 +16,21 @@ import org.bukkit.event.entity.PlayerLeashEntityEvent;
|
||||
import org.bukkit.event.player.PlayerInteractEntityEvent;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
public class EnableLeashingVillagers implements VillagerOptimizerModule, Listener {
|
||||
public class EnableLeashingVillagers extends VillagerOptimizerModule implements Listener {
|
||||
|
||||
private final ServerImplementation scheduler;
|
||||
private final VillagerCache villagerCache;
|
||||
private final boolean only_optimized, log_enabled;
|
||||
|
||||
public EnableLeashingVillagers() {
|
||||
shouldEnable();
|
||||
this.scheduler = VillagerOptimizer.getFoliaLib().getImpl();
|
||||
this.villagerCache = VillagerOptimizer.getCache();
|
||||
Config config = VillagerOptimizer.getConfiguration();
|
||||
config.master().addComment("gameplay.villagers-can-be-leashed.enable", """
|
||||
Enable leashing of villagers, enabling players to easily move villagers to where they want them to be.""");
|
||||
this.only_optimized = config.getBoolean("gameplay.villagers-can-be-leashed.only-optimized", false,
|
||||
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("gameplay.villagers-can-be-leashed.log", false);
|
||||
this.log_enabled = config.getBoolean(configPath + ".log", false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enable() {
|
||||
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
|
||||
plugin.getServer().getPluginManager().registerEvents(this, plugin);
|
||||
}
|
||||
|
||||
@ -51,21 +41,22 @@ public class EnableLeashingVillagers implements VillagerOptimizerModule, Listene
|
||||
|
||||
@Override
|
||||
public boolean shouldEnable() {
|
||||
return VillagerOptimizer.getConfiguration().getBoolean("gameplay.villagers-can-be-leashed.enable", false);
|
||||
return config.getBoolean(configPath + ".enable", false);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
private void onLeash(PlayerInteractEntityEvent event) {
|
||||
if (!event.getRightClicked().getType().equals(EntityType.VILLAGER)) return;
|
||||
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().equals(Material.LEAD)) return;
|
||||
if (handItem == null || handItem.getType() != XMaterial.LEAD.parseMaterial()) return;
|
||||
|
||||
Villager villager = (Villager) event.getRightClicked();
|
||||
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
|
||||
if (only_optimized && !villagerCache.getOrAdd(villager).isOptimized()) return;
|
||||
|
||||
// Call event for compatibility with other plugins, constructing non deprecated if available
|
||||
PlayerLeashEntityEvent leashEvent;
|
||||
@ -78,16 +69,15 @@ public class EnableLeashingVillagers implements VillagerOptimizerModule, Listene
|
||||
// If canceled by any plugin, do nothing
|
||||
if (!leashEvent.callEvent()) return;
|
||||
|
||||
scheduler.runAtEntity(villager, leash -> {
|
||||
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) {
|
||||
VillagerOptimizer.getLog().info(Component.text(player.getName() + " leashed a villager at " +
|
||||
CommonUtil.formatLocation(villager.getLocation())).color(VillagerOptimizer.plugin_style.color()));
|
||||
info(player.getName() + " leashed a villager at " + LocationUtil.toString(villager.getLocation()));
|
||||
}
|
||||
});
|
||||
}, null);
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,8 @@
|
||||
package me.xginko.villageroptimizer.modules.gameplay;
|
||||
|
||||
import me.xginko.villageroptimizer.VillagerOptimizer;
|
||||
import me.xginko.villageroptimizer.WrappedVillager;
|
||||
import com.cryptomorin.xseries.XEntityType;
|
||||
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import me.xginko.villageroptimizer.wrapper.WrappedVillager;
|
||||
import org.bukkit.entity.Villager;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
@ -11,15 +10,14 @@ import org.bukkit.event.HandlerList;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.entity.EntityTransformEvent;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
public class FixOptimisationAfterCure extends VillagerOptimizerModule implements Listener {
|
||||
|
||||
public class FixOptimisationAfterCure implements VillagerOptimizerModule, Listener {
|
||||
|
||||
public FixOptimisationAfterCure() {}
|
||||
public FixOptimisationAfterCure() {
|
||||
super("post-cure-optimization-fix");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enable() {
|
||||
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
|
||||
plugin.getServer().getPluginManager().registerEvents(this, plugin);
|
||||
}
|
||||
|
||||
@ -36,14 +34,14 @@ public class FixOptimisationAfterCure implements VillagerOptimizerModule, Listen
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
private void onTransform(EntityTransformEvent event) {
|
||||
if (
|
||||
event.getTransformReason().equals(EntityTransformEvent.TransformReason.CURED)
|
||||
&& event.getTransformedEntity().getType().equals(EntityType.VILLAGER)
|
||||
event.getTransformReason() == EntityTransformEvent.TransformReason.CURED
|
||||
&& event.getTransformedEntity().getType() == XEntityType.VILLAGER.get()
|
||||
) {
|
||||
Villager villager = (Villager) event.getTransformedEntity();
|
||||
VillagerOptimizer.getFoliaLib().getImpl().runAtEntityLater(villager, () -> {
|
||||
WrappedVillager wVillager = VillagerOptimizer.getCache().getOrAdd(villager);
|
||||
scheduling.entitySpecificScheduler(villager).runDelayed(() -> {
|
||||
WrappedVillager wVillager = wrapperCache.get(villager, WrappedVillager::new);
|
||||
wVillager.setOptimizationType(wVillager.getOptimizationType());
|
||||
}, 2, TimeUnit.SECONDS);
|
||||
}, null, 40L);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +1,12 @@
|
||||
package me.xginko.villageroptimizer.modules.gameplay;
|
||||
|
||||
import com.tcoded.folialib.impl.ServerImplementation;
|
||||
import com.cryptomorin.xseries.XPotion;
|
||||
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 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;
|
||||
@ -17,37 +17,34 @@ import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.inventory.InventoryCloseEvent;
|
||||
import org.bukkit.event.inventory.InventoryType;
|
||||
import org.bukkit.potion.PotionEffect;
|
||||
import org.bukkit.potion.PotionEffectType;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class LevelOptimizedProfession implements VillagerOptimizerModule, Listener {
|
||||
public class LevelOptimizedProfession extends VillagerOptimizerModule implements Listener {
|
||||
|
||||
private static final PotionEffect SUPER_SLOWNESS = new PotionEffect(
|
||||
XPotion.SLOWNESS.getPotionEffectType(), 120, 120, false, false);
|
||||
|
||||
private final ServerImplementation scheduler;
|
||||
private final VillagerCache villagerCache;
|
||||
private final boolean notify_player;
|
||||
private final long cooldown_millis;
|
||||
|
||||
public LevelOptimizedProfession() {
|
||||
shouldEnable();
|
||||
this.scheduler = VillagerOptimizer.getFoliaLib().getImpl();
|
||||
this.villagerCache = VillagerOptimizer.getCache();
|
||||
Config config = VillagerOptimizer.getConfiguration();
|
||||
config.master().addComment("gameplay.level-optimized-profession", """
|
||||
This is needed to allow optimized villagers to level up.\s
|
||||
Temporarily enables the villagers AI to allow it to level up and then disables it again.""");
|
||||
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("gameplay.level-optimized-profession.level-check-cooldown-seconds", 5, """
|
||||
Cooldown in seconds until the level of a villager will be checked and updated again.\s
|
||||
Recommended to leave as is."""));
|
||||
this.notify_player = config.getBoolean("gameplay.level-optimized-profession.notify-player", true,
|
||||
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() {
|
||||
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
|
||||
plugin.getServer().getPluginManager().registerEvents(this, plugin);
|
||||
}
|
||||
|
||||
@ -61,35 +58,36 @@ public class LevelOptimizedProfession implements VillagerOptimizerModule, Listen
|
||||
return true;
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
private void onTradeScreenClose(InventoryCloseEvent event) {
|
||||
if (
|
||||
event.getInventory().getType().equals(InventoryType.MERCHANT)
|
||||
&& event.getInventory().getHolder() instanceof Villager villager
|
||||
event.getInventory().getType() == InventoryType.MERCHANT
|
||||
&& event.getInventory().getHolder() instanceof Villager
|
||||
) {
|
||||
WrappedVillager wVillager = villagerCache.getOrAdd(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()) {
|
||||
scheduler.runAtEntity(villager, enableAI -> {
|
||||
villager.addPotionEffect(new PotionEffect(PotionEffectType.SLOW, 120, 120, false, false));
|
||||
villager.setAware(true);
|
||||
if (wVillager.calculateLevel() <= villager.getVillagerLevel()) return;
|
||||
|
||||
scheduler.runAtEntityLater(villager, disableAI -> {
|
||||
villager.setAware(false);
|
||||
wVillager.saveLastLevelUp();
|
||||
}, 5, TimeUnit.SECONDS);
|
||||
});
|
||||
}
|
||||
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(CommonUtil.formatDuration(Duration.ofMillis(wVillager.getLevelCooldownMillis(cooldown_millis))))
|
||||
.replacement(Util.formatDuration(Duration.ofMillis(wVillager.getLevelCooldownMillis(cooldown_millis))))
|
||||
.build();
|
||||
VillagerOptimizer.getLang(player.locale()).villager_leveling_up.forEach(line -> player.sendMessage(line.replaceText(timeLeft)));
|
||||
VillagerOptimizer.getLang(player.locale()).villager_leveling_up
|
||||
.forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(timeLeft)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,7 @@
|
||||
package me.xginko.villageroptimizer.modules.gameplay;
|
||||
|
||||
import me.xginko.villageroptimizer.VillagerOptimizer;
|
||||
import com.cryptomorin.xseries.XEntityType;
|
||||
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;
|
||||
@ -10,13 +9,19 @@ import org.bukkit.event.HandlerList;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.entity.CreatureSpawnEvent;
|
||||
|
||||
public class MakeVillagersSpawnAdult implements VillagerOptimizerModule, Listener {
|
||||
public class MakeVillagersSpawnAdult extends VillagerOptimizerModule implements Listener {
|
||||
|
||||
public MakeVillagersSpawnAdult() {}
|
||||
public MakeVillagersSpawnAdult() {
|
||||
super("gameplay.villagers-spawn-as-adults");
|
||||
config.master().addComment(configPath + ".enable",
|
||||
"Spawned villagers will immediately be adults.\n" +
|
||||
"This is to save some more resources as players don't have to keep unoptimized\n" +
|
||||
"villagers loaded because they have to wait for them to turn into adults before they can\n" +
|
||||
"optimize them.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enable() {
|
||||
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
|
||||
plugin.getServer().getPluginManager().registerEvents(this, plugin);
|
||||
}
|
||||
|
||||
@ -27,17 +32,13 @@ public class MakeVillagersSpawnAdult implements VillagerOptimizerModule, Listene
|
||||
|
||||
@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.""");
|
||||
return config.getBoolean(configPath + ".enable", false);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
private void onVillagerSpawn(CreatureSpawnEvent event) {
|
||||
if (event.getEntityType() == EntityType.VILLAGER) {
|
||||
Villager villager = (Villager) event.getEntity();
|
||||
if (event.getEntityType() == XEntityType.VILLAGER.get()) {
|
||||
final Villager villager = (Villager) event.getEntity();
|
||||
if (!villager.isAdult()) villager.setAdult();
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,8 @@
|
||||
package me.xginko.villageroptimizer.modules.gameplay;
|
||||
|
||||
import com.destroystokyo.paper.event.entity.EntityKnockbackByEntityEvent;
|
||||
import me.xginko.villageroptimizer.VillagerCache;
|
||||
import me.xginko.villageroptimizer.VillagerOptimizer;
|
||||
import me.xginko.villageroptimizer.config.Config;
|
||||
import com.cryptomorin.xseries.XEntityType;
|
||||
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import me.xginko.villageroptimizer.wrapper.WrappedVillager;
|
||||
import org.bukkit.entity.Villager;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
@ -14,45 +11,43 @@ import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.entity.EntityDamageEvent;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class PreventOptimizedDamage implements VillagerOptimizerModule, Listener {
|
||||
public class PreventOptimizedDamage extends VillagerOptimizerModule implements Listener {
|
||||
|
||||
private final VillagerCache villagerCache;
|
||||
private final Set<EntityDamageEvent.DamageCause> damage_causes_to_cancel;
|
||||
private final boolean cancelKnockback;
|
||||
private final boolean cancel_knockback;
|
||||
|
||||
public PreventOptimizedDamage() {
|
||||
shouldEnable();
|
||||
this.villagerCache = VillagerOptimizer.getCache();
|
||||
Config config = VillagerOptimizer.getConfiguration();
|
||||
config.master().addComment("gameplay.prevent-damage-to-optimized.enable",
|
||||
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.cancelKnockback = config.getBoolean("gameplay.prevent-damage-to-optimized.prevent-knockback-from-entity", true,
|
||||
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("gameplay.prevent-damage-to-optimized.damage-causes-to-cancel",
|
||||
Arrays.stream(EntityDamageEvent.DamageCause.values()).map(Enum::name).sorted().toList(), """
|
||||
These are all current entries in the game. Remove what you do not need blocked.\s
|
||||
If you want a description or need to add a previously removed type, refer to:\s
|
||||
https://jd.papermc.io/paper/1.20/org/bukkit/event/entity/EntityDamageEvent.DamageCause.html"""
|
||||
).stream().map(configuredDamageCause -> {
|
||||
try {
|
||||
return EntityDamageEvent.DamageCause.valueOf(configuredDamageCause);
|
||||
} catch (IllegalArgumentException e) {
|
||||
VillagerOptimizer.getLog().warn("(prevent-damage-to-optimized) DamageCause '"+configuredDamageCause +
|
||||
"' not recognized. Please use correct DamageCause enums from: " +
|
||||
"https://jd.papermc.io/paper/1.20/org/bukkit/event/entity/EntityDamageEvent.DamageCause.html");
|
||||
return null;
|
||||
}
|
||||
}).filter(Objects::nonNull).collect(Collectors.toCollection(HashSet::new));
|
||||
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() {
|
||||
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
|
||||
plugin.getServer().getPluginManager().registerEvents(this, plugin);
|
||||
}
|
||||
|
||||
@ -63,26 +58,26 @@ public class PreventOptimizedDamage implements VillagerOptimizerModule, Listener
|
||||
|
||||
@Override
|
||||
public boolean shouldEnable() {
|
||||
return VillagerOptimizer.getConfiguration().getBoolean("gameplay.prevent-damage-to-optimized.enable", true);
|
||||
return config.getBoolean(configPath + ".enable", true);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
private void onDamageByEntity(EntityDamageEvent event) {
|
||||
if (
|
||||
event.getEntityType().equals(EntityType.VILLAGER)
|
||||
event.getEntityType() == XEntityType.VILLAGER.get()
|
||||
&& damage_causes_to_cancel.contains(event.getCause())
|
||||
&& villagerCache.getOrAdd((Villager) event.getEntity()).isOptimized()
|
||||
&& wrapperCache.get((Villager) event.getEntity(), WrappedVillager::new).isOptimized()
|
||||
) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
private void onKnockbackByEntity(EntityKnockbackByEntityEvent event) {
|
||||
private void onKnockbackByEntity(com.destroystokyo.paper.event.entity.EntityKnockbackByEntityEvent event) {
|
||||
if (
|
||||
cancelKnockback
|
||||
&& event.getEntityType().equals(EntityType.VILLAGER)
|
||||
&& villagerCache.getOrAdd((Villager) event.getEntity()).isOptimized()
|
||||
cancel_knockback
|
||||
&& event.getEntityType() == XEntityType.VILLAGER.get()
|
||||
&& wrapperCache.get((Villager) event.getEntity(), WrappedVillager::new).isOptimized()
|
||||
) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
|
@ -1,11 +1,9 @@
|
||||
package me.xginko.villageroptimizer.modules.gameplay;
|
||||
|
||||
import com.destroystokyo.paper.event.entity.EntityPathfindEvent;
|
||||
import me.xginko.villageroptimizer.VillagerCache;
|
||||
import me.xginko.villageroptimizer.VillagerOptimizer;
|
||||
import com.cryptomorin.xseries.XEntityType;
|
||||
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;
|
||||
@ -15,17 +13,16 @@ 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 class PreventOptimizedTargeting extends VillagerOptimizerModule implements Listener {
|
||||
|
||||
public PreventOptimizedTargeting() {
|
||||
this.villagerCache = VillagerOptimizer.getCache();
|
||||
super("gameplay.prevent-entities-from-targeting-optimized");
|
||||
config.master().addComment(configPath + ".enable",
|
||||
"Prevents hostile entities from targeting optimized villagers.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enable() {
|
||||
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
|
||||
plugin.getServer().getPluginManager().registerEvents(this, plugin);
|
||||
}
|
||||
|
||||
@ -36,18 +33,16 @@ public class PreventOptimizedTargeting implements VillagerOptimizerModule, Liste
|
||||
|
||||
@Override
|
||||
public boolean shouldEnable() {
|
||||
return VillagerOptimizer.getConfiguration().getBoolean("gameplay.prevent-entities-from-targeting-optimized.enable", true,
|
||||
"Prevents hostile entities from targeting optimized villagers.");
|
||||
return config.getBoolean(configPath + ".enable", true);
|
||||
}
|
||||
|
||||
@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();
|
||||
final Entity target = event.getTarget();
|
||||
if (
|
||||
target != null
|
||||
&& target.getType() == EntityType.VILLAGER
|
||||
&& villagerCache.getOrAdd((Villager) target).isOptimized()
|
||||
&& target.getType() == XEntityType.VILLAGER.get()
|
||||
&& wrapperCache.get((Villager) target, WrappedVillager::new).isOptimized()
|
||||
) {
|
||||
event.setTarget(null);
|
||||
event.setCancelled(true);
|
||||
@ -55,12 +50,12 @@ public class PreventOptimizedTargeting implements VillagerOptimizerModule, Liste
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
private void onEntityTargetVillager(EntityPathfindEvent event) {
|
||||
Entity target = event.getTargetEntity();
|
||||
private void onEntityTargetVillager(com.destroystokyo.paper.event.entity.EntityPathfindEvent event) {
|
||||
final Entity target = event.getTargetEntity();
|
||||
if (
|
||||
target != null
|
||||
&& target.getType() == EntityType.VILLAGER
|
||||
&& villagerCache.getOrAdd((Villager) target).isOptimized()
|
||||
&& target.getType() == XEntityType.VILLAGER.get()
|
||||
&& wrapperCache.get((Villager) target, WrappedVillager::new).isOptimized()
|
||||
) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
@ -69,11 +64,11 @@ public class PreventOptimizedTargeting implements VillagerOptimizerModule, Liste
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
private void onEntityAttackVillager(EntityDamageByEntityEvent event) {
|
||||
if (
|
||||
event.getEntityType() == EntityType.VILLAGER
|
||||
&& event.getDamager() instanceof Mob attacker
|
||||
&& villagerCache.getOrAdd((Villager) event.getEntity()).isOptimized()
|
||||
event.getEntityType() == XEntityType.VILLAGER.get()
|
||||
&& event.getDamager() instanceof Mob
|
||||
&& wrapperCache.get((Villager) event.getEntity(), WrappedVillager::new).isOptimized()
|
||||
) {
|
||||
attacker.setTarget(null);
|
||||
((Mob) event.getDamager()).setTarget(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
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.Bypass;
|
||||
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;
|
||||
@ -15,26 +15,22 @@ import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.event.inventory.InventoryType;
|
||||
import org.bukkit.event.inventory.TradeSelectEvent;
|
||||
|
||||
public class PreventUnoptimizedTrading implements VillagerOptimizerModule, Listener {
|
||||
public class PreventUnoptimizedTrading extends VillagerOptimizerModule implements Listener {
|
||||
|
||||
private final VillagerCache villagerCache;
|
||||
private final boolean notify_player;
|
||||
|
||||
public PreventUnoptimizedTrading() {
|
||||
shouldEnable();
|
||||
this.villagerCache = VillagerOptimizer.getCache();
|
||||
Config config = VillagerOptimizer.getConfiguration();
|
||||
config.master().addComment("gameplay.prevent-trading-with-unoptimized.enable", """
|
||||
Will prevent players from selecting and using trades of unoptimized villagers.\s
|
||||
Use this if you have a lot of villagers and therefore want to force your players to optimize them.\s
|
||||
Inventories can still be opened so players can move villagers around.""");
|
||||
this.notify_player = config.getBoolean("gameplay.prevent-trading-with-unoptimized.notify-player", true,
|
||||
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() {
|
||||
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
|
||||
plugin.getServer().getPluginManager().registerEvents(this, plugin);
|
||||
}
|
||||
|
||||
@ -45,40 +41,36 @@ public class PreventUnoptimizedTrading implements VillagerOptimizerModule, Liste
|
||||
|
||||
@Override
|
||||
public boolean shouldEnable() {
|
||||
return VillagerOptimizer.getConfiguration().getBoolean("gameplay.prevent-trading-with-unoptimized.enable", false);
|
||||
return config.getBoolean(configPath + ".enable", false);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
private void onTradeOpen(TradeSelectEvent event) {
|
||||
if (event.getWhoClicked().hasPermission(Bypass.TRADE_PREVENTION.get())) return;
|
||||
if (event.getInventory().getType() != InventoryType.MERCHANT) return;
|
||||
if (event.getWhoClicked().hasPermission(Permissions.Bypass.TRADE_PREVENTION.get())) return;
|
||||
if (!(event.getInventory().getHolder() instanceof Villager)) return;
|
||||
if (wrapperCache.get((Villager) event.getInventory().getHolder(), WrappedVillager::new).isOptimized()) return;
|
||||
|
||||
if (
|
||||
event.getInventory().getType().equals(InventoryType.MERCHANT)
|
||||
&& event.getInventory().getHolder() instanceof Villager villager
|
||||
&& !villagerCache.getOrAdd(villager).isOptimized()
|
||||
) {
|
||||
event.setCancelled(true);
|
||||
if (notify_player) {
|
||||
Player player = (Player) event.getWhoClicked();
|
||||
VillagerOptimizer.getLang(player.locale()).optimize_for_trading.forEach(player::sendMessage);
|
||||
}
|
||||
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.NORMAL, ignoreCancelled = true)
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
private void onInventoryClick(InventoryClickEvent event) {
|
||||
if (event.getWhoClicked().hasPermission(Bypass.TRADE_PREVENTION.get())) return;
|
||||
if (event.getInventory().getType() != InventoryType.MERCHANT) return;
|
||||
if (event.getWhoClicked().hasPermission(Permissions.Bypass.TRADE_PREVENTION.get())) return;
|
||||
if (!(event.getInventory().getHolder() instanceof Villager)) return;
|
||||
if (wrapperCache.get((Villager) event.getInventory().getHolder(), WrappedVillager::new).isOptimized()) return;
|
||||
|
||||
if (
|
||||
event.getInventory().getType().equals(InventoryType.MERCHANT)
|
||||
&& event.getInventory().getHolder() instanceof Villager villager
|
||||
&& !villagerCache.getOrAdd(villager).isOptimized()
|
||||
) {
|
||||
event.setCancelled(true);
|
||||
if (notify_player) {
|
||||
Player player = (Player) event.getWhoClicked();
|
||||
VillagerOptimizer.getLang(player.locale()).optimize_for_trading.forEach(player::sendMessage);
|
||||
}
|
||||
event.setCancelled(true);
|
||||
|
||||
if (notify_player) {
|
||||
Player player = (Player) event.getWhoClicked();
|
||||
VillagerOptimizer.getLang(player.locale()).optimize_for_trading.forEach(line -> KyoriUtil.sendMessage(player, line));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,80 +0,0 @@
|
||||
package me.xginko.villageroptimizer.modules.gameplay;
|
||||
|
||||
import com.tcoded.folialib.impl.ServerImplementation;
|
||||
import me.xginko.villageroptimizer.VillagerOptimizer;
|
||||
import me.xginko.villageroptimizer.WrappedVillager;
|
||||
import me.xginko.villageroptimizer.config.Config;
|
||||
import me.xginko.villageroptimizer.events.VillagerOptimizeEvent;
|
||||
import me.xginko.villageroptimizer.events.VillagerUnoptimizeEvent;
|
||||
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import org.bukkit.entity.Villager;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.bukkit.event.Listener;
|
||||
|
||||
public class RenameOptimizedVillagers implements VillagerOptimizerModule, Listener {
|
||||
|
||||
private final ServerImplementation scheduler;
|
||||
private final Component optimized_name;
|
||||
private final boolean overwrite_previous_name;
|
||||
|
||||
public RenameOptimizedVillagers() {
|
||||
shouldEnable();
|
||||
this.scheduler = VillagerOptimizer.getFoliaLib().getImpl();
|
||||
Config config = VillagerOptimizer.getConfiguration();
|
||||
config.master().addComment("gameplay.rename-optimized-villagers.enable", """
|
||||
Will change a villager's name to the name configured below when they are optimized.\s
|
||||
These names will be removed when unoptimized again if they were not changed in the meantime.""");
|
||||
this.optimized_name = MiniMessage.miniMessage().deserialize(config.getString("gameplay.rename-optimized-villagers.optimized-name", "<green>Optimized",
|
||||
"The name that will be used to mark optimized villagers. Uses MiniMessage format."));
|
||||
this.overwrite_previous_name = config.getBoolean("gameplay.rename-optimized-villagers.overwrite-existing-name", false,
|
||||
"If set to true, will rename even if the villager has already been named.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enable() {
|
||||
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
|
||||
plugin.getServer().getPluginManager().registerEvents(this, plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disable() {
|
||||
HandlerList.unregisterAll(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldEnable() {
|
||||
return VillagerOptimizer.getConfiguration().getBoolean("gameplay.rename-optimized-villagers.enable", false);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
private void onOptimize(VillagerOptimizeEvent event) {
|
||||
WrappedVillager wVillager = event.getWrappedVillager();
|
||||
Villager villager = wVillager.villager();
|
||||
|
||||
if (overwrite_previous_name || villager.customName() == null) {
|
||||
scheduler.runAtEntityLater(villager, () -> {
|
||||
villager.customName(optimized_name);
|
||||
wVillager.memorizeName(optimized_name);
|
||||
}, 10L);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
private void onUnOptimize(VillagerUnoptimizeEvent event) {
|
||||
WrappedVillager wVillager = event.getWrappedVillager();
|
||||
Villager villager = wVillager.villager();
|
||||
|
||||
scheduler.runAtEntityLater(villager, () -> {
|
||||
final Component currentName = villager.customName();
|
||||
final Component memorizedName = wVillager.getMemorizedName();
|
||||
if (currentName != null && currentName.equals(memorizedName))
|
||||
villager.customName(null);
|
||||
if (memorizedName != null)
|
||||
wVillager.forgetName();
|
||||
}, 10L);
|
||||
}
|
||||
}
|
@ -1,17 +1,14 @@
|
||||
package me.xginko.villageroptimizer.modules.gameplay;
|
||||
|
||||
import com.cryptomorin.xseries.XEntityType;
|
||||
import me.xginko.villageroptimizer.VillagerOptimizer;
|
||||
import me.xginko.villageroptimizer.VillagerCache;
|
||||
import me.xginko.villageroptimizer.config.Config;
|
||||
import me.xginko.villageroptimizer.enums.permissions.Bypass;
|
||||
import me.xginko.villageroptimizer.WrappedVillager;
|
||||
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
|
||||
import me.xginko.villageroptimizer.utils.CommonUtil;
|
||||
import net.kyori.adventure.text.Component;
|
||||
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.Location;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.entity.Villager;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
@ -20,30 +17,32 @@ 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 implements VillagerOptimizerModule, Listener {
|
||||
public class RestockOptimizedTrades extends VillagerOptimizerModule implements Listener {
|
||||
|
||||
private final VillagerCache villagerCache;
|
||||
private final long restock_delay_millis;
|
||||
private final SortedSet<Long> restockDayTimes;
|
||||
private final boolean log_enabled, notify_player;
|
||||
|
||||
public RestockOptimizedTrades() {
|
||||
shouldEnable();
|
||||
this.villagerCache = VillagerOptimizer.getCache();
|
||||
Config config = VillagerOptimizer.getConfiguration();
|
||||
config.master().addComment("gameplay.restock-optimized-trades", """
|
||||
This is for automatic restocking of trades for optimized villagers. Optimized Villagers\s
|
||||
don't have enough AI to restock their trades naturally, so this is here as a workaround.""");
|
||||
this.restock_delay_millis = config.getInt("gameplay.restock-optimized-trades.delay-in-ticks", 1000,
|
||||
"1 second = 20 ticks. There are 24.000 ticks in a single minecraft day.") * 50L;
|
||||
this.notify_player = config.getBoolean("gameplay.restock-optimized-trades.notify-player", true,
|
||||
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("gameplay.restock-optimized-trades.log", false);
|
||||
this.log_enabled = config.getBoolean(configPath + ".log", false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enable() {
|
||||
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
|
||||
plugin.getServer().getPluginManager().registerEvents(this, plugin);
|
||||
}
|
||||
|
||||
@ -57,32 +56,55 @@ public class RestockOptimizedTrades implements VillagerOptimizerModule, Listener
|
||||
return true;
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
|
||||
private void onInteract(PlayerInteractEntityEvent event) {
|
||||
if (!event.getRightClicked().getType().equals(EntityType.VILLAGER)) return;
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
private void onPlayerInteractEntity(PlayerInteractEntityEvent event) {
|
||||
if (event.getRightClicked().getType() != XEntityType.VILLAGER.get()) return;
|
||||
|
||||
WrappedVillager wVillager = villagerCache.getOrAdd((Villager) event.getRightClicked());
|
||||
if (!wVillager.isOptimized()) return;
|
||||
Player player = event.getPlayer();
|
||||
WrappedVillager wrapped = wrapperCache.get((Villager) event.getRightClicked(), WrappedVillager::new);
|
||||
if (!wrapped.isOptimized()) return;
|
||||
|
||||
final boolean player_bypassing = player.hasPermission(Bypass.RESTOCK_COOLDOWN.get());
|
||||
if (event.getPlayer().hasPermission(Permissions.Bypass.RESTOCK_COOLDOWN.get())) {
|
||||
wrapped.restock();
|
||||
return;
|
||||
}
|
||||
|
||||
if (wVillager.canRestock(restock_delay_millis) || player_bypassing) {
|
||||
wVillager.restock();
|
||||
wVillager.saveRestockTime();
|
||||
if (notify_player && !player_bypassing) {
|
||||
final TextReplacementConfig timeLeft = TextReplacementConfig.builder()
|
||||
.matchLiteral("%time%")
|
||||
.replacement(CommonUtil.formatDuration(Duration.ofMillis(wVillager.getRestockCooldownMillis(restock_delay_millis))))
|
||||
.build();
|
||||
VillagerOptimizer.getLang(player.locale()).trades_restocked.forEach(line -> player.sendMessage(line.replaceText(timeLeft)));
|
||||
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 (log_enabled) {
|
||||
final Location location = wVillager.villager().getLocation();
|
||||
VillagerOptimizer.getLog().info(Component.text("Restocked optimized villager at " +
|
||||
"x=" + location.getBlockX() + ", y=" + location.getBlockY() + ", z=" + location.getBlockZ() +
|
||||
", world=" + location.getWorld().getName()).style(VillagerOptimizer.plugin_style));
|
||||
|
||||
if (!restocked) {
|
||||
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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,27 +1,24 @@
|
||||
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.enums.OptimizationType;
|
||||
import me.xginko.villageroptimizer.struct.enums.OptimizationType;
|
||||
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
|
||||
import me.xginko.villageroptimizer.wrapper.WrappedVillager;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.entity.VillagerCareerChangeEvent;
|
||||
|
||||
public class UnoptimizeOnJobLoose implements VillagerOptimizerModule, Listener {
|
||||
|
||||
private final VillagerCache villagerCache;
|
||||
public class UnoptimizeOnJobLoose extends VillagerOptimizerModule implements Listener {
|
||||
|
||||
public UnoptimizeOnJobLoose() {
|
||||
this.villagerCache = VillagerOptimizer.getCache();
|
||||
super("gameplay.unoptimize-on-job-loose");
|
||||
config.master().addComment(configPath + ".enable",
|
||||
"Villagers that get their jobs reset will become unoptimized again.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enable() {
|
||||
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
|
||||
plugin.getServer().getPluginManager().registerEvents(this, plugin);
|
||||
}
|
||||
|
||||
@ -32,18 +29,15 @@ public class UnoptimizeOnJobLoose implements VillagerOptimizerModule, Listener {
|
||||
|
||||
@Override
|
||||
public boolean shouldEnable() {
|
||||
return VillagerOptimizer.getConfiguration().getBoolean("gameplay.unoptimize-on-job-loose.enable", true,
|
||||
"Villagers that get their jobs reset will become unoptimized again.");
|
||||
return config.getBoolean(configPath + ".enable", true);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
private void onJobReset(VillagerCareerChangeEvent event) {
|
||||
if (!event.getReason().equals(VillagerCareerChangeEvent.ChangeReason.LOSING_JOB)) return;
|
||||
|
||||
WrappedVillager wrappedVillager = villagerCache.getOrAdd(event.getEntity());
|
||||
|
||||
if (wrappedVillager.isOptimized()) {
|
||||
wrappedVillager.setOptimizationType(OptimizationType.NONE);
|
||||
if (event.getReason() != VillagerCareerChangeEvent.ChangeReason.LOSING_JOB) return;
|
||||
final WrappedVillager wrapped = wrapperCache.get(event.getEntity(), WrappedVillager::new);
|
||||
if (wrapped.isOptimized()) {
|
||||
wrapped.setOptimizationType(OptimizationType.NONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,50 @@
|
||||
package me.xginko.villageroptimizer.modules.gameplay;
|
||||
|
||||
import me.xginko.villageroptimizer.events.VillagerOptimizeEvent;
|
||||
import me.xginko.villageroptimizer.events.VillagerUnoptimizeEvent;
|
||||
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
|
||||
import org.bukkit.entity.Villager;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.bukkit.event.Listener;
|
||||
|
||||
public class VisuallyHighlightOptimized extends VillagerOptimizerModule implements Listener {
|
||||
|
||||
public VisuallyHighlightOptimized() {
|
||||
super("gameplay.outline-optimized-villagers");
|
||||
config.master().addComment("gameplay.outline-optimized-villagers.enable",
|
||||
"Will make optimized villagers glow.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enable() {
|
||||
plugin.getServer().getPluginManager().registerEvents(this, plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disable() {
|
||||
HandlerList.unregisterAll(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldEnable() {
|
||||
return config.getBoolean("gameplay.outline-optimized-villagers.enable", false);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
private void onOptimize(VillagerOptimizeEvent event) {
|
||||
Villager villager = event.getWrappedVillager().villager;
|
||||
scheduling.entitySpecificScheduler(villager).run(glow -> {
|
||||
if (!villager.isGlowing()) villager.setGlowing(true);
|
||||
}, null);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
private void onUnOptimize(VillagerUnoptimizeEvent event) {
|
||||
Villager villager = event.getWrappedVillager().villager;
|
||||
scheduling.entitySpecificScheduler(villager).run(unGlow -> {
|
||||
if (villager.isGlowing()) villager.setGlowing(false);
|
||||
}, null);
|
||||
}
|
||||
}
|
@ -0,0 +1,240 @@
|
||||
package me.xginko.villageroptimizer.modules.optimization;
|
||||
|
||||
import com.cryptomorin.xseries.XEntityType;
|
||||
import com.destroystokyo.paper.event.entity.EntityPathfindEvent;
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import me.xginko.villageroptimizer.VillagerOptimizer;
|
||||
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
|
||||
import me.xginko.villageroptimizer.struct.enums.OptimizationType;
|
||||
import me.xginko.villageroptimizer.struct.models.BlockRegion2D;
|
||||
import me.xginko.villageroptimizer.wrapper.WrappedVillager;
|
||||
import net.kyori.adventure.text.TextReplacementConfig;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.entity.Villager;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.entity.EntityInteractEvent;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class OptimizeByActivity extends VillagerOptimizerModule implements Listener {
|
||||
|
||||
protected static class RegionData {
|
||||
|
||||
public final BlockRegion2D region;
|
||||
public final AtomicInteger pathfindCount, entityInteractCount;
|
||||
public final AtomicBoolean regionBusy;
|
||||
|
||||
public RegionData(BlockRegion2D region) {
|
||||
this.region = region;
|
||||
this.pathfindCount = new AtomicInteger();
|
||||
this.entityInteractCount = new AtomicInteger();
|
||||
this.regionBusy = new AtomicBoolean(false);
|
||||
}
|
||||
}
|
||||
|
||||
private final Cache<BlockRegion2D, RegionData> regionDataCache;
|
||||
private final double checkRadius;
|
||||
private final int pathfindLimit, entityInteractLimit;
|
||||
private final boolean notifyPlayers, doLogging;
|
||||
|
||||
public OptimizeByActivity() {
|
||||
super("optimization-methods.regional-activity");
|
||||
config.master().addComment(configPath + ".enable",
|
||||
"Enable optimization by naming villagers to one of the names configured below.\n" +
|
||||
"Nametag optimized villagers will be unoptimized again when they are renamed to something else.");
|
||||
|
||||
this.checkRadius = config.getDouble(configPath + ".check-radius-blocks", 500.0,
|
||||
"The radius in blocks in which activity will be grouped together and measured.");
|
||||
this.regionDataCache = Caffeine.newBuilder().expireAfterWrite(Duration.ofMillis(
|
||||
config.getInt(configPath + ".data-keep-time-millis", 10000,
|
||||
"The time in milliseconds before a region and its data will be expired\n" +
|
||||
"if no activity has been detected.\n" +
|
||||
"For proper functionality, needs to be at least as long as your pause time."))).build();
|
||||
|
||||
this.pathfindLimit = config.getInt(configPath + ".limits.pathfind-event", 150);
|
||||
this.entityInteractLimit = config.getInt(configPath + ".limits.interact-event", 50);
|
||||
|
||||
this.notifyPlayers = config.getBoolean(configPath + ".notify-players", true,
|
||||
"Sends players a message to any player near an auto-optimized villager.");
|
||||
this.doLogging = config.getBoolean(configPath + ".log", false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enable() {
|
||||
plugin.getServer().getPluginManager().registerEvents(this, plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disable() {
|
||||
HandlerList.unregisterAll(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldEnable() {
|
||||
return config.getBoolean(configPath + ".enable", false);
|
||||
}
|
||||
|
||||
private @NotNull RegionData getRegionData(Location location) {
|
||||
return regionDataCache.get(getRegion(location), RegionData::new);
|
||||
}
|
||||
|
||||
private @NotNull BlockRegion2D getRegion(Location location) {
|
||||
// Find and return region containing this location
|
||||
for (Map.Entry<BlockRegion2D, RegionData> regionDataEntry : regionDataCache.asMap().entrySet()) {
|
||||
if (regionDataEntry.getKey().contains(location)) {
|
||||
return regionDataEntry.getKey();
|
||||
}
|
||||
}
|
||||
|
||||
// Create and cache region if none exists
|
||||
BlockRegion2D region = BlockRegion2D.of(location.getWorld(), location.getX(), location.getZ(), checkRadius);
|
||||
regionDataCache.put(region, new RegionData(region));
|
||||
return region;
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
private void onEntityPathfind(EntityPathfindEvent event) {
|
||||
if (event.getEntityType() != XEntityType.VILLAGER.get()) return;
|
||||
|
||||
Location location = event.getEntity().getLocation();
|
||||
BlockRegion2D region2D = getRegion(location);
|
||||
RegionData regionData = getRegionData(location);
|
||||
|
||||
if (regionData.regionBusy.get() || regionData.pathfindCount.incrementAndGet() <= pathfindLimit) {
|
||||
return;
|
||||
}
|
||||
|
||||
regionData.regionBusy.set(true);
|
||||
|
||||
AtomicInteger optimizeCount = new AtomicInteger();
|
||||
Set<Player> playersWithinArea = new CopyOnWriteArraySet<>();
|
||||
|
||||
region2D.getEntities()
|
||||
.thenAccept(entities -> {
|
||||
for (Entity entity : entities) {
|
||||
scheduling.entitySpecificScheduler(entity).run(() -> {
|
||||
if (entity.getType() == XEntityType.VILLAGER.get()) {
|
||||
WrappedVillager wrappedVillager = wrapperCache.get((Villager) entity, WrappedVillager::new);
|
||||
|
||||
if (wrappedVillager.isOptimized()) {
|
||||
return;
|
||||
}
|
||||
|
||||
wrappedVillager.setOptimizationType(OptimizationType.REGIONAL_ACTIVITY);
|
||||
optimizeCount.incrementAndGet();
|
||||
}
|
||||
|
||||
if (notifyPlayers && entity.getType() == XEntityType.PLAYER.get()) {
|
||||
playersWithinArea.add((Player) entity);
|
||||
}
|
||||
}, null);
|
||||
}
|
||||
})
|
||||
.thenRun(() -> {
|
||||
if (notifyPlayers) {
|
||||
TextReplacementConfig amount = TextReplacementConfig.builder()
|
||||
.matchLiteral("%amount%")
|
||||
.replacement(optimizeCount.toString())
|
||||
.build();
|
||||
|
||||
for (Player player : playersWithinArea) {
|
||||
VillagerOptimizer.scheduling().entitySpecificScheduler(player).run(() ->
|
||||
VillagerOptimizer.getLang(player.locale()).activity_optimize_success
|
||||
.forEach(line -> player.sendMessage(line.replaceText(amount))),
|
||||
null);
|
||||
}
|
||||
|
||||
playersWithinArea.clear();
|
||||
}
|
||||
|
||||
if (doLogging) {
|
||||
info( "Optimized " + optimizeCount.get() + " villagers in a radius of " + checkRadius +
|
||||
" blocks from center at x=" + regionData.region.getCenterX() + ", z=" + regionData.region.getCenterZ() +
|
||||
" in world " + location.getWorld().getName() +
|
||||
"because of too high pathfinding activity within the configured timeframe: " +
|
||||
regionData.pathfindCount + " (limit: " + pathfindLimit + ")");
|
||||
}
|
||||
|
||||
regionDataCache.invalidate(region2D);
|
||||
});
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
private void onEntityInteract(EntityInteractEvent event) {
|
||||
if (event.getEntityType() != XEntityType.VILLAGER.get()) return;
|
||||
|
||||
Location location = event.getEntity().getLocation();
|
||||
BlockRegion2D region2D = getRegion(location);
|
||||
RegionData regionData = getRegionData(location);
|
||||
|
||||
if (regionData.regionBusy.get() || regionData.entityInteractCount.incrementAndGet() <= entityInteractLimit) {
|
||||
return;
|
||||
}
|
||||
|
||||
regionData.regionBusy.set(true);
|
||||
|
||||
AtomicInteger optimizeCount = new AtomicInteger();
|
||||
Set<Player> playersWithinArea = new CopyOnWriteArraySet<>();
|
||||
|
||||
region2D.getEntities()
|
||||
.thenAccept(entities -> {
|
||||
for (Entity entity : entities) {
|
||||
scheduling.entitySpecificScheduler(entity).run(() -> {
|
||||
if (entity.getType() == XEntityType.VILLAGER.get()) {
|
||||
WrappedVillager wrappedVillager = wrapperCache.get((Villager) entity, WrappedVillager::new);
|
||||
|
||||
if (wrappedVillager.isOptimized()) {
|
||||
return;
|
||||
}
|
||||
|
||||
wrappedVillager.setOptimizationType(OptimizationType.REGIONAL_ACTIVITY);
|
||||
optimizeCount.incrementAndGet();
|
||||
}
|
||||
|
||||
if (notifyPlayers && entity.getType() == XEntityType.PLAYER.get()) {
|
||||
playersWithinArea.add((Player) entity);
|
||||
}
|
||||
}, null);
|
||||
}
|
||||
})
|
||||
.thenRun(() -> {
|
||||
if (notifyPlayers) {
|
||||
TextReplacementConfig amount = TextReplacementConfig.builder()
|
||||
.matchLiteral("%amount%")
|
||||
.replacement(optimizeCount.toString())
|
||||
.build();
|
||||
|
||||
for (Player player : playersWithinArea) {
|
||||
VillagerOptimizer.scheduling().entitySpecificScheduler(player).run(() ->
|
||||
VillagerOptimizer.getLang(player.locale()).activity_optimize_success
|
||||
.forEach(line -> player.sendMessage(line.replaceText(amount))),
|
||||
null);
|
||||
}
|
||||
|
||||
playersWithinArea.clear();
|
||||
}
|
||||
|
||||
if (doLogging) {
|
||||
info( "Optimized " + optimizeCount.get() + " villagers in a radius of " + checkRadius +
|
||||
" blocks from center at x=" + regionData.region.getCenterX() + ", z=" + regionData.region.getCenterZ() +
|
||||
" in world " + location.getWorld().getName() +
|
||||
"because of too many villagers interacting with objects within the configured timeframe: " +
|
||||
regionData.pathfindCount + " (limit: " + pathfindLimit + ")");
|
||||
}
|
||||
|
||||
regionDataCache.invalidate(region2D);
|
||||
});
|
||||
}
|
||||
}
|
@ -1,23 +1,20 @@
|
||||
package me.xginko.villageroptimizer.modules.optimization;
|
||||
|
||||
import me.xginko.villageroptimizer.VillagerCache;
|
||||
import com.cryptomorin.xseries.XMaterial;
|
||||
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.Bypass;
|
||||
import me.xginko.villageroptimizer.enums.permissions.Optimize;
|
||||
import me.xginko.villageroptimizer.struct.enums.OptimizationType;
|
||||
import me.xginko.villageroptimizer.struct.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 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.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;
|
||||
@ -28,58 +25,60 @@ import org.bukkit.event.block.BlockBreakEvent;
|
||||
import org.bukkit.event.block.BlockPlaceEvent;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.HashSet;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class OptimizeByBlock implements VillagerOptimizerModule, Listener {
|
||||
public class OptimizeByBlock extends VillagerOptimizerModule implements Listener {
|
||||
|
||||
private final VillagerCache villagerCache;
|
||||
private final Set<Material> blocks_that_disable;
|
||||
private final long cooldown_millis;
|
||||
private final double search_radius;
|
||||
private final boolean only_while_sneaking, notify_player, log_enabled;
|
||||
|
||||
public OptimizeByBlock() {
|
||||
shouldEnable();
|
||||
this.villagerCache = VillagerOptimizer.getCache();
|
||||
Config config = VillagerOptimizer.getConfiguration();
|
||||
config.master().addComment("optimization-methods.block-optimization.enable", """
|
||||
When enabled, the closest villager standing near a configured block being placed will be optimized.\s
|
||||
If a configured block is broken nearby, the closest villager will become unoptimized again.""");
|
||||
this.blocks_that_disable = config.getList("optimization-methods.block-optimization.materials", List.of(
|
||||
"LAPIS_BLOCK", "GLOWSTONE", "IRON_BLOCK"
|
||||
), "Values here need to be valid bukkit Material enums for your server version."
|
||||
).stream().map(configuredMaterial -> {
|
||||
try {
|
||||
return Material.valueOf(configuredMaterial);
|
||||
} catch (IllegalArgumentException e) {
|
||||
VillagerOptimizer.getLog().warn("(block-optimization) Material '"+configuredMaterial +
|
||||
"' not recognized. Please use correct Material enums from: " +
|
||||
"https://jd.papermc.io/paper/1.20/org/bukkit/Material.html");
|
||||
return null;
|
||||
}
|
||||
}).filter(Objects::nonNull).collect(Collectors.toCollection(HashSet::new));
|
||||
super("optimization-methods.block-optimization");
|
||||
config.master().addComment(configPath + ".enable",
|
||||
"When enabled, the closest villager standing near a configured block being placed will be optimized.\n" +
|
||||
"If a configured block is broken nearby, the closest villager will become unoptimized again.");
|
||||
List<String> defaults = Stream.of(XMaterial.LAPIS_BLOCK, XMaterial.GLOWSTONE, XMaterial.IRON_BLOCK)
|
||||
.filter(XMaterial::isSupported)
|
||||
.map(Enum::name)
|
||||
.collect(Collectors.toList());
|
||||
this.blocks_that_disable = config.getList(configPath + ".materials", defaults,
|
||||
"Values here need to be valid bukkit Material enums for your server version.")
|
||||
.stream()
|
||||
.map(configuredMaterial -> {
|
||||
try {
|
||||
return Material.valueOf(configuredMaterial);
|
||||
} catch (IllegalArgumentException e) {
|
||||
warn("Material '" + configuredMaterial + "' not recognized. Please use correct Material enums from: " +
|
||||
"https://jd.papermc.io/paper/1.20/org/bukkit/Material.html");
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toCollection(() -> EnumSet.noneOf(Material.class)));
|
||||
this.cooldown_millis = TimeUnit.SECONDS.toMillis(
|
||||
config.getInt("optimization-methods.block-optimization.optimize-cooldown-seconds", 600, """
|
||||
Cooldown in seconds until a villager can be optimized again by using specific blocks.\s
|
||||
Here for configuration freedom. Recommended to leave as is to not enable any exploitable behavior."""));
|
||||
this.search_radius = config.getDouble("optimization-methods.block-optimization.search-radius-in-blocks", 2.0, """
|
||||
The radius in blocks a villager can be away from the player when he places an optimize block.\s
|
||||
The closest unoptimized villager to the player will be optimized.""") / 2;
|
||||
this.only_while_sneaking = config.getBoolean("optimization-methods.block-optimization.only-when-sneaking", true,
|
||||
"Only optimize/unoptimize by workstation when player is sneaking during place or break.");
|
||||
this.notify_player = config.getBoolean("optimization-methods.block-optimization.notify-player", true,
|
||||
config.getInt(configPath + ".optimize-cooldown-seconds", 600,
|
||||
"Cooldown in seconds until a villager can be optimized again by using specific blocks.\n" +
|
||||
"Here for configuration freedom. Recommended to leave as is to not enable any exploitable behavior."));
|
||||
this.search_radius = config.getDouble(configPath + ".search-radius-in-blocks", 2.0,
|
||||
"The radius in blocks a villager can be away from the player when he places an optimize block.\n" +
|
||||
"The closest unoptimized villager to the player will be optimized.") / 2;
|
||||
this.only_while_sneaking = config.getBoolean(configPath + ".only-when-sneaking", true,
|
||||
"Only optimize/unoptimize by block when player is sneaking during place or break.");
|
||||
this.notify_player = config.getBoolean(configPath + ".notify-player", true,
|
||||
"Sends players a message when they successfully optimized or unoptimized a villager.");
|
||||
this.log_enabled = config.getBoolean("optimization-methods.block-optimization.log", false);
|
||||
this.log_enabled = config.getBoolean(configPath + ".log", false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enable() {
|
||||
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
|
||||
plugin.getServer().getPluginManager().registerEvents(this, plugin);
|
||||
}
|
||||
|
||||
@ -90,31 +89,30 @@ public class OptimizeByBlock implements VillagerOptimizerModule, Listener {
|
||||
|
||||
@Override
|
||||
public boolean shouldEnable() {
|
||||
return VillagerOptimizer.getConfiguration().getBoolean("optimization-methods.block-optimization.enable", false);
|
||||
return config.getBoolean(configPath + ".enable", false);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
private void onBlockPlace(BlockPlaceEvent event) {
|
||||
Block placed = event.getBlock();
|
||||
final Block placed = event.getBlock();
|
||||
if (!blocks_that_disable.contains(placed.getType())) return;
|
||||
Player player = event.getPlayer();
|
||||
if (!player.hasPermission(Optimize.BLOCK.get())) return;
|
||||
final Player player = event.getPlayer();
|
||||
if (!player.hasPermission(Permissions.Optimize.BLOCK.get())) return;
|
||||
if (only_while_sneaking && !player.isSneaking()) return;
|
||||
|
||||
final Location blockLoc = placed.getLocation().toCenterLocation();
|
||||
WrappedVillager closestOptimizableVillager = null;
|
||||
double closestDistance = Double.MAX_VALUE;
|
||||
|
||||
for (Entity entity : blockLoc.getNearbyEntities(search_radius, search_radius, search_radius)) {
|
||||
if (!entity.getType().equals(EntityType.VILLAGER)) continue;
|
||||
Villager villager = (Villager) entity;
|
||||
for (Villager villager : blockLoc.getNearbyEntitiesByType(Villager.class, search_radius)) {
|
||||
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().distanceSquared(blockLoc);
|
||||
final double distance = LocationUtil.relDistance3DSquared(villager.getLocation(), blockLoc);
|
||||
if (distance >= closestDistance) continue;
|
||||
|
||||
if (distance < closestDistance && wVillager.canOptimize(cooldown_millis)) {
|
||||
final WrappedVillager wVillager = wrapperCache.get(villager, WrappedVillager::new);
|
||||
if (wVillager.canOptimize(cooldown_millis)) {
|
||||
closestOptimizableVillager = wVillager;
|
||||
closestDistance = distance;
|
||||
}
|
||||
@ -122,64 +120,66 @@ public class OptimizeByBlock implements VillagerOptimizerModule, Listener {
|
||||
|
||||
if (closestOptimizableVillager == null) return;
|
||||
|
||||
if (closestOptimizableVillager.canOptimize(cooldown_millis) || player.hasPermission(Bypass.BLOCK_COOLDOWN.get())) {
|
||||
VillagerOptimizeEvent optimizeEvent = new VillagerOptimizeEvent(closestOptimizableVillager, OptimizationType.BLOCK, player, event.isAsynchronous());
|
||||
if (!optimizeEvent.callEvent()) return;
|
||||
if (closestOptimizableVillager.canOptimize(cooldown_millis) || player.hasPermission(Permissions.Bypass.BLOCK_COOLDOWN.get())) {
|
||||
VillagerOptimizeEvent optimizeEvent = new VillagerOptimizeEvent(
|
||||
closestOptimizableVillager,
|
||||
OptimizationType.BLOCK,
|
||||
player,
|
||||
event.isAsynchronous()
|
||||
);
|
||||
|
||||
if (!optimizeEvent.callEvent()) return;
|
||||
closestOptimizableVillager.setOptimizationType(optimizeEvent.getOptimizationType());
|
||||
closestOptimizableVillager.saveOptimizeTime();
|
||||
|
||||
if (notify_player) {
|
||||
final TextReplacementConfig vilProfession = TextReplacementConfig.builder()
|
||||
.matchLiteral("%vil_profession%")
|
||||
.replacement(closestOptimizableVillager.villager().getProfession().toString().toLowerCase())
|
||||
.replacement(Util.toNiceString(closestOptimizableVillager.villager.getProfession()))
|
||||
.build();
|
||||
final TextReplacementConfig placedMaterial = TextReplacementConfig.builder()
|
||||
.matchLiteral("%blocktype%")
|
||||
.replacement(placed.getType().toString().toLowerCase())
|
||||
.replacement(Util.toNiceString(placed.getType()))
|
||||
.build();
|
||||
VillagerOptimizer.getLang(player.locale()).block_optimize_success.forEach(line -> player.sendMessage(line
|
||||
.replaceText(vilProfession)
|
||||
.replaceText(placedMaterial)
|
||||
));
|
||||
VillagerOptimizer.getLang(player.locale()).block_optimize_success
|
||||
.forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(vilProfession).replaceText(placedMaterial)));
|
||||
}
|
||||
|
||||
if (log_enabled) {
|
||||
VillagerOptimizer.getLog().info(Component.text(player.getName() + " optimized villager by block at " +
|
||||
CommonUtil.formatLocation(closestOptimizableVillager.villager().getLocation())).color(VillagerOptimizer.plugin_style.color()));
|
||||
info(player.getName() + " optimized villager at " +
|
||||
LocationUtil.toString(closestOptimizableVillager.villager.getLocation()));
|
||||
}
|
||||
} else {
|
||||
CommonUtil.shakeHead(closestOptimizableVillager.villager());
|
||||
closestOptimizableVillager.sayNo();
|
||||
if (notify_player) {
|
||||
final TextReplacementConfig timeLeft = TextReplacementConfig.builder()
|
||||
.matchLiteral("%time%")
|
||||
.replacement(CommonUtil.formatDuration(Duration.ofMillis(closestOptimizableVillager.getOptimizeCooldownMillis(cooldown_millis))))
|
||||
.replacement(Util.formatDuration(Duration.ofMillis(closestOptimizableVillager.getOptimizeCooldownMillis(cooldown_millis))))
|
||||
.build();
|
||||
VillagerOptimizer.getLang(player.locale()).block_on_optimize_cooldown.forEach(line -> player.sendMessage(line.replaceText(timeLeft)));
|
||||
VillagerOptimizer.getLang(player.locale()).block_on_optimize_cooldown
|
||||
.forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(timeLeft)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
private void onBlockBreak(BlockBreakEvent event) {
|
||||
Block broken = event.getBlock();
|
||||
final Block broken = event.getBlock();
|
||||
if (!blocks_that_disable.contains(broken.getType())) return;
|
||||
Player player = event.getPlayer();
|
||||
if (!player.hasPermission(Optimize.BLOCK.get())) return;
|
||||
final Player player = event.getPlayer();
|
||||
if (!player.hasPermission(Permissions.Optimize.BLOCK.get())) return;
|
||||
if (only_while_sneaking && !player.isSneaking()) return;
|
||||
|
||||
final Location blockLoc = broken.getLocation().toCenterLocation();
|
||||
WrappedVillager closestOptimizedVillager = null;
|
||||
double closestDistance = Double.MAX_VALUE;
|
||||
|
||||
for (Entity entity : blockLoc.getNearbyEntities(search_radius, search_radius, search_radius)) {
|
||||
if (!entity.getType().equals(EntityType.VILLAGER)) continue;
|
||||
Villager villager = (Villager) entity;
|
||||
for (Villager villager : blockLoc.getNearbyEntitiesByType(Villager.class, search_radius)) {
|
||||
final double distance = LocationUtil.relDistance3DSquared(villager.getLocation(), blockLoc);
|
||||
if (distance >= closestDistance) continue;
|
||||
|
||||
WrappedVillager wVillager = villagerCache.getOrAdd(villager);
|
||||
final double distance = entity.getLocation().distanceSquared(blockLoc);
|
||||
|
||||
if (distance < closestDistance && wVillager.isOptimized()) {
|
||||
final WrappedVillager wVillager = wrapperCache.get(villager, WrappedVillager::new);
|
||||
if (wVillager.isOptimized()) {
|
||||
closestOptimizedVillager = wVillager;
|
||||
closestDistance = distance;
|
||||
}
|
||||
@ -187,29 +187,32 @@ public class OptimizeByBlock implements VillagerOptimizerModule, Listener {
|
||||
|
||||
if (closestOptimizedVillager == null) return;
|
||||
|
||||
VillagerUnoptimizeEvent unOptimizeEvent = new VillagerUnoptimizeEvent(closestOptimizedVillager, player, OptimizationType.BLOCK, event.isAsynchronous());
|
||||
if (!unOptimizeEvent.callEvent()) return;
|
||||
VillagerUnoptimizeEvent unOptimizeEvent = new VillagerUnoptimizeEvent(
|
||||
closestOptimizedVillager,
|
||||
player,
|
||||
OptimizationType.BLOCK,
|
||||
event.isAsynchronous()
|
||||
);
|
||||
|
||||
if (!unOptimizeEvent.callEvent()) return;
|
||||
closestOptimizedVillager.setOptimizationType(OptimizationType.NONE);
|
||||
|
||||
if (notify_player) {
|
||||
final TextReplacementConfig vilProfession = TextReplacementConfig.builder()
|
||||
.matchLiteral("%vil_profession%")
|
||||
.replacement(closestOptimizedVillager.villager().getProfession().toString().toLowerCase())
|
||||
.replacement(Util.toNiceString(closestOptimizedVillager.villager.getProfession()))
|
||||
.build();
|
||||
final TextReplacementConfig brokenMaterial = TextReplacementConfig.builder()
|
||||
.matchLiteral("%blocktype%")
|
||||
.replacement(broken.getType().toString().toLowerCase())
|
||||
.replacement(Util.toNiceString(broken.getType()))
|
||||
.build();
|
||||
VillagerOptimizer.getLang(player.locale()).block_unoptimize_success.forEach(line -> player.sendMessage(line
|
||||
.replaceText(vilProfession)
|
||||
.replaceText(brokenMaterial)
|
||||
));
|
||||
VillagerOptimizer.getLang(player.locale()).block_unoptimize_success
|
||||
.forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(vilProfession).replaceText(brokenMaterial)));
|
||||
}
|
||||
|
||||
if (log_enabled) {
|
||||
VillagerOptimizer.getLog().info(Component.text(player.getName() + " unoptimized villager by block at " +
|
||||
CommonUtil.formatLocation(closestOptimizedVillager.villager().getLocation())).color(VillagerOptimizer.plugin_style.color()));
|
||||
info(player.getName() + " unoptimized villager using " + Util.toNiceString(broken.getType()) +
|
||||
LocationUtil.toString(closestOptimizedVillager.villager.getLocation()));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,21 +1,20 @@
|
||||
package me.xginko.villageroptimizer.modules.optimization;
|
||||
|
||||
import me.xginko.villageroptimizer.VillagerCache;
|
||||
import com.cryptomorin.xseries.XEntityType;
|
||||
import com.cryptomorin.xseries.XMaterial;
|
||||
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.Bypass;
|
||||
import me.xginko.villageroptimizer.enums.permissions.Optimize;
|
||||
import me.xginko.villageroptimizer.struct.enums.OptimizationType;
|
||||
import me.xginko.villageroptimizer.struct.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 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 net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.GameMode;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.entity.Villager;
|
||||
import org.bukkit.event.EventHandler;
|
||||
@ -27,41 +26,39 @@ import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class OptimizeByNametag implements VillagerOptimizerModule, Listener {
|
||||
public class OptimizeByNametag extends VillagerOptimizerModule implements Listener {
|
||||
|
||||
private final VillagerCache villagerCache;
|
||||
private final Set<String> nametags;
|
||||
private final long cooldown;
|
||||
private final boolean consume_nametag, notify_player, log_enabled;
|
||||
|
||||
public OptimizeByNametag() {
|
||||
shouldEnable();
|
||||
this.villagerCache = VillagerOptimizer.getCache();
|
||||
Config config = VillagerOptimizer.getConfiguration();
|
||||
config.master().addComment("optimization-methods.nametag-optimization.enable", """
|
||||
Enable optimization by naming villagers to one of the names configured below.\s
|
||||
Nametag optimized villagers will be unoptimized again when they are renamed to something else.""");
|
||||
this.nametags = config.getList("optimization-methods.nametag-optimization.names", List.of("Optimize", "DisableAI"),
|
||||
super("optimization-methods.nametag-optimization");
|
||||
config.master().addComment(configPath + ".enable",
|
||||
"Enable optimization by naming villagers to one of the names configured below.\n" +
|
||||
"Nametag optimized villagers will be unoptimized again when they are renamed to something else.");
|
||||
this.nametags = config.getList(configPath + ".names", Arrays.asList("Optimize", "DisableAI"),
|
||||
"Names are case insensitive, capital letters won't matter.")
|
||||
.stream().map(String::toLowerCase).collect(Collectors.toCollection(HashSet::new));
|
||||
this.consume_nametag = config.getBoolean("optimization-methods.nametag-optimization.nametags-get-consumed", true,
|
||||
this.consume_nametag = config.getBoolean(configPath + ".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,
|
||||
this.cooldown = TimeUnit.SECONDS.toMillis(
|
||||
config.getInt(configPath + ".optimize-cooldown-seconds", 600,
|
||||
"Cooldown in seconds until a villager can be optimized again using a nametag.\n" +
|
||||
"Here for configuration freedom. Recommended to leave as is to not enable any exploitable behavior."));
|
||||
this.notify_player = config.getBoolean(configPath + ".notify-player", true,
|
||||
"Sends players a message when they successfully optimized a villager.");
|
||||
this.log_enabled = config.getBoolean("optimization-methods.nametag-optimization.log", false);
|
||||
this.log_enabled = config.getBoolean(configPath + ".log", false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enable() {
|
||||
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
|
||||
plugin.getServer().getPluginManager().registerEvents(this, plugin);
|
||||
}
|
||||
|
||||
@ -72,75 +69,88 @@ public class OptimizeByNametag implements VillagerOptimizerModule, Listener {
|
||||
|
||||
@Override
|
||||
public boolean shouldEnable() {
|
||||
return VillagerOptimizer.getConfiguration().getBoolean("optimization-methods.nametag-optimization.enable", true);
|
||||
return config.getBoolean(configPath + ".enable", true);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@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(Optimize.NAMETAG.get())) return;
|
||||
if (event.getRightClicked().getType() != XEntityType.VILLAGER.get()) return;
|
||||
final 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();
|
||||
final ItemStack usedItem = player.getInventory().getItem(event.getHand());
|
||||
if (usedItem != null && usedItem.getType() != XMaterial.NAME_TAG.parseMaterial()) return;
|
||||
if (!usedItem.hasItemMeta()) return;
|
||||
final ItemMeta meta = usedItem.getItemMeta();
|
||||
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);
|
||||
final String nameTagPlainText = ChatColor.stripColor(meta.getDisplayName());
|
||||
final WrappedVillager wrapped = wrapperCache.get((Villager) event.getRightClicked(), WrappedVillager::new);
|
||||
|
||||
if (nametags.contains(nameTagPlainText.toLowerCase())) {
|
||||
if (wrapped.canOptimize(cooldown) || player.hasPermission(Permissions.Bypass.NAMETAG_COOLDOWN.get())) {
|
||||
VillagerOptimizeEvent optimizeEvent = new VillagerOptimizeEvent(
|
||||
wrapped,
|
||||
OptimizationType.NAMETAG,
|
||||
player,
|
||||
event.isAsynchronous()
|
||||
);
|
||||
|
||||
if (nametags.contains(name.toLowerCase())) {
|
||||
if (wVillager.canOptimize(cooldown) || player.hasPermission(Bypass.NAMETAG_COOLDOWN.get())) {
|
||||
VillagerOptimizeEvent optimizeEvent = new VillagerOptimizeEvent(wVillager, OptimizationType.NAMETAG, player, event.isAsynchronous());
|
||||
if (!optimizeEvent.callEvent()) return;
|
||||
|
||||
if (!consume_nametag) {
|
||||
event.setCancelled(true);
|
||||
villager.customName(newVillagerName);
|
||||
wrapped.setOptimizationType(optimizeEvent.getOptimizationType());
|
||||
wrapped.saveOptimizeTime();
|
||||
|
||||
if (!consume_nametag && player.getGameMode() == GameMode.SURVIVAL) {
|
||||
player.getInventory().addItem(usedItem.asOne());
|
||||
}
|
||||
|
||||
wVillager.setOptimizationType(optimizeEvent.getOptimizationType());
|
||||
wVillager.saveOptimizeTime();
|
||||
|
||||
if (notify_player) {
|
||||
VillagerOptimizer.getLang(player.locale()).nametag_optimize_success.forEach(player::sendMessage);
|
||||
VillagerOptimizer.getLang(player.locale()).nametag_optimize_success
|
||||
.forEach(line -> KyoriUtil.sendMessage(player, line));
|
||||
}
|
||||
|
||||
if (log_enabled) {
|
||||
VillagerOptimizer.getLog().info(Component.text(player.getName() +
|
||||
" optimized villager by nametag '" + name + "' at " +
|
||||
CommonUtil.formatLocation(wVillager.villager().getLocation())).color(VillagerOptimizer.plugin_style.color()));
|
||||
info(player.getName() + " optimized villager using nametag '" + nameTagPlainText + "' at " +
|
||||
LocationUtil.toString(wrapped.villager.getLocation()));
|
||||
}
|
||||
} else {
|
||||
event.setCancelled(true);
|
||||
CommonUtil.shakeHead(villager);
|
||||
wrapped.sayNo();
|
||||
if (notify_player) {
|
||||
final TextReplacementConfig timeLeft = TextReplacementConfig.builder()
|
||||
.matchLiteral("%time%")
|
||||
.replacement(CommonUtil.formatDuration(Duration.ofMillis(wVillager.getOptimizeCooldownMillis(cooldown))))
|
||||
.replacement(Util.formatDuration(Duration.ofMillis(wrapped.getOptimizeCooldownMillis(cooldown))))
|
||||
.build();
|
||||
VillagerOptimizer.getLang(player.locale()).nametag_on_optimize_cooldown.forEach(line -> player.sendMessage(line.replaceText(timeLeft)));
|
||||
VillagerOptimizer.getLang(player.locale()).nametag_on_optimize_cooldown
|
||||
.forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(timeLeft)));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (wVillager.isOptimized()) {
|
||||
VillagerUnoptimizeEvent unOptimizeEvent = new VillagerUnoptimizeEvent(wVillager, player, OptimizationType.NAMETAG, event.isAsynchronous());
|
||||
if (!unOptimizeEvent.callEvent()) return;
|
||||
if (wrapped.isOptimized()) {
|
||||
VillagerUnoptimizeEvent unOptimizeEvent = new VillagerUnoptimizeEvent(
|
||||
wrapped,
|
||||
player,
|
||||
OptimizationType.NAMETAG,
|
||||
event.isAsynchronous()
|
||||
);
|
||||
|
||||
wVillager.setOptimizationType(OptimizationType.NONE);
|
||||
if (!unOptimizeEvent.callEvent()) return;
|
||||
wrapped.setOptimizationType(OptimizationType.NONE);
|
||||
|
||||
if (!consume_nametag && player.getGameMode() == GameMode.SURVIVAL) {
|
||||
player.getInventory().addItem(usedItem.asOne());
|
||||
}
|
||||
|
||||
if (notify_player) {
|
||||
VillagerOptimizer.getLang(player.locale()).nametag_unoptimize_success.forEach(player::sendMessage);
|
||||
VillagerOptimizer.getLang(player.locale()).nametag_unoptimize_success
|
||||
.forEach(line -> KyoriUtil.sendMessage(player, line));
|
||||
}
|
||||
|
||||
if (log_enabled) {
|
||||
VillagerOptimizer.getLog().info(Component.text(player.getName() +
|
||||
" unoptimized villager by nametag '" + name + "' at " +
|
||||
CommonUtil.formatLocation(wVillager.villager().getLocation())).color(VillagerOptimizer.plugin_style.color()));
|
||||
info(player.getName() + " unoptimized villager using nametag '" + nameTagPlainText + "' at " +
|
||||
LocationUtil.toString(wrapped.villager.getLocation()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,26 +1,18 @@
|
||||
package me.xginko.villageroptimizer.modules.optimization;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import com.tcoded.folialib.impl.ServerImplementation;
|
||||
import com.tcoded.folialib.wrapper.task.WrappedTask;
|
||||
import me.xginko.villageroptimizer.VillagerCache;
|
||||
import me.xginko.villageroptimizer.VillagerOptimizer;
|
||||
import me.xginko.villageroptimizer.WrappedVillager;
|
||||
import me.xginko.villageroptimizer.config.Config;
|
||||
import me.xginko.villageroptimizer.enums.OptimizationType;
|
||||
import me.xginko.villageroptimizer.enums.permissions.Bypass;
|
||||
import me.xginko.villageroptimizer.enums.permissions.Optimize;
|
||||
import me.xginko.villageroptimizer.struct.enums.OptimizationType;
|
||||
import me.xginko.villageroptimizer.struct.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 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.Location;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.entity.Villager;
|
||||
import org.bukkit.event.EventHandler;
|
||||
@ -29,56 +21,43 @@ import org.bukkit.event.HandlerList;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.block.BlockBreakEvent;
|
||||
import org.bukkit.event.block.BlockPlaceEvent;
|
||||
import org.bukkit.util.NumberConversions;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class OptimizeByWorkstation implements VillagerOptimizerModule, Listener {
|
||||
public class OptimizeByWorkstation extends VillagerOptimizerModule implements Listener {
|
||||
|
||||
private final ServerImplementation scheduler;
|
||||
private final VillagerCache villagerCache;
|
||||
private final Cache<Location, WrappedTask> pending_optimizations;
|
||||
private final long cooldown_millis, delay_millis, resettable_delay_millis;
|
||||
private final double search_radius, search_radius_squared;
|
||||
private final long cooldown_millis;
|
||||
private final double search_radius;
|
||||
private final int check_duration_ticks;
|
||||
private final boolean only_while_sneaking, log_enabled, notify_player;
|
||||
|
||||
public OptimizeByWorkstation() {
|
||||
shouldEnable();
|
||||
this.scheduler = VillagerOptimizer.getFoliaLib().getImpl();
|
||||
this.villagerCache = VillagerOptimizer.getCache();
|
||||
Config config = VillagerOptimizer.getConfiguration();
|
||||
config.master().addComment("optimization-methods.workstation-optimization.enable", """
|
||||
When enabled, villagers that have a job and have been traded with at least once will become optimized,\s
|
||||
if near their workstation. If the workstation is broken, the villager will become unoptimized again.""");
|
||||
this.delay_millis = Math.max(config.getInt("optimization-methods.workstation-optimization.delay.default-delay-in-ticks", 10, """
|
||||
The delay in ticks the plugin should wait before trying to optimize the closest villager on workstation place.\s
|
||||
Gives the villager time to claim the placed workstation. Minimum delay is 1 Tick (Not recommended)"""), 1) * 50L;
|
||||
this.resettable_delay_millis = Math.max(config.getInt("optimization-methods.workstation-optimization.delay.resettable-delay-in-ticks", 60, """
|
||||
The delay in ticks the plugin should wait before trying to optimize a villager that can loose its profession\s
|
||||
by having their workstation destroyed.\s
|
||||
Intended to fix issues while trade rolling."""), 1) * 50L;
|
||||
this.pending_optimizations = Caffeine.newBuilder()
|
||||
.expireAfterWrite(Duration.ofMillis(Math.max(resettable_delay_millis, delay_millis) + 500L))
|
||||
.build();
|
||||
this.search_radius = config.getDouble("optimization-methods.workstation-optimization.search-radius-in-blocks", 2.0, """
|
||||
The radius in blocks a villager can be away from the player when he places a workstation.\s
|
||||
The closest unoptimized villager to the player will be optimized.""");
|
||||
this.search_radius_squared = NumberConversions.square(search_radius);
|
||||
super("optimization-methods.workstation-optimization");
|
||||
config.master().addComment(configPath + ".enable",
|
||||
"When enabled, villagers that have a job and have been traded with at least once will become optimized,\n" +
|
||||
"if near their workstation. If the workstation is broken, the villager will become unoptimized again.");
|
||||
this.check_duration_ticks = Math.max(config.getInt(configPath + ".check-linger-duration-ticks", 100,
|
||||
"After a workstation has been placed, the plugin will wait for the configured amount of time in ticks\n" +
|
||||
"for a villager to claim that workstation. Not recommended to go below 100 ticks."), 1);
|
||||
this.search_radius = config.getDouble(configPath + ".search-radius-in-blocks", 2.0,
|
||||
"The radius in blocks a villager can be away from the player when he places a workstation.\n" +
|
||||
"The closest unoptimized villager to the player will be optimized.");
|
||||
this.cooldown_millis = TimeUnit.SECONDS.toMillis(
|
||||
config.getInt("optimization-methods.workstation-optimization.optimize-cooldown-seconds", 600, """
|
||||
Cooldown in seconds until a villager can be optimized again using a workstation.\s
|
||||
Here for configuration freedom. Recommended to leave as is to not enable any exploitable behavior."""));
|
||||
this.only_while_sneaking = config.getBoolean("optimization-methods.workstation-optimization.only-when-sneaking", true,
|
||||
"Only optimize/unoptimize by workstation when player is sneaking during place or break");
|
||||
this.notify_player = config.getBoolean("optimization-methods.workstation-optimization.notify-player", true,
|
||||
Math.max(1, config.getInt(configPath + ".optimize-cooldown-seconds", 600,
|
||||
"Cooldown in seconds until a villager can be optimized again using a workstation.\n" +
|
||||
"Here for configuration freedom. Recommended to leave as is to not enable any exploitable behavior.")));
|
||||
this.only_while_sneaking = config.getBoolean(configPath + ".only-when-sneaking", true,
|
||||
"Only optimize/unoptimize by workstation when player is sneaking during place or break. Useful for villager rolling.");
|
||||
this.notify_player = config.getBoolean(configPath + ".notify-player", true,
|
||||
"Sends players a message when they successfully optimized a villager.");
|
||||
this.log_enabled = config.getBoolean("optimization-methods.workstation-optimization.log", false);
|
||||
this.log_enabled = config.getBoolean(configPath + ".log", false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enable() {
|
||||
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
|
||||
plugin.getServer().getPluginManager().registerEvents(this, plugin);
|
||||
}
|
||||
|
||||
@ -89,142 +68,146 @@ public class OptimizeByWorkstation implements VillagerOptimizerModule, Listener
|
||||
|
||||
@Override
|
||||
public boolean shouldEnable() {
|
||||
return VillagerOptimizer.getConfiguration().getBoolean("optimization-methods.workstation-optimization.enable", false);
|
||||
return config.getBoolean(configPath + ".enable", false);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
private void onBlockPlace(BlockPlaceEvent event) {
|
||||
final Block placed = event.getBlock();
|
||||
final Villager.Profession workstationProfession = CommonUtil.getWorkstationProfession(placed.getType());
|
||||
if (workstationProfession.equals(Villager.Profession.NONE)) return;
|
||||
final Villager.Profession workstationProfession = Util.getWorkstationProfession(placed.getType());
|
||||
if (workstationProfession == null) return;
|
||||
|
||||
final Player player = event.getPlayer();
|
||||
if (!player.hasPermission(Optimize.WORKSTATION.get())) return;
|
||||
if (only_while_sneaking && !player.isSneaking()) return;
|
||||
if (!player.hasPermission(Permissions.Optimize.WORKSTATION.get())) return;
|
||||
|
||||
final Location workstationLoc = placed.getLocation().toCenterLocation();
|
||||
WrappedVillager toOptimize = null;
|
||||
final Location workstationLoc = placed.getLocation();
|
||||
final AtomicBoolean taskComplete = new AtomicBoolean();
|
||||
final AtomicInteger taskAliveTicks = new AtomicInteger();
|
||||
|
||||
for (Entity entity : workstationLoc.getNearbyEntities(search_radius, search_radius, search_radius)) {
|
||||
if (!entity.getType().equals(EntityType.VILLAGER)) continue;
|
||||
Villager villager = (Villager) entity;
|
||||
if (!villager.getProfession().equals(workstationProfession)) continue;
|
||||
|
||||
WrappedVillager wVillager = villagerCache.getOrAdd(villager);
|
||||
|
||||
final Location jobSite = wVillager.getJobSite();
|
||||
if (jobSite == null) continue;
|
||||
if (jobSite.distanceSquared(workstationLoc) > search_radius_squared) continue;
|
||||
|
||||
if (wVillager.canOptimize(cooldown_millis)) {
|
||||
toOptimize = wVillager;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (toOptimize == null) return;
|
||||
WrappedVillager finalToOptimize = toOptimize;
|
||||
|
||||
pending_optimizations.put(placed.getLocation(), scheduler.runAtLocationLater(workstationLoc, () -> {
|
||||
if (!finalToOptimize.canOptimize(cooldown_millis) && !player.hasPermission(Bypass.WORKSTATION_COOLDOWN.get())) {
|
||||
CommonUtil.shakeHead(finalToOptimize.villager());
|
||||
if (notify_player) {
|
||||
final TextReplacementConfig timeLeft = TextReplacementConfig.builder()
|
||||
.matchLiteral("%time%")
|
||||
.replacement(CommonUtil.formatDuration(Duration.ofMillis(finalToOptimize.getOptimizeCooldownMillis(cooldown_millis))))
|
||||
.build();
|
||||
VillagerOptimizer.getLang(player.locale()).nametag_on_optimize_cooldown.forEach(line -> player.sendMessage(line
|
||||
.replaceText(timeLeft)
|
||||
));
|
||||
}
|
||||
scheduling.regionSpecificScheduler(workstationLoc).runAtFixedRate(repeatingTask -> {
|
||||
if (taskComplete.get() || taskAliveTicks.getAndAdd(10) > check_duration_ticks) {
|
||||
repeatingTask.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
VillagerOptimizeEvent optimizeEvent = new VillagerOptimizeEvent(finalToOptimize, OptimizationType.WORKSTATION, player, event.isAsynchronous());
|
||||
if (!optimizeEvent.callEvent()) return;
|
||||
for (Villager villager : workstationLoc.getNearbyEntitiesByType(Villager.class, search_radius)) {
|
||||
scheduling.entitySpecificScheduler(villager).run(() -> {
|
||||
if (villager.getProfession() != workstationProfession) return;
|
||||
WrappedVillager wrapped = wrapperCache.get(villager, WrappedVillager::new);
|
||||
|
||||
finalToOptimize.setOptimizationType(optimizeEvent.getOptimizationType());
|
||||
finalToOptimize.saveOptimizeTime();
|
||||
Location jobSite = wrapped.getJobSite();
|
||||
if (jobSite == null || jobSite.getWorld().getUID() != workstationLoc.getWorld().getUID()) return;
|
||||
if (LocationUtil.relDistance3DSquared(jobSite, workstationLoc) > 1) return;
|
||||
|
||||
if (notify_player) {
|
||||
final TextReplacementConfig vilProfession = TextReplacementConfig.builder()
|
||||
.matchLiteral("%vil_profession%")
|
||||
.replacement(finalToOptimize.villager().getProfession().toString().toLowerCase())
|
||||
.build();
|
||||
final TextReplacementConfig placedWorkstation = TextReplacementConfig.builder()
|
||||
.matchLiteral("%workstation%")
|
||||
.replacement(placed.getType().toString().toLowerCase())
|
||||
.build();
|
||||
VillagerOptimizer.getLang(player.locale()).workstation_optimize_success.forEach(line -> player.sendMessage(line
|
||||
.replaceText(vilProfession)
|
||||
.replaceText(placedWorkstation)
|
||||
));
|
||||
if (!wrapped.canOptimize(cooldown_millis) && !player.hasPermission(Permissions.Bypass.WORKSTATION_COOLDOWN.get())) {
|
||||
wrapped.sayNo();
|
||||
|
||||
if (notify_player) {
|
||||
final TextReplacementConfig timeLeft = TextReplacementConfig.builder()
|
||||
.matchLiteral("%time%")
|
||||
.replacement(Util.formatDuration(Duration.ofMillis(wrapped.getOptimizeCooldownMillis(cooldown_millis))))
|
||||
.build();
|
||||
VillagerOptimizer.getLang(player.locale()).nametag_on_optimize_cooldown
|
||||
.forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(timeLeft)));
|
||||
}
|
||||
|
||||
taskComplete.set(true);
|
||||
return;
|
||||
}
|
||||
|
||||
VillagerOptimizeEvent optimizeEvent = new VillagerOptimizeEvent(
|
||||
wrapped,
|
||||
OptimizationType.WORKSTATION,
|
||||
player,
|
||||
event.isAsynchronous()
|
||||
);
|
||||
|
||||
if (!optimizeEvent.callEvent()) return;
|
||||
|
||||
wrapped.setOptimizationType(optimizeEvent.getOptimizationType());
|
||||
wrapped.saveOptimizeTime();
|
||||
|
||||
if (notify_player) {
|
||||
final TextReplacementConfig vilProfession = TextReplacementConfig.builder()
|
||||
.matchLiteral("%vil_profession%")
|
||||
.replacement(Util.toNiceString(wrapped.villager.getProfession()))
|
||||
.build();
|
||||
final TextReplacementConfig placedWorkstation = TextReplacementConfig.builder()
|
||||
.matchLiteral("%blocktype%")
|
||||
.replacement(Util.toNiceString(placed.getType()))
|
||||
.build();
|
||||
VillagerOptimizer.getLang(player.locale()).workstation_optimize_success
|
||||
.forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(vilProfession).replaceText(placedWorkstation)));
|
||||
}
|
||||
|
||||
if (log_enabled) {
|
||||
info(player.getName() + " optimized villager using workstation " + Util.toNiceString(placed.getType()) + " at " +
|
||||
LocationUtil.toString(wrapped.villager.getLocation()));
|
||||
}
|
||||
|
||||
taskComplete.set(true);
|
||||
}, null);
|
||||
}
|
||||
|
||||
if (log_enabled) {
|
||||
VillagerOptimizer.getLog().info(Component.text(player.getName() +
|
||||
" optimized villager by workstation (" + placed.getType().toString().toLowerCase() + ") at " +
|
||||
CommonUtil.formatLocation(finalToOptimize.villager().getLocation())).color(VillagerOptimizer.plugin_style.color()));
|
||||
}
|
||||
}, toOptimize.canLooseProfession() ? resettable_delay_millis : delay_millis, TimeUnit.MILLISECONDS));
|
||||
}, 1L, 10L);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
private void onBlockBreak(BlockBreakEvent event) {
|
||||
final Block broken = event.getBlock();
|
||||
// Cancel any pending optimization for this block
|
||||
WrappedTask pendingOpt = pending_optimizations.getIfPresent(broken.getLocation());
|
||||
if (pendingOpt != null) pendingOpt.cancel();
|
||||
final Villager.Profession workstationProfession = Util.getWorkstationProfession(broken.getType());
|
||||
if (workstationProfession == null) return;
|
||||
|
||||
final Villager.Profession workstationProfession = CommonUtil.getWorkstationProfession(broken.getType());
|
||||
if (workstationProfession.equals(Villager.Profession.NONE)) return;
|
||||
final Player player = event.getPlayer();
|
||||
if (!player.hasPermission(Optimize.WORKSTATION.get())) return;
|
||||
if (!player.hasPermission(Permissions.Optimize.WORKSTATION.get())) return;
|
||||
if (only_while_sneaking && !player.isSneaking()) return;
|
||||
|
||||
final Location workstationLoc = broken.getLocation();
|
||||
WrappedVillager closestOptimizedVillager = null;
|
||||
WrappedVillager closestOptimized = 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;
|
||||
for (Villager villager : workstationLoc.getNearbyEntitiesByType(Villager.class, search_radius)) {
|
||||
if (!villager.getProfession().equals(workstationProfession)) continue;
|
||||
final double distance = LocationUtil.relDistance3DSquared(villager.getLocation(), workstationLoc);
|
||||
if (distance >= closestDistance) continue;
|
||||
|
||||
WrappedVillager wVillager = villagerCache.getOrAdd(villager);
|
||||
final double distance = entity.getLocation().distanceSquared(workstationLoc);
|
||||
WrappedVillager wrapped = wrapperCache.get(villager, WrappedVillager::new);
|
||||
|
||||
if (distance < closestDistance && wVillager.isOptimized()) {
|
||||
closestOptimizedVillager = wVillager;
|
||||
if (wrapped.isOptimized()) {
|
||||
closestOptimized = wrapped;
|
||||
closestDistance = distance;
|
||||
}
|
||||
}
|
||||
|
||||
if (closestOptimizedVillager == null) return;
|
||||
if (closestOptimized == null) return;
|
||||
|
||||
VillagerUnoptimizeEvent unOptimizeEvent = new VillagerUnoptimizeEvent(
|
||||
closestOptimized,
|
||||
player,
|
||||
OptimizationType.WORKSTATION,
|
||||
event.isAsynchronous()
|
||||
);
|
||||
|
||||
VillagerUnoptimizeEvent unOptimizeEvent = new VillagerUnoptimizeEvent(closestOptimizedVillager, player, OptimizationType.WORKSTATION, event.isAsynchronous());
|
||||
if (!unOptimizeEvent.callEvent()) return;
|
||||
|
||||
closestOptimizedVillager.setOptimizationType(OptimizationType.NONE);
|
||||
closestOptimized.setOptimizationType(OptimizationType.NONE);
|
||||
|
||||
if (notify_player) {
|
||||
final TextReplacementConfig vilProfession = TextReplacementConfig.builder()
|
||||
.matchLiteral("%vil_profession%")
|
||||
.replacement(closestOptimizedVillager.villager().getProfession().toString().toLowerCase())
|
||||
.replacement(Util.toNiceString(closestOptimized.villager.getProfession()))
|
||||
.build();
|
||||
final TextReplacementConfig brokenWorkstation = TextReplacementConfig.builder()
|
||||
.matchLiteral("%workstation%")
|
||||
.replacement(broken.getType().toString().toLowerCase())
|
||||
.matchLiteral("%blocktype%")
|
||||
.replacement(Util.toNiceString(broken.getType()))
|
||||
.build();
|
||||
VillagerOptimizer.getLang(player.locale()).workstation_unoptimize_success.forEach(line -> player.sendMessage(line
|
||||
.replaceText(vilProfession)
|
||||
.replaceText(brokenWorkstation)
|
||||
));
|
||||
VillagerOptimizer.getLang(player.locale()).workstation_unoptimize_success
|
||||
.forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(vilProfession).replaceText(brokenWorkstation)));
|
||||
}
|
||||
|
||||
if (log_enabled) {
|
||||
VillagerOptimizer.getLog().info(Component.text(player.getName() +
|
||||
" unoptimized villager by workstation (" + broken.getType().toString().toLowerCase() + ") at " +
|
||||
CommonUtil.formatLocation(closestOptimizedVillager.villager().getLocation())).color(VillagerOptimizer.plugin_style.color()));
|
||||
info(player.getName() + " unoptimized villager using workstation " + Util.toNiceString(broken.getType()) + " at " +
|
||||
LocationUtil.toString(closestOptimized.villager.getLocation()));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package me.xginko.villageroptimizer.struct;
|
||||
|
||||
public interface Disableable {
|
||||
void disable();
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package me.xginko.villageroptimizer.struct;
|
||||
|
||||
public interface Enableable {
|
||||
void enable();
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package me.xginko.villageroptimizer.enums;
|
||||
package me.xginko.villageroptimizer.struct.enums;
|
||||
|
||||
import net.kyori.adventure.key.Namespaced;
|
||||
import org.bukkit.Keyed;
|
||||
@ -10,9 +10,9 @@ import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class Keyring {
|
||||
public final class Keyring {
|
||||
|
||||
public enum Spaces implements Namespaced {
|
||||
public enum Space implements Namespaced {
|
||||
|
||||
VillagerOptimizer("VillagerOptimizer"),
|
||||
AntiVillagerLag("AntiVillagerLag");
|
||||
@ -20,7 +20,7 @@ public class Keyring {
|
||||
@Pattern("[a-z0-9_\\-.]+")
|
||||
private final @NotNull String namespace;
|
||||
|
||||
Spaces(@NotNull @Pattern("[a-z0-9_\\-.]+") String pluginName) {
|
||||
Space(@NotNull @Pattern("[a-z0-9_\\-.]+") String pluginName) {
|
||||
this.namespace = pluginName.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
@ -43,24 +43,21 @@ public class Keyring {
|
||||
* @return a {@link NamespacedKey} that can be used to test for and read data stored by plugins
|
||||
* from a {@link PersistentDataContainer}
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
public static NamespacedKey getKey(@NotNull String pluginName, @NotNull String key) {
|
||||
return new NamespacedKey(pluginName.toLowerCase(Locale.ROOT), key);
|
||||
return new NamespacedKey(pluginName.toLowerCase(Locale.ROOT), key.toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
public enum VillagerOptimizer implements Keyed {
|
||||
|
||||
OPTIMIZATION_TYPE("optimization-type"),
|
||||
LAST_OPTIMIZE("last-optimize"),
|
||||
LAST_LEVELUP("last-levelup"),
|
||||
LAST_RESTOCK("last-restock"),
|
||||
LAST_OPTIMIZE_NAME("last-optimize-name");
|
||||
LAST_OPTIMIZE_SYSTIME_MILLIS("last-optimize"),
|
||||
LAST_LEVELUP_SYSTIME_MILLIS("last-levelup"),
|
||||
LAST_RESTOCK_WORLD_FULLTIME("last-restock-full-time");
|
||||
|
||||
private final @NotNull NamespacedKey key;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
VillagerOptimizer(@NotNull String key) {
|
||||
this.key = new NamespacedKey(Spaces.VillagerOptimizer.namespace(), key);
|
||||
this.key = Keyring.getKey(Space.VillagerOptimizer.namespace(), key);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -72,7 +69,7 @@ public class Keyring {
|
||||
public enum AntiVillagerLag implements Keyed {
|
||||
|
||||
NEXT_OPTIMIZATION_SYSTIME_SECONDS("cooldown"), // Returns LONG -> (System.currentTimeMillis() / 1000) + cooldown seconds
|
||||
LAST_RESTOCK_WORLDFULLTIME("time"), // Returns LONG -> villager.getWorld().getFullTime()
|
||||
LAST_RESTOCK_WORLD_FULLTIME("time"), // Returns LONG -> villager.getWorld().getFullTime()
|
||||
NEXT_LEVELUP_SYSTIME_SECONDS("levelCooldown"), // Returns LONG -> (System.currentTimeMillis() / 1000) + cooldown seconds
|
||||
OPTIMIZED_ANY("Marker"), // Returns STRING -> "AVL"
|
||||
OPTIMIZED_BLOCK("disabledByBlock"), // Returns STRING -> key().toString()
|
||||
@ -80,9 +77,8 @@ public class Keyring {
|
||||
|
||||
private final @NotNull NamespacedKey key;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
AntiVillagerLag(@NotNull String avlKey) {
|
||||
this.key = new NamespacedKey(Spaces.AntiVillagerLag.namespace(), avlKey);
|
||||
this.key = Keyring.getKey(Space.AntiVillagerLag.namespace(), avlKey);
|
||||
}
|
||||
|
||||
@Override
|
@ -1,6 +1,8 @@
|
||||
package me.xginko.villageroptimizer.enums;
|
||||
package me.xginko.villageroptimizer.struct.enums;
|
||||
|
||||
public enum OptimizationType {
|
||||
CHUNK_LIMIT,
|
||||
REGIONAL_ACTIVITY,
|
||||
COMMAND,
|
||||
NAMETAG,
|
||||
WORKSTATION,
|
@ -0,0 +1,95 @@
|
||||
package me.xginko.villageroptimizer.struct.enums;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.permissions.Permission;
|
||||
import org.bukkit.permissions.PermissionDefault;
|
||||
|
||||
public final class Permissions {
|
||||
|
||||
public enum Bypass {
|
||||
TRADE_PREVENTION(new Permission("villageroptimizer.bypass.tradeprevention",
|
||||
"Permission to bypass unoptimized trade prevention", PermissionDefault.FALSE)),
|
||||
RESTOCK_COOLDOWN(new Permission("villageroptimizer.bypass.restockcooldown",
|
||||
"Permission to bypass restock cooldown on optimized villagers", PermissionDefault.FALSE)),
|
||||
NAMETAG_COOLDOWN(new Permission("villageroptimizer.bypass.nametagcooldown",
|
||||
"Permission to bypass Nametag optimization cooldown", PermissionDefault.FALSE)),
|
||||
BLOCK_COOLDOWN(new Permission("villageroptimizer.bypass.blockcooldown",
|
||||
"Permission to bypass Block optimization cooldown", PermissionDefault.FALSE)),
|
||||
WORKSTATION_COOLDOWN(new Permission("villageroptimizer.bypass.workstationcooldown",
|
||||
"Permission to bypass Workstation optimization cooldown", PermissionDefault.FALSE)),
|
||||
COMMAND_COOLDOWN(new Permission("villageroptimizer.bypass.commandcooldown",
|
||||
"Permission to bypass command optimization cooldown", PermissionDefault.FALSE));
|
||||
|
||||
private final Permission permission;
|
||||
|
||||
Bypass(Permission permission) {
|
||||
this.permission = permission;
|
||||
}
|
||||
|
||||
public Permission get() {
|
||||
return permission;
|
||||
}
|
||||
}
|
||||
|
||||
public enum Commands {
|
||||
VERSION(new Permission("villageroptimizer.cmd.version",
|
||||
"Permission get the plugin version", PermissionDefault.OP)),
|
||||
RELOAD(new Permission("villageroptimizer.cmd.reload",
|
||||
"Permission to reload the plugin config", PermissionDefault.OP)),
|
||||
DISABLE(new Permission("villageroptimizer.cmd.disable",
|
||||
"Permission to disable the plugin", PermissionDefault.OP)),
|
||||
OPTIMIZE_RADIUS(new Permission("villageroptimizer.cmd.optimize",
|
||||
"Permission to optimize villagers in a radius", PermissionDefault.TRUE)),
|
||||
UNOPTIMIZE_RADIUS(new Permission("villageroptimizer.cmd.unoptimize",
|
||||
"Permission to unoptimize villagers in a radius", PermissionDefault.TRUE));
|
||||
|
||||
private final Permission permission;
|
||||
|
||||
Commands(Permission permission) {
|
||||
this.permission = permission;
|
||||
}
|
||||
|
||||
public Permission get() {
|
||||
return permission;
|
||||
}
|
||||
}
|
||||
|
||||
public enum Optimize {
|
||||
NAMETAG(new Permission("villageroptimizer.optimize.nametag",
|
||||
"Permission to optimize / unoptimize using Nametags", PermissionDefault.TRUE)),
|
||||
BLOCK(new Permission("villageroptimizer.optimize.block",
|
||||
"Permission to optimize / unoptimize using Blocks", PermissionDefault.TRUE)),
|
||||
WORKSTATION(new Permission("villageroptimizer.optimize.workstation",
|
||||
"Permission to optimize / unoptimize using Workstations", PermissionDefault.TRUE));
|
||||
|
||||
private final Permission permission;
|
||||
|
||||
Optimize(Permission permission) {
|
||||
this.permission = permission;
|
||||
}
|
||||
|
||||
public Permission get() {
|
||||
return permission;
|
||||
}
|
||||
}
|
||||
|
||||
public static void registerAll() {
|
||||
for (Bypass perm : Bypass.values()) {
|
||||
try {
|
||||
Bukkit.getPluginManager().addPermission(perm.get());
|
||||
} catch (IllegalArgumentException ignored) {}
|
||||
}
|
||||
|
||||
for (Commands perm : Commands.values()) {
|
||||
try {
|
||||
Bukkit.getPluginManager().addPermission(perm.get());
|
||||
} catch (IllegalArgumentException ignored) {}
|
||||
}
|
||||
|
||||
for (Optimize perm : Optimize.values()) {
|
||||
try {
|
||||
Bukkit.getPluginManager().addPermission(perm.get());
|
||||
} catch (IllegalArgumentException ignored) {}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,131 @@
|
||||
package me.xginko.villageroptimizer.struct.models;
|
||||
|
||||
import me.xginko.villageroptimizer.VillagerOptimizer;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Entity;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class BlockRegion2D {
|
||||
|
||||
private final UUID worldUID;
|
||||
private final double halfSideLength, centerX, centerZ;
|
||||
|
||||
/**
|
||||
* A square region on a minecraft world map.
|
||||
*
|
||||
* @param worldUID The UUID of the world this region is in.
|
||||
* @param centerX The X-axis of the center location on the map.
|
||||
* @param centerZ The Z-axis of the center location on the map.
|
||||
* @param halfSideLength Half the length of the square's side. Acts like a radius would on circular regions.
|
||||
*/
|
||||
public BlockRegion2D(UUID worldUID, double centerX, double centerZ, double halfSideLength) {
|
||||
this.worldUID = worldUID;
|
||||
this.centerX = centerX;
|
||||
this.centerZ = centerZ;
|
||||
this.halfSideLength = halfSideLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a square region on a minecraft world map.
|
||||
*
|
||||
* @param worldUID The UUID of the world this region is in.
|
||||
* @param centerX The X-axis of the center location on the map.
|
||||
* @param centerZ The Z-axis of the center location on the map.
|
||||
* @param halfSideLength Half the length of the square's side. Acts like a radius would on circular regions.
|
||||
*/
|
||||
public static BlockRegion2D of(UUID worldUID, double centerX, double centerZ, double halfSideLength) {
|
||||
return new BlockRegion2D(worldUID, centerX, centerZ, halfSideLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a square region on a minecraft world map.
|
||||
*
|
||||
* @param world The world this region is in.
|
||||
* @param centerX The X-axis of the center location on the map.
|
||||
* @param centerZ The Z-axis of the center location on the map.
|
||||
* @param halfSideLength Half the length of the square's side. Acts like a radius would on circular regions.
|
||||
*/
|
||||
public static BlockRegion2D of(World world, double centerX, double centerZ, double halfSideLength) {
|
||||
return BlockRegion2D.of(world.getUID(), centerX, centerZ, halfSideLength);
|
||||
}
|
||||
|
||||
public UUID getWorldUID() {
|
||||
return this.worldUID;
|
||||
}
|
||||
|
||||
public double getHalfSideLength() {
|
||||
return this.halfSideLength;
|
||||
}
|
||||
|
||||
public double getCenterX() {
|
||||
return this.centerX;
|
||||
}
|
||||
|
||||
public double getCenterZ() {
|
||||
return this.centerZ;
|
||||
}
|
||||
|
||||
public boolean contains(Location location) {
|
||||
if (!location.getWorld().getUID().equals(this.worldUID)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return location.getX() >= this.centerX - this.halfSideLength
|
||||
&& location.getX() <= this.centerX + this.halfSideLength
|
||||
&& location.getZ() >= this.centerZ - this.halfSideLength
|
||||
&& location.getZ() <= this.centerZ + this.halfSideLength;
|
||||
}
|
||||
|
||||
public CompletableFuture<Collection<Entity>> getEntities() {
|
||||
World world = Bukkit.getWorld(worldUID);
|
||||
|
||||
if (world == null) {
|
||||
// Only way I can imagine this happening would be if the server is using a world manager plugin and unloads
|
||||
// the world during an operation.
|
||||
// Since these plugins are rather common though, we will silently complete with an empty set instead of exceptionally.
|
||||
return CompletableFuture.completedFuture(Collections.emptySet());
|
||||
}
|
||||
|
||||
CompletableFuture<Collection<Entity>> future = new CompletableFuture<>();
|
||||
Location centerLoc = new Location(world, centerX, world.getMinHeight(), centerZ);
|
||||
|
||||
VillagerOptimizer.scheduling().regionSpecificScheduler(centerLoc).run(() -> future.complete(
|
||||
centerLoc.getNearbyEntities(
|
||||
halfSideLength,
|
||||
Math.abs(world.getMaxHeight()) + Math.abs(world.getMinHeight()), // World y can be between -64 and 320, we want everything from top to bottom
|
||||
halfSideLength
|
||||
)));
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (null == obj || obj.getClass() != BlockRegion2D.class)
|
||||
return false;
|
||||
BlockRegion2D blockRegion2D = (BlockRegion2D)obj;
|
||||
return blockRegion2D.worldUID.equals(this.worldUID) && blockRegion2D.centerX == this.centerX && blockRegion2D.centerZ == this.centerZ;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(this.worldUID, this.centerX, this.centerZ, this.halfSideLength);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "BlockRegion2D{" +
|
||||
" radius(half side length)=" + halfSideLength +
|
||||
", centerX=" + centerX +
|
||||
", centerZ=" + centerZ +
|
||||
", worldUID=" + worldUID +
|
||||
"}";
|
||||
}
|
||||
}
|
@ -0,0 +1,349 @@
|
||||
package me.xginko.villageroptimizer.struct.models;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.AbstractSet;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public final class ExpiringSet<E> extends AbstractSet<E> implements Set<E> {
|
||||
|
||||
private final Cache<E, Object> cache;
|
||||
private static final Object PRESENT = new Object(); // Dummy value to associate with an Object in the backing Cache
|
||||
|
||||
public ExpiringSet(long duration, TimeUnit unit) {
|
||||
this.cache = Caffeine.newBuilder().expireAfterWrite(duration, unit).build();
|
||||
}
|
||||
|
||||
public ExpiringSet(Duration duration) {
|
||||
this.cache = Caffeine.newBuilder().expireAfterWrite(duration).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of elements in this set (its cardinality). If this
|
||||
* set contains more than {@code Integer.MAX_VALUE} elements, returns
|
||||
* {@code Integer.MAX_VALUE}.
|
||||
*
|
||||
* @return the number of elements in this set (its cardinality)
|
||||
*/
|
||||
@Override
|
||||
public int size() {
|
||||
return this.cache.asMap().size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if this set contains no elements.
|
||||
*
|
||||
* @return {@code true} if this set contains no elements
|
||||
*/
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return this.cache.asMap().isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if this set contains the specified element.
|
||||
* More formally, returns {@code true} if and only if this set
|
||||
* contains an element {@code e} such that
|
||||
* {@code Objects.equals(o, e)}.
|
||||
*
|
||||
* @param item element whose presence in this set is to be tested
|
||||
* @return {@code true} if this set contains the specified element
|
||||
* @throws ClassCastException if the type of the specified element
|
||||
* is incompatible with this set
|
||||
* (<a href="Collection.html#optional-restrictions">optional</a>)
|
||||
* @throws NullPointerException if the specified element is null and this
|
||||
* set does not permit null elements
|
||||
* (<a href="Collection.html#optional-restrictions">optional</a>)
|
||||
*/
|
||||
@Override
|
||||
public boolean contains(Object item) {
|
||||
return this.cache.getIfPresent(item) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an iterator over the elements in this set. The elements are
|
||||
* returned in no particular order (unless this set is an instance of some
|
||||
* class that provides a guarantee).
|
||||
*
|
||||
* @return an iterator over the elements in this set
|
||||
*/
|
||||
@Override
|
||||
public @NotNull Iterator<E> iterator() {
|
||||
return this.cache.asMap().keySet().iterator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array containing all of the elements in this set.
|
||||
* If this set makes any guarantees as to what order its elements
|
||||
* are returned by its iterator, this method must return the
|
||||
* elements in the same order.
|
||||
*
|
||||
* <p>The returned array will be "safe" in that no references to it
|
||||
* are maintained by this set. (In other words, this method must
|
||||
* allocate a new array even if this set is backed by an array).
|
||||
* The caller is thus free to modify the returned array.
|
||||
*
|
||||
* <p>This method acts as bridge between array-based and collection-based
|
||||
* APIs.
|
||||
*
|
||||
* @return an array containing all the elements in this set
|
||||
*/
|
||||
@Override
|
||||
public @NotNull Object @NotNull [] toArray() {
|
||||
return this.cache.asMap().keySet().toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array containing all of the elements in this set; the
|
||||
* runtime type of the returned array is that of the specified array.
|
||||
* If the set fits in the specified array, it is returned therein.
|
||||
* Otherwise, a new array is allocated with the runtime type of the
|
||||
* specified array and the size of this set.
|
||||
*
|
||||
* <p>If this set fits in the specified array with room to spare
|
||||
* (i.e., the array has more elements than this set), the element in
|
||||
* the array immediately following the end of the set is set to
|
||||
* {@code null}. (This is useful in determining the length of this
|
||||
* set <i>only</i> if the caller knows that this set does not contain
|
||||
* any null elements.)
|
||||
*
|
||||
* <p>If this set makes any guarantees as to what order its elements
|
||||
* are returned by its iterator, this method must return the elements
|
||||
* in the same order.
|
||||
*
|
||||
* <p>Like the {@link #toArray()} method, this method acts as bridge between
|
||||
* array-based and collection-based APIs. Further, this method allows
|
||||
* precise control over the runtime type of the output array, and may,
|
||||
* under certain circumstances, be used to save allocation costs.
|
||||
*
|
||||
* <p>Suppose {@code x} is a set known to contain only strings.
|
||||
* The following code can be used to dump the set into a newly allocated
|
||||
* array of {@code String}:
|
||||
*
|
||||
* <pre>
|
||||
* String[] y = x.toArray(new String[0]);</pre>
|
||||
* <p>
|
||||
* Note that {@code toArray(new Object[0])} is identical in function to
|
||||
* {@code toArray()}.
|
||||
*
|
||||
* @param a the array into which the elements of this set are to be
|
||||
* stored, if it is big enough; otherwise, a new array of the same
|
||||
* runtime type is allocated for this purpose.
|
||||
* @return an array containing all the elements in this set
|
||||
* @throws ArrayStoreException if the runtime type of the specified array
|
||||
* is not a supertype of the runtime type of every element in this
|
||||
* set
|
||||
* @throws NullPointerException if the specified array is null
|
||||
*/
|
||||
@Override
|
||||
public @NotNull <T> T @NotNull [] toArray(@NotNull T @NotNull [] a) {
|
||||
return this.cache.asMap().keySet().toArray(a);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the specified element to this set if it is not already present
|
||||
* (optional operation). More formally, adds the specified element
|
||||
* {@code e} to this set if the set contains no element {@code e2}
|
||||
* such that
|
||||
* {@code Objects.equals(e, e2)}.
|
||||
* If this set already contains the element, the call leaves the set
|
||||
* unchanged and returns {@code false}. In combination with the
|
||||
* restriction on constructors, this ensures that sets never contain
|
||||
* duplicate elements.
|
||||
*
|
||||
* <p>The stipulation above does not imply that sets must accept all
|
||||
* elements; sets may refuse to add any particular element, including
|
||||
* {@code null}, and throw an exception, as described in the
|
||||
* specification for {@link Collection#add Collection.add}.
|
||||
* Individual set implementations should clearly document any
|
||||
* restrictions on the elements that they may contain.
|
||||
*
|
||||
* @param item element to be added to this set
|
||||
* @return {@code true} if this set did not already contain the specified
|
||||
* element
|
||||
* @throws UnsupportedOperationException if the {@code add} operation
|
||||
* is not supported by this set
|
||||
* @throws ClassCastException if the class of the specified element
|
||||
* prevents it from being added to this set
|
||||
* @throws NullPointerException if the specified element is null and this
|
||||
* set does not permit null elements
|
||||
* @throws IllegalArgumentException if some property of the specified element
|
||||
* prevents it from being added to this set
|
||||
*/
|
||||
public boolean add(E item) {
|
||||
boolean containedBefore = contains(item);
|
||||
this.cache.put(item, PRESENT);
|
||||
return !containedBefore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the specified element from this set if it is present
|
||||
* (optional operation). More formally, removes an element {@code e}
|
||||
* such that
|
||||
* {@code Objects.equals(o, e)}, if
|
||||
* this set contains such an element. Returns {@code true} if this set
|
||||
* contained the element (or equivalently, if this set changed as a
|
||||
* result of the call). (This set will not contain the element once the
|
||||
* call returns.)
|
||||
*
|
||||
* @param o object to be removed from this set, if present
|
||||
* @return {@code true} if this set contained the specified element
|
||||
* @throws ClassCastException if the type of the specified element
|
||||
* is incompatible with this set
|
||||
* (<a href="Collection.html#optional-restrictions">optional</a>)
|
||||
* @throws NullPointerException if the specified element is null and this
|
||||
* set does not permit null elements
|
||||
* (<a href="Collection.html#optional-restrictions">optional</a>)
|
||||
* @throws UnsupportedOperationException if the {@code remove} operation
|
||||
* is not supported by this set
|
||||
*/
|
||||
@Override
|
||||
public boolean remove(Object o) {
|
||||
boolean present = contains(o);
|
||||
this.cache.invalidate(o);
|
||||
return present;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if this set contains all of the elements of the
|
||||
* specified collection. If the specified collection is also a set, this
|
||||
* method returns {@code true} if it is a <i>subset</i> of this set.
|
||||
*
|
||||
* @param c collection to be checked for containment in this set
|
||||
* @return {@code true} if this set contains all of the elements of the
|
||||
* specified collection
|
||||
* @throws ClassCastException if the types of one or more elements
|
||||
* in the specified collection are incompatible with this
|
||||
* set
|
||||
* (<a href="Collection.html#optional-restrictions">optional</a>)
|
||||
* @throws NullPointerException if the specified collection contains one
|
||||
* or more null elements and this set does not permit null
|
||||
* elements
|
||||
* (<a href="Collection.html#optional-restrictions">optional</a>),
|
||||
* or if the specified collection is null
|
||||
* @see #contains(Object)
|
||||
*/
|
||||
@Override
|
||||
public boolean containsAll(@NotNull Collection<?> c) {
|
||||
for (Object o : c) {
|
||||
if (!contains(o)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds all of the elements in the specified collection to this set if
|
||||
* they're not already present (optional operation). If the specified
|
||||
* collection is also a set, the {@code addAll} operation effectively
|
||||
* modifies this set so that its value is the <i>union</i> of the two
|
||||
* sets. The behavior of this operation is undefined if the specified
|
||||
* collection is modified while the operation is in progress.
|
||||
*
|
||||
* @param c collection containing elements to be added to this set
|
||||
* @return {@code true} if this set changed as a result of the call
|
||||
* @throws UnsupportedOperationException if the {@code addAll} operation
|
||||
* is not supported by this set
|
||||
* @throws ClassCastException if the class of an element of the
|
||||
* specified collection prevents it from being added to this set
|
||||
* @throws NullPointerException if the specified collection contains one
|
||||
* or more null elements and this set does not permit null
|
||||
* elements, or if the specified collection is null
|
||||
* @throws IllegalArgumentException if some property of an element of the
|
||||
* specified collection prevents it from being added to this set
|
||||
* @see #add(Object)
|
||||
*/
|
||||
@Override
|
||||
public boolean addAll(@NotNull Collection<? extends E> c) {
|
||||
boolean changed = false;
|
||||
for (E o : c) {
|
||||
if (add(o)) {
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retains only the elements in this set that are contained in the
|
||||
* specified collection (optional operation). In other words, removes
|
||||
* from this set all of its elements that are not contained in the
|
||||
* specified collection. If the specified collection is also a set, this
|
||||
* operation effectively modifies this set so that its value is the
|
||||
* <i>intersection</i> of the two sets.
|
||||
*
|
||||
* @param c collection containing elements to be retained in this set
|
||||
* @return {@code true} if this set changed as a result of the call
|
||||
* @throws UnsupportedOperationException if the {@code retainAll} operation
|
||||
* is not supported by this set
|
||||
* @throws ClassCastException if the class of an element of this set
|
||||
* is incompatible with the specified collection
|
||||
* (<a href="Collection.html#optional-restrictions">optional</a>)
|
||||
* @throws NullPointerException if this set contains a null element and the
|
||||
* specified collection does not permit null elements
|
||||
* (<a href="Collection.html#optional-restrictions">optional</a>),
|
||||
* or if the specified collection is null
|
||||
* @see #remove(Object)
|
||||
*/
|
||||
@Override
|
||||
public boolean retainAll(@NotNull Collection<?> c) {
|
||||
boolean changed = false;
|
||||
for (E e : this.cache.asMap().keySet()) {
|
||||
if (!c.contains(e) && remove(e)) {
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes from this set all of its elements that are contained in the
|
||||
* specified collection (optional operation). If the specified
|
||||
* collection is also a set, this operation effectively modifies this
|
||||
* set so that its value is the <i>asymmetric set difference</i> of
|
||||
* the two sets.
|
||||
*
|
||||
* @param c collection containing elements to be removed from this set
|
||||
* @return {@code true} if this set changed as a result of the call
|
||||
* @throws UnsupportedOperationException if the {@code removeAll} operation
|
||||
* is not supported by this set
|
||||
* @throws ClassCastException if the class of an element of this set
|
||||
* is incompatible with the specified collection
|
||||
* (<a href="Collection.html#optional-restrictions">optional</a>)
|
||||
* @throws NullPointerException if this set contains a null element and the
|
||||
* specified collection does not permit null elements
|
||||
* (<a href="Collection.html#optional-restrictions">optional</a>),
|
||||
* or if the specified collection is null
|
||||
* @see #remove(Object)
|
||||
* @see #contains(Object)
|
||||
*/
|
||||
@Override
|
||||
public boolean removeAll(@NotNull Collection<?> c) {
|
||||
boolean changed = false;
|
||||
for (E e : this.cache.asMap().keySet()) {
|
||||
if (remove(e)) {
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all of the elements from this set (optional operation).
|
||||
* The set will be empty after this call returns.
|
||||
*
|
||||
* @throws UnsupportedOperationException if the {@code clear} method
|
||||
* is not supported by this set
|
||||
*/
|
||||
@Override
|
||||
public void clear() {
|
||||
this.cache.invalidateAll();
|
||||
}
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
package me.xginko.villageroptimizer.utils;
|
||||
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Villager;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
public class CommonUtil {
|
||||
public static @NotNull String formatDuration(Duration duration) {
|
||||
final int seconds = duration.toSecondsPart();
|
||||
final int minutes = duration.toMinutesPart();
|
||||
final int hours = duration.toHoursPart();
|
||||
|
||||
if (hours > 0) {
|
||||
return String.format("%02dh %02dm %02ds", hours, minutes, seconds);
|
||||
} else if (minutes > 0) {
|
||||
return String.format("%02dm %02ds", minutes, seconds);
|
||||
} else {
|
||||
return String.format("%02ds", seconds);
|
||||
}
|
||||
}
|
||||
|
||||
public static String formatLocation(@NotNull Location location) {
|
||||
return "[" + location.getWorld().getName() + "] x=" + location.getBlockX() + ", y=" + location.getBlockY() + ", z=" + location.getBlockZ();
|
||||
}
|
||||
|
||||
private static boolean specificChunkLoadedMethodAvailable = true;
|
||||
public static boolean isEntitiesLoaded(@NotNull Chunk chunk) {
|
||||
if (!specificChunkLoadedMethodAvailable) {
|
||||
return chunk.isLoaded();
|
||||
}
|
||||
try {
|
||||
return chunk.isEntitiesLoaded();
|
||||
} catch (NoSuchMethodError e) {
|
||||
specificChunkLoadedMethodAvailable = false;
|
||||
return chunk.isLoaded();
|
||||
}
|
||||
}
|
||||
|
||||
public static void shakeHead(@NotNull Villager villager) {
|
||||
try {
|
||||
villager.shakeHead();
|
||||
} catch (NoSuchMethodError ignored) {}
|
||||
}
|
||||
|
||||
public static Villager.Profession getWorkstationProfession(@NotNull Material workstation) {
|
||||
return switch (workstation) {
|
||||
case BARREL -> Villager.Profession.FISHERMAN;
|
||||
case CARTOGRAPHY_TABLE -> Villager.Profession.CARTOGRAPHER;
|
||||
case SMOKER -> Villager.Profession.BUTCHER;
|
||||
case SMITHING_TABLE -> Villager.Profession.TOOLSMITH;
|
||||
case GRINDSTONE -> Villager.Profession.WEAPONSMITH;
|
||||
case BLAST_FURNACE -> Villager.Profession.ARMORER;
|
||||
case CAULDRON -> Villager.Profession.LEATHERWORKER;
|
||||
case BREWING_STAND -> Villager.Profession.CLERIC;
|
||||
case COMPOSTER -> Villager.Profession.FARMER;
|
||||
case FLETCHING_TABLE -> Villager.Profession.FLETCHER;
|
||||
case LOOM -> Villager.Profession.SHEPHERD;
|
||||
case LECTERN -> Villager.Profession.LIBRARIAN;
|
||||
case STONECUTTER -> Villager.Profession.MASON;
|
||||
default -> Villager.Profession.NONE;
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
package me.xginko.villageroptimizer.utils;
|
||||
|
||||
import me.xginko.villageroptimizer.VillagerOptimizer;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.TextReplacementConfig;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class KyoriUtil {
|
||||
|
||||
public static void sendMessage(@NotNull CommandSender sender, @NotNull Component message) {
|
||||
VillagerOptimizer.audiences().sender(sender).sendMessage(message);
|
||||
}
|
||||
|
||||
public static void sendActionBar(@NotNull CommandSender sender, @NotNull Component message) {
|
||||
VillagerOptimizer.audiences().sender(sender).sendActionBar(message);
|
||||
}
|
||||
|
||||
public static @NotNull Component toUpperCase(@NotNull Component input, @NotNull Locale locale) {
|
||||
return input.replaceText(TextReplacementConfig.builder()
|
||||
.match("(?s).*")
|
||||
.replacement((result, builder) -> builder.content(result.group(0).toUpperCase(locale)))
|
||||
.build());
|
||||
}
|
||||
|
||||
public static @NotNull String translateChatColor(@NotNull String string) {
|
||||
string = string.replace("&0", "<black>");
|
||||
string = string.replace("&1", "<dark_blue>");
|
||||
string = string.replace("&2", "<dark_green>");
|
||||
string = string.replace("&3", "<dark_aqua>");
|
||||
string = string.replace("&4", "<dark_red>");
|
||||
string = string.replace("&5", "<dark_purple>");
|
||||
string = string.replace("&6", "<gold>");
|
||||
string = string.replace("&7", "<gray>");
|
||||
string = string.replace("&8", "<dark_gray>");
|
||||
string = string.replace("&9", "<blue>");
|
||||
string = string.replace("&a", "<green>");
|
||||
string = string.replace("&b", "<aqua>");
|
||||
string = string.replace("&c", "<red>");
|
||||
string = string.replace("&d", "<light_purple>");
|
||||
string = string.replace("&e", "<yellow>");
|
||||
string = string.replace("&f", "<white>");
|
||||
string = string.replace("&k", "<obfuscated>");
|
||||
string = string.replace("&l", "<bold>");
|
||||
string = string.replace("&m", "<strikethrough>");
|
||||
string = string.replace("&n", "<underlined>");
|
||||
string = string.replace("&o", "<italic>");
|
||||
string = string.replace("&r", "<reset>");
|
||||
return string;
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
package me.xginko.villageroptimizer.utils;
|
||||
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.util.NumberConversions;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class LocationUtil {
|
||||
|
||||
public static @NotNull String toString(@NotNull Location location) {
|
||||
return "[" + location.getWorld().getName() + "] x=" + location.getBlockX() + ", y=" + location.getBlockY() + ", z=" + location.getBlockZ();
|
||||
}
|
||||
|
||||
public static double relDistance2DSquared(@NotNull Location from, @NotNull Location to) {
|
||||
double toX = to.getX();
|
||||
double toZ = to.getZ();
|
||||
double fromX = from.getX();
|
||||
double fromZ = from.getZ();
|
||||
|
||||
// Make sure distance is relative since one block in the nether equates to 8 in the overworld/end
|
||||
if (to.getWorld().getEnvironment() != from.getWorld().getEnvironment()) {
|
||||
if (from.getWorld().getEnvironment() == World.Environment.NETHER) {
|
||||
fromX *= 8;
|
||||
fromZ *= 8;
|
||||
}
|
||||
if (to.getWorld().getEnvironment() == World.Environment.NETHER) {
|
||||
toX *= 8;
|
||||
toZ *= 8;
|
||||
}
|
||||
}
|
||||
|
||||
return NumberConversions.square(toX - fromX) + NumberConversions.square(toZ - fromZ);
|
||||
}
|
||||
|
||||
public static double relDistance3DSquared(@NotNull Location from, @NotNull Location to) {
|
||||
double toY = to.getY();
|
||||
double fromY = from.getY();
|
||||
|
||||
// Clamp Y levels the same way minecraft would for portal creation logic
|
||||
if (fromY < to.getWorld().getMinHeight())
|
||||
fromY = to.getWorld().getMinHeight();
|
||||
if (fromY > to.getWorld().getMaxHeight())
|
||||
fromY = to.getWorld().getMaxHeight();
|
||||
if (toY < from.getWorld().getMinHeight())
|
||||
toY = from.getWorld().getMinHeight();
|
||||
if (toY > from.getWorld().getMaxHeight())
|
||||
toY = from.getWorld().getMaxHeight();
|
||||
|
||||
return relDistance2DSquared(from, to) + NumberConversions.square(toY - fromY);
|
||||
}
|
||||
|
||||
public static double relDistance2D(@NotNull Location from, @NotNull Location to) {
|
||||
return Math.sqrt(relDistance2DSquared(from, to));
|
||||
}
|
||||
|
||||
public static double relDistance3D(@NotNull Location from, @NotNull Location to) {
|
||||
return Math.sqrt(relDistance3DSquared(from, to));
|
||||
}
|
||||
}
|
97
src/main/java/me/xginko/villageroptimizer/utils/Util.java
Normal file
97
src/main/java/me/xginko/villageroptimizer/utils/Util.java
Normal file
@ -0,0 +1,97 @@
|
||||
package me.xginko.villageroptimizer.utils;
|
||||
|
||||
import com.cryptomorin.xseries.XMaterial;
|
||||
import net.kyori.adventure.text.format.Style;
|
||||
import net.kyori.adventure.text.format.TextColor;
|
||||
import net.kyori.adventure.text.format.TextDecoration;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Villager;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
public class Util {
|
||||
|
||||
public static final @NotNull TextColor PL_COLOR;
|
||||
public static final @NotNull Style PL_STYLE;
|
||||
private static final @NotNull Map<Material, Villager.Profession> PROFESSION_MAP;
|
||||
private static boolean canUseIsEntitiesLoaded;
|
||||
|
||||
static {
|
||||
PL_COLOR = TextColor.color(102,255,230);
|
||||
PL_STYLE = Style.style(PL_COLOR, TextDecoration.BOLD);
|
||||
|
||||
PROFESSION_MAP = new HashMap<>();
|
||||
PROFESSION_MAP.put(XMaterial.LOOM.parseMaterial(), Villager.Profession.SHEPHERD);
|
||||
PROFESSION_MAP.put(XMaterial.BARREL.parseMaterial(), Villager.Profession.FISHERMAN);
|
||||
PROFESSION_MAP.put(XMaterial.SMOKER.parseMaterial(), Villager.Profession.BUTCHER);
|
||||
PROFESSION_MAP.put(XMaterial.LECTERN.parseMaterial(), Villager.Profession.LIBRARIAN);
|
||||
PROFESSION_MAP.put(XMaterial.CAULDRON.parseMaterial(), Villager.Profession.LEATHERWORKER);
|
||||
PROFESSION_MAP.put(XMaterial.COMPOSTER.parseMaterial(), Villager.Profession.FARMER);
|
||||
PROFESSION_MAP.put(XMaterial.GRINDSTONE.parseMaterial(), Villager.Profession.WEAPONSMITH);
|
||||
PROFESSION_MAP.put(XMaterial.STONECUTTER.parseMaterial(), Villager.Profession.MASON);
|
||||
PROFESSION_MAP.put(XMaterial.BREWING_STAND.parseMaterial(), Villager.Profession.CLERIC);
|
||||
PROFESSION_MAP.put(XMaterial.BLAST_FURNACE.parseMaterial(), Villager.Profession.ARMORER);
|
||||
PROFESSION_MAP.put(XMaterial.SMITHING_TABLE.parseMaterial(), Villager.Profession.TOOLSMITH);
|
||||
PROFESSION_MAP.put(XMaterial.FLETCHING_TABLE.parseMaterial(), Villager.Profession.FLETCHER);
|
||||
PROFESSION_MAP.put(XMaterial.CARTOGRAPHY_TABLE.parseMaterial(), Villager.Profession.CARTOGRAPHER);
|
||||
|
||||
try {
|
||||
Chunk.class.getMethod("isEntitiesLoaded");
|
||||
canUseIsEntitiesLoaded = true;
|
||||
} catch (NoSuchMethodException e) {
|
||||
canUseIsEntitiesLoaded = false;
|
||||
}
|
||||
}
|
||||
|
||||
public static @Nullable Villager.Profession getWorkstationProfession(@NotNull Material workstation) {
|
||||
return PROFESSION_MAP.getOrDefault(workstation, null);
|
||||
}
|
||||
|
||||
public static boolean isChunkLoaded(@NotNull Chunk chunk) {
|
||||
return canUseIsEntitiesLoaded ? chunk.isEntitiesLoaded() : chunk.isLoaded();
|
||||
}
|
||||
|
||||
public static @NotNull String formatDuration(@NotNull Duration duration) {
|
||||
if (duration.isNegative()) duration = duration.negated();
|
||||
|
||||
final int seconds = (int) (duration.getSeconds() % 60);
|
||||
final int minutes = (int) (duration.toMinutes() % 60);
|
||||
final int hours = (int) (duration.toHours() % 24);
|
||||
|
||||
if (hours > 0) {
|
||||
return String.format("%02dh %02dm %02ds", hours, minutes, seconds);
|
||||
} else if (minutes > 0) {
|
||||
return String.format("%02dm %02ds", minutes, seconds);
|
||||
} else {
|
||||
return String.format("%02ds", seconds);
|
||||
}
|
||||
}
|
||||
|
||||
public static @NotNull String toNiceString(@NotNull Object input) {
|
||||
// Get name
|
||||
String name;
|
||||
if (input instanceof Enum<?>) {
|
||||
name = ((Enum<?>) input).name();
|
||||
} else {
|
||||
name = input.toString();
|
||||
}
|
||||
|
||||
// Turn something like "REDSTONE_TORCH" into "redstone torch"
|
||||
String[] lowercaseWords = name.toLowerCase(Locale.ROOT).split("_");
|
||||
|
||||
// Capitalize first letter for each word
|
||||
for (int i = 0; i < lowercaseWords.length; i++) {
|
||||
String word = lowercaseWords[i];
|
||||
lowercaseWords[i] = word.substring(0, 1).toUpperCase() + word.substring(1);
|
||||
}
|
||||
|
||||
// return as nice string
|
||||
return String.join(" ", lowercaseWords);
|
||||
}
|
||||
}
|
@ -0,0 +1,100 @@
|
||||
package me.xginko.villageroptimizer.wrapper;
|
||||
|
||||
import me.xginko.villageroptimizer.VillagerOptimizer;
|
||||
import me.xginko.villageroptimizer.struct.enums.Keyring;
|
||||
import me.xginko.villageroptimizer.struct.enums.OptimizationType;
|
||||
import org.bukkit.entity.Villager;
|
||||
import org.bukkit.persistence.PersistentDataContainer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public abstract class PDCWrapper {
|
||||
|
||||
public final Villager villager;
|
||||
public final PersistentDataContainer dataContainer;
|
||||
|
||||
public PDCWrapper(Villager villager) {
|
||||
this.villager = villager;
|
||||
this.dataContainer = villager.getPersistentDataContainer();
|
||||
}
|
||||
|
||||
public static PDCWrapper[] forVillager(Villager villager) {
|
||||
if (VillagerOptimizer.config().support_other_plugins) {
|
||||
return new PDCWrapper[]{new PDCWrapperVO(villager), new PDCWrapperAVL(villager)};
|
||||
} else {
|
||||
return new PDCWrapper[]{new PDCWrapperVO(villager)};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The namespace of the handler
|
||||
*/
|
||||
public abstract Keyring.Space getSpace();
|
||||
|
||||
/**
|
||||
* @return True if the villager is optimized by plugin, otherwise false.
|
||||
*/
|
||||
public abstract boolean isOptimized();
|
||||
|
||||
/**
|
||||
* @param cooldown_millis The configured cooldown in millis until the next optimization is allowed to occur.
|
||||
* @return True if villager can be optimized again, otherwise false.
|
||||
*/
|
||||
public abstract boolean canOptimize(long cooldown_millis);
|
||||
|
||||
/**
|
||||
* @param type OptimizationType the villager should be set to.
|
||||
*/
|
||||
public abstract void setOptimizationType(OptimizationType type);
|
||||
|
||||
/**
|
||||
* @return The current OptimizationType of the villager.
|
||||
*/
|
||||
@NotNull
|
||||
public abstract OptimizationType getOptimizationType();
|
||||
|
||||
/**
|
||||
* Saves the system time when the villager was last optimized.
|
||||
*/
|
||||
public abstract void saveOptimizeTime();
|
||||
|
||||
/**
|
||||
* For convenience so the remaining millis since the last stored optimize time
|
||||
* can be easily calculated.
|
||||
* This enables new configured cooldowns to instantly apply instead of them being persistent.
|
||||
*
|
||||
* @param cooldown_millis The configured cooldown in milliseconds you want to check against.
|
||||
* @return The time left in millis until the villager can be optimized again.
|
||||
*/
|
||||
public abstract long getOptimizeCooldownMillis(long cooldown_millis);
|
||||
|
||||
/**
|
||||
* Gets the time of the day in ticks when the entity was last restocked.
|
||||
* This value is affected by /time set
|
||||
* @return The time of the minecraft day (in ticks) when the villager was last restocked
|
||||
*/
|
||||
public abstract long getLastRestockFullTime();
|
||||
|
||||
/**
|
||||
* Saves the time of when the entity was last restocked.
|
||||
*/
|
||||
public abstract void saveRestockTime();
|
||||
|
||||
/**
|
||||
* @param cooldown_millis The configured cooldown in milliseconds you want to check against.
|
||||
* @return Whether the villager can be leveled up or not with the checked milliseconds
|
||||
*/
|
||||
public abstract boolean canLevelUp(long cooldown_millis);
|
||||
|
||||
/**
|
||||
* Saves the time of the in-game world when the entity was last leveled up.
|
||||
*/
|
||||
public abstract void saveLastLevelUp();
|
||||
|
||||
/**
|
||||
* Here for convenience so the remaining millis since the last stored level-up time
|
||||
* can be easily calculated.
|
||||
*
|
||||
* @return The time of the in-game world when the entity was last leveled up.
|
||||
*/
|
||||
public abstract long getLevelCooldownMillis(long cooldown_millis);
|
||||
}
|
@ -0,0 +1,131 @@
|
||||
package me.xginko.villageroptimizer.wrapper;
|
||||
|
||||
import me.xginko.villageroptimizer.VillagerOptimizer;
|
||||
import me.xginko.villageroptimizer.struct.enums.Keyring;
|
||||
import me.xginko.villageroptimizer.struct.enums.OptimizationType;
|
||||
import org.bukkit.entity.Villager;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public final class PDCWrapperAVL extends PDCWrapper {
|
||||
|
||||
PDCWrapperAVL(@NotNull Villager villager) {
|
||||
super(villager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Keyring.Space getSpace() {
|
||||
return Keyring.Space.AntiVillagerLag;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOptimized() {
|
||||
return dataContainer.has(Keyring.AntiVillagerLag.OPTIMIZED_ANY.getKey(), PersistentDataType.STRING)
|
||||
|| dataContainer.has(Keyring.AntiVillagerLag.OPTIMIZED_WORKSTATION.getKey(), PersistentDataType.STRING)
|
||||
|| dataContainer.has(Keyring.AntiVillagerLag.OPTIMIZED_BLOCK.getKey(), PersistentDataType.STRING);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canOptimize(long cooldown_millis) {
|
||||
return !dataContainer.has(Keyring.AntiVillagerLag.NEXT_OPTIMIZATION_SYSTIME_SECONDS.getKey(), PersistentDataType.LONG)
|
||||
|| System.currentTimeMillis() > TimeUnit.SECONDS.toMillis(dataContainer.get(Keyring.AntiVillagerLag.NEXT_OPTIMIZATION_SYSTIME_SECONDS.getKey(), PersistentDataType.LONG));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOptimizationType(OptimizationType type) {
|
||||
VillagerOptimizer.scheduling().entitySpecificScheduler(villager).runAtFixedRate(setOptimization -> {
|
||||
// Keep repeating task until villager is no longer trading with a player
|
||||
if (villager.isTrading()) return;
|
||||
|
||||
if (type == OptimizationType.NONE) {
|
||||
if (dataContainer.has(Keyring.AntiVillagerLag.OPTIMIZED_ANY.getKey(), PersistentDataType.STRING))
|
||||
dataContainer.remove(Keyring.AntiVillagerLag.OPTIMIZED_ANY.getKey());
|
||||
if (dataContainer.has(Keyring.AntiVillagerLag.OPTIMIZED_WORKSTATION.getKey(), PersistentDataType.STRING))
|
||||
dataContainer.remove(Keyring.AntiVillagerLag.OPTIMIZED_WORKSTATION.getKey());
|
||||
if (dataContainer.has(Keyring.AntiVillagerLag.OPTIMIZED_BLOCK.getKey(), PersistentDataType.STRING))
|
||||
dataContainer.remove(Keyring.AntiVillagerLag.OPTIMIZED_BLOCK.getKey());
|
||||
|
||||
villager.setAware(true);
|
||||
villager.setAI(true);
|
||||
} else {
|
||||
switch (type) {
|
||||
case BLOCK:
|
||||
dataContainer.set(Keyring.AntiVillagerLag.OPTIMIZED_BLOCK.getKey(), PersistentDataType.STRING, Keyring.AntiVillagerLag.OPTIMIZED_BLOCK.getKey().toString());
|
||||
break;
|
||||
case WORKSTATION:
|
||||
dataContainer.set(Keyring.AntiVillagerLag.OPTIMIZED_WORKSTATION.getKey(), PersistentDataType.STRING, Keyring.AntiVillagerLag.OPTIMIZED_WORKSTATION.getKey().toString());
|
||||
break;
|
||||
case COMMAND:
|
||||
case NAMETAG:
|
||||
dataContainer.set(Keyring.AntiVillagerLag.OPTIMIZED_ANY.getKey(), PersistentDataType.STRING, "AVL");
|
||||
break;
|
||||
}
|
||||
|
||||
villager.setAware(false);
|
||||
}
|
||||
|
||||
// End repeating task once logic is finished
|
||||
setOptimization.cancel();
|
||||
}, null, 1L, 20L);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull OptimizationType getOptimizationType() {
|
||||
if (dataContainer.has(Keyring.AntiVillagerLag.OPTIMIZED_BLOCK.getKey(), PersistentDataType.STRING)) {
|
||||
return OptimizationType.BLOCK;
|
||||
}
|
||||
if (dataContainer.has(Keyring.AntiVillagerLag.OPTIMIZED_WORKSTATION.getKey(), PersistentDataType.STRING)) {
|
||||
return OptimizationType.WORKSTATION;
|
||||
}
|
||||
if (dataContainer.has(Keyring.AntiVillagerLag.OPTIMIZED_ANY.getKey(), PersistentDataType.STRING)) {
|
||||
return OptimizationType.COMMAND; // Best we can do
|
||||
}
|
||||
return OptimizationType.NONE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveOptimizeTime() {
|
||||
// We do nothing here to not break stuff
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getOptimizeCooldownMillis(long cooldown_millis) {
|
||||
if (dataContainer.has(Keyring.AntiVillagerLag.NEXT_OPTIMIZATION_SYSTIME_SECONDS.getKey(), PersistentDataType.LONG)) {
|
||||
return TimeUnit.SECONDS.toMillis(dataContainer.get(Keyring.AntiVillagerLag.NEXT_OPTIMIZATION_SYSTIME_SECONDS.getKey(), PersistentDataType.LONG) - System.currentTimeMillis());
|
||||
}
|
||||
return cooldown_millis;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getLastRestockFullTime() {
|
||||
if (dataContainer.has(Keyring.AntiVillagerLag.LAST_RESTOCK_WORLD_FULLTIME.getKey(), PersistentDataType.LONG)) {
|
||||
return dataContainer.get(Keyring.AntiVillagerLag.LAST_RESTOCK_WORLD_FULLTIME.getKey(), PersistentDataType.LONG);
|
||||
}
|
||||
return 0L;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveRestockTime() {
|
||||
dataContainer.set(Keyring.AntiVillagerLag.LAST_RESTOCK_WORLD_FULLTIME.getKey(), PersistentDataType.LONG, villager.getWorld().getFullTime());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canLevelUp(long cooldown_millis) {
|
||||
return !dataContainer.has(Keyring.AntiVillagerLag.NEXT_LEVELUP_SYSTIME_SECONDS.getKey(), PersistentDataType.LONG)
|
||||
|| System.currentTimeMillis() > TimeUnit.SECONDS.toMillis(dataContainer.get(Keyring.AntiVillagerLag.NEXT_LEVELUP_SYSTIME_SECONDS.getKey(), PersistentDataType.LONG));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveLastLevelUp() {
|
||||
// We do nothing here to not break stuff
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getLevelCooldownMillis(long cooldown_millis) {
|
||||
if (dataContainer.has(Keyring.AntiVillagerLag.NEXT_LEVELUP_SYSTIME_SECONDS.getKey(), PersistentDataType.LONG))
|
||||
return System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(dataContainer.get(Keyring.AntiVillagerLag.NEXT_LEVELUP_SYSTIME_SECONDS.getKey(), PersistentDataType.LONG));
|
||||
return cooldown_millis;
|
||||
}
|
||||
}
|
@ -0,0 +1,122 @@
|
||||
package me.xginko.villageroptimizer.wrapper;
|
||||
|
||||
import me.xginko.villageroptimizer.VillagerOptimizer;
|
||||
import me.xginko.villageroptimizer.struct.enums.Keyring;
|
||||
import me.xginko.villageroptimizer.struct.enums.OptimizationType;
|
||||
import org.bukkit.entity.Villager;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public final class PDCWrapperVO extends PDCWrapper {
|
||||
|
||||
PDCWrapperVO(@NotNull Villager villager) {
|
||||
super(villager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Keyring.Space getSpace() {
|
||||
return Keyring.Space.VillagerOptimizer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOptimized() {
|
||||
return dataContainer.has(Keyring.VillagerOptimizer.OPTIMIZATION_TYPE.getKey(), PersistentDataType.STRING);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canOptimize(long cooldown_millis) {
|
||||
return System.currentTimeMillis() > getLastOptimize() + cooldown_millis;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOptimizationType(OptimizationType type) {
|
||||
VillagerOptimizer.scheduling().entitySpecificScheduler(villager).runAtFixedRate(setOptimization -> {
|
||||
// Keep repeating task until villager is no longer trading with a player
|
||||
if (villager.isTrading()) return;
|
||||
|
||||
if (type == OptimizationType.NONE) {
|
||||
if (isOptimized())
|
||||
dataContainer.remove(Keyring.VillagerOptimizer.OPTIMIZATION_TYPE.getKey());
|
||||
villager.setAware(true);
|
||||
villager.setAI(true);
|
||||
} else {
|
||||
dataContainer.set(Keyring.VillagerOptimizer.OPTIMIZATION_TYPE.getKey(), PersistentDataType.STRING, type.name());
|
||||
villager.setAware(false);
|
||||
}
|
||||
|
||||
// End repeating task once logic is finished
|
||||
setOptimization.cancel();
|
||||
}, null, 1L, 20L);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull OptimizationType getOptimizationType() {
|
||||
if (isOptimized()) {
|
||||
return OptimizationType.valueOf(dataContainer.get(Keyring.VillagerOptimizer.OPTIMIZATION_TYPE.getKey(), PersistentDataType.STRING));
|
||||
} else {
|
||||
return OptimizationType.NONE;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveOptimizeTime() {
|
||||
dataContainer.set(Keyring.VillagerOptimizer.LAST_OPTIMIZE_SYSTIME_MILLIS.getKey(), PersistentDataType.LONG, System.currentTimeMillis());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The system time in millis when the villager was last optimized, 0L if the villager was never optimized.
|
||||
*/
|
||||
private long getLastOptimize() {
|
||||
if (dataContainer.has(Keyring.VillagerOptimizer.LAST_OPTIMIZE_SYSTIME_MILLIS.getKey(), PersistentDataType.LONG)) {
|
||||
return dataContainer.get(Keyring.VillagerOptimizer.LAST_OPTIMIZE_SYSTIME_MILLIS.getKey(), PersistentDataType.LONG);
|
||||
}
|
||||
return 0L;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getOptimizeCooldownMillis(long cooldown_millis) {
|
||||
if (getLastOptimize() > 0L) {
|
||||
return cooldown_millis - (System.currentTimeMillis() - getLastOptimize());
|
||||
}
|
||||
return cooldown_millis;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getLastRestockFullTime() {
|
||||
if (dataContainer.has(Keyring.VillagerOptimizer.LAST_RESTOCK_WORLD_FULLTIME.getKey(), PersistentDataType.LONG)) {
|
||||
return dataContainer.get(Keyring.VillagerOptimizer.LAST_RESTOCK_WORLD_FULLTIME.getKey(), PersistentDataType.LONG);
|
||||
}
|
||||
return 0L;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveRestockTime() {
|
||||
dataContainer.set(Keyring.VillagerOptimizer.LAST_RESTOCK_WORLD_FULLTIME.getKey(), PersistentDataType.LONG, villager.getWorld().getFullTime());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canLevelUp(long cooldown_millis) {
|
||||
return System.currentTimeMillis() >= getLastLevelUpTime() + cooldown_millis;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveLastLevelUp() {
|
||||
dataContainer.set(Keyring.VillagerOptimizer.LAST_LEVELUP_SYSTIME_MILLIS.getKey(), PersistentDataType.LONG, System.currentTimeMillis());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The systime in millis when the entity was last leveled up.
|
||||
*/
|
||||
private long getLastLevelUpTime() {
|
||||
if (dataContainer.has(Keyring.VillagerOptimizer.LAST_LEVELUP_SYSTIME_MILLIS.getKey(), PersistentDataType.LONG))
|
||||
return dataContainer.get(Keyring.VillagerOptimizer.LAST_LEVELUP_SYSTIME_MILLIS.getKey(), PersistentDataType.LONG);
|
||||
return 0L;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getLevelCooldownMillis(long cooldown_millis) {
|
||||
if (dataContainer.has(Keyring.VillagerOptimizer.LAST_LEVELUP_SYSTIME_MILLIS.getKey(), PersistentDataType.LONG))
|
||||
return System.currentTimeMillis() - (dataContainer.get(Keyring.VillagerOptimizer.LAST_LEVELUP_SYSTIME_MILLIS.getKey(), PersistentDataType.LONG) + cooldown_millis);
|
||||
return cooldown_millis;
|
||||
}
|
||||
}
|
@ -0,0 +1,192 @@
|
||||
package me.xginko.villageroptimizer.wrapper;
|
||||
|
||||
import me.xginko.villageroptimizer.VillagerOptimizer;
|
||||
import me.xginko.villageroptimizer.struct.enums.Keyring;
|
||||
import me.xginko.villageroptimizer.struct.enums.OptimizationType;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.entity.Villager;
|
||||
import org.bukkit.entity.memory.MemoryKey;
|
||||
import org.bukkit.event.entity.VillagerReplenishTradeEvent;
|
||||
import org.bukkit.inventory.MerchantRecipe;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class WrappedVillager extends PDCWrapper {
|
||||
|
||||
private final @NotNull PDCWrapper[] pdcWrappers;
|
||||
|
||||
public WrappedVillager(@NotNull Villager villager) {
|
||||
super(villager);
|
||||
this.pdcWrappers = PDCWrapper.forVillager(villager);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a number between 0 and 24000
|
||||
* is affected by /time set
|
||||
*/
|
||||
public long currentDayTimeTicks() {
|
||||
return villager.getWorld().getTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the tick time of the world
|
||||
* is affected by /time set
|
||||
*/
|
||||
public long currentFullTimeTicks() {
|
||||
return villager.getWorld().getFullTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* Restock all trading recipes.
|
||||
*/
|
||||
public void restock() {
|
||||
VillagerOptimizer.scheduling().entitySpecificScheduler(villager).run(() -> {
|
||||
for (MerchantRecipe merchantRecipe : villager.getRecipes()) {
|
||||
VillagerReplenishTradeEvent restockRecipeEvent = new VillagerReplenishTradeEvent(villager, merchantRecipe);
|
||||
if (restockRecipeEvent.callEvent()) {
|
||||
restockRecipeEvent.getRecipe().setUses(0);
|
||||
}
|
||||
}
|
||||
}, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The level between 1-5 calculated from the villagers experience.
|
||||
*/
|
||||
public int calculateLevel() {
|
||||
// https://minecraft.fandom.com/wiki/Trading#Mechanics
|
||||
int vilEXP = villager.getVillagerExperience();
|
||||
if (vilEXP >= 250) return 5;
|
||||
if (vilEXP >= 150) return 4;
|
||||
if (vilEXP >= 70) return 3;
|
||||
if (vilEXP >= 10) return 2;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if the villager can lose its acquired profession by having its workstation destroyed.
|
||||
*/
|
||||
public boolean canLooseProfession() {
|
||||
// A villager with a level of 1 and no trading experience is liable to lose its profession.
|
||||
return villager.getVillagerLevel() <= 1 && villager.getVillagerExperience() <= 0;
|
||||
}
|
||||
|
||||
public void sayNo() {
|
||||
try {
|
||||
villager.shakeHead();
|
||||
} catch (NoSuchMethodError e) {
|
||||
villager.getWorld().playSound(villager.getEyeLocation(), Sound.ENTITY_VILLAGER_NO, 1.0F, 1.0F);
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable Location getJobSite() {
|
||||
return villager.getMemory(MemoryKey.JOB_SITE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Keyring.Space getSpace() {
|
||||
return Keyring.Space.VillagerOptimizer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOptimized() {
|
||||
for (PDCWrapper pdcWrapper : pdcWrappers) {
|
||||
if (pdcWrapper.isOptimized()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canOptimize(long cooldown_millis) {
|
||||
for (PDCWrapper pdcWrapper : pdcWrappers) {
|
||||
if (!pdcWrapper.canOptimize(cooldown_millis)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOptimizationType(OptimizationType type) {
|
||||
for (PDCWrapper pdcWrapper : pdcWrappers) {
|
||||
pdcWrapper.setOptimizationType(type);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull OptimizationType getOptimizationType() {
|
||||
OptimizationType result = OptimizationType.NONE;
|
||||
for (PDCWrapper pdcWrapper : pdcWrappers) {
|
||||
OptimizationType type = pdcWrapper.getOptimizationType();
|
||||
if (type != OptimizationType.NONE) {
|
||||
if (pdcWrapper.getSpace() == Keyring.Space.VillagerOptimizer) {
|
||||
return type;
|
||||
} else {
|
||||
result = type;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveOptimizeTime() {
|
||||
for (PDCWrapper pdcWrapper : pdcWrappers) {
|
||||
pdcWrapper.saveOptimizeTime();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getOptimizeCooldownMillis(long cooldown_millis) {
|
||||
long cooldown = 0L;
|
||||
for (PDCWrapper pdcWrapper : pdcWrappers) {
|
||||
cooldown = Math.max(cooldown, pdcWrapper.getOptimizeCooldownMillis(cooldown_millis));
|
||||
}
|
||||
return cooldown;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getLastRestockFullTime() {
|
||||
long cooldown = 0L;
|
||||
for (PDCWrapper pdcWrapper : pdcWrappers) {
|
||||
cooldown = Math.max(cooldown, pdcWrapper.getLastRestockFullTime());
|
||||
}
|
||||
return cooldown;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveRestockTime() {
|
||||
for (PDCWrapper pdcWrapper : pdcWrappers) {
|
||||
pdcWrapper.saveRestockTime();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canLevelUp(long cooldown_millis) {
|
||||
for (PDCWrapper pdcWrapper : pdcWrappers) {
|
||||
if (!pdcWrapper.canLevelUp(cooldown_millis)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveLastLevelUp() {
|
||||
for (PDCWrapper pdcWrapper : pdcWrappers) {
|
||||
pdcWrapper.saveLastLevelUp();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getLevelCooldownMillis(long cooldown_millis) {
|
||||
long cooldown = cooldown_millis;
|
||||
for (PDCWrapper pdcWrapper : pdcWrappers) {
|
||||
cooldown = Math.max(cooldown, pdcWrapper.getLevelCooldownMillis(cooldown_millis));
|
||||
}
|
||||
return cooldown;
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
me.xginko.villageroptimizer.logging.ComponentLoggerProviderImpl
|
44
src/main/resources/lang/ko_kr.yml
Normal file
44
src/main/resources/lang/ko_kr.yml
Normal file
@ -0,0 +1,44 @@
|
||||
messages:
|
||||
no-permission: <red>이 명령을 사용할 권한이 없습니다.
|
||||
trades-restocked:
|
||||
- <green>모든 거래가 재입고되었습니다! 다음 재입고까지 %time% 남았습니다.
|
||||
optimize-to-trade:
|
||||
- <red>거래하기 전에 이 주민을 최적화해야 합니다.
|
||||
villager-leveling-up:
|
||||
- <yellow>주민이 현재 레벨 업 중입니다! %time% 후에 주민을 다시 사용할 수 있습니다.
|
||||
nametag:
|
||||
optimize-success:
|
||||
- <green>네임태그을 사용하여 주민을 성공적으로 최적화했습니다.
|
||||
optimize-on-cooldown:
|
||||
- <gray>%time% 후에 이 주민을 다시 최적화할 수 있습니다.
|
||||
unoptimize-success:
|
||||
- <green>네임택을 제거하여 주민을 성공적으로 최적화 해제했습니다.
|
||||
block:
|
||||
optimize-success:
|
||||
- <green>%blocktype% 블록을 사용하여 %vil_profession% 주민을 성공적으로 최적화했습니다.
|
||||
optimize-on-cooldown:
|
||||
- <gray>%time% 후에 이 주민을 다시 최적화할 수 있습니다.
|
||||
unoptimize-success:
|
||||
- <green>%blocktype% 블록을 제거하여 %vil_profession% 주민을 성공적으로 최적화 해제했습니다.
|
||||
workstation:
|
||||
optimize-success:
|
||||
- <green>%blocktype% 작업대 블록을 사용하여 %vil_profession% 주민을 성공적으로 최적화했습니다.
|
||||
optimize-on-cooldown:
|
||||
- <gray>%time% 후에 이 주민을 다시 최적화할 수 있습니다.
|
||||
unoptimize-success:
|
||||
- <green>%blocktype% 작업대 블록을 제거하여 주민을 성공적으로 최적화 해제했습니다.
|
||||
command:
|
||||
optimize-success:
|
||||
- <green>%radius% 블록 반경 내 %amount% 개의 주민을 성공적으로 최적화했습니다.
|
||||
radius-limit-exceed:
|
||||
- <red>입력한 반경이 %distance% 블록의 제한을 초과합니다.
|
||||
optimize-fail:
|
||||
- <gray>%amount% 개의 주민이 최근에 이미 최적화되어 최적화할 수 없습니다.
|
||||
unoptimize-success:
|
||||
- <green>%radius% 블록 반경 내 %amount% 개의 주민을 성공적으로 최적화 해제했습니다.
|
||||
specify-radius:
|
||||
- <red>반경을 지정하세요.
|
||||
radius-invalid:
|
||||
- <red>입력한 반경이 유효한 숫자가 아닙니다. 다시 시도하세요.
|
||||
no-villagers-nearby:
|
||||
- <gray>%radius% 블록 반경 내에서 사용 중인 주민을 찾을 수 없습니다.
|
44
src/main/resources/lang/zh_cn.yml
Normal file
44
src/main/resources/lang/zh_cn.yml
Normal file
@ -0,0 +1,44 @@
|
||||
messages:
|
||||
no-permission: <red>您没有使用此命令的权限。
|
||||
trades-restocked:
|
||||
- <green>所有交易已经重新补充!下次补货时间为 %time%。
|
||||
optimize-to-trade:
|
||||
- <red>在与村民交易之前,您需要优化该村民。
|
||||
villager-leveling-up:
|
||||
- <yellow>村民正在升级中!您可以在 %time% 后再次与村民交互。
|
||||
nametag:
|
||||
optimize-success:
|
||||
- <green>成功使用名牌优化了村民。
|
||||
optimize-on-cooldown:
|
||||
- <gray>您需要等待 %time% 才能再次优化该村民。
|
||||
unoptimize-success:
|
||||
- <green>成功使用名牌取消了村民的优化状态。
|
||||
block:
|
||||
optimize-success:
|
||||
- <green>%vil_profession% 村民成功使用 %blocktype% 方块进行了优化。
|
||||
optimize-on-cooldown:
|
||||
- <gray>您需要等待 %time% 才能再次优化该村民。
|
||||
unoptimize-success:
|
||||
- <green>成功移除了 %blocktype% 方块,取消了 %vil_profession% 村民的优化状态。
|
||||
workstation:
|
||||
optimize-success:
|
||||
- <green>%vil_profession% 村民成功使用工作站方块 %blocktype% 进行了优化。
|
||||
optimize-on-cooldown:
|
||||
- <gray>您需要等待 %time% 才能再次优化该村民。
|
||||
unoptimize-success:
|
||||
- <green>成功移除了工作站方块 %blocktype%,取消了村民的优化状态。
|
||||
command:
|
||||
optimize-success:
|
||||
- <green>成功优化了 %amount% 名村民,半径为 %radius% 方块。
|
||||
radius-limit-exceed:
|
||||
- <red>您输入的半径超过了 %distance% 方块的限制。
|
||||
optimize-fail:
|
||||
- <gray>%amount% 名村民因为最近已经被优化过而无法再次优化。
|
||||
unoptimize-success:
|
||||
- <green>成功取消了 %amount% 名村民的优化状态,半径为 %radius% 方块。
|
||||
specify-radius:
|
||||
- <red>请指定一个半径。
|
||||
radius-invalid:
|
||||
- <red>您输入的半径不是有效的数字。请重试。
|
||||
no-villagers-nearby:
|
||||
- <gray>在 %radius% 方块范围内找不到任何就业的村民。
|
@ -5,6 +5,8 @@ authors: [ xGinko ]
|
||||
description: ${project.description}
|
||||
website: ${project.url}
|
||||
api-version: '1.16'
|
||||
softdepend:
|
||||
- AntiVillagerLag
|
||||
folia-supported: true
|
||||
|
||||
commands:
|
||||
|
Loading…
x
Reference in New Issue
Block a user