Compare commits

...

105 Commits

Author SHA1 Message Date
xGinko
15fcd14e16 add experimental regional optimization 2024-09-06 01:46:18 +02:00
xGinko
4019afe89b dont break loop 2024-09-02 02:11:07 +02:00
xGinko
6f1077ad2d only add to inv if survival 2024-09-02 02:09:14 +02:00
xGinko
72e2673248 try recreate vanilla restocking mechanic 2024-09-02 01:49:20 +02:00
xGinko
04d6cf604c try recreate vanilla restocking mechanic 2024-09-02 01:49:20 +02:00
Ginko
d4ba859176
Merge pull request #9 from OdgeBodge/master
Prevent nametag from being consumed when unoptimising
2024-08-29 23:34:30 +02:00
odgebodge
56d6baf9de don't consume on unoptimise 2024-08-29 22:12:09 +01:00
xGinko
ca3c486d8b cleanup main class 2024-08-09 14:55:53 +02:00
xGinko
9dd32b1207 add another missing return 2024-08-09 14:46:27 +02:00
xGinko
da300b2958 dont continue after disable call 2024-08-09 14:43:37 +02:00
xGinko
f90d3ac3f2 cleanup wrappers 2024-08-09 14:24:45 +02:00
xGinko
1360be9917 use cross compatible scheduler 2024-08-06 02:31:49 +02:00
xGinko
42a1bdec57 do more scheduling for folia 2024-08-06 02:30:31 +02:00
xGinko
6fe1330038 simplify command reload 2024-07-30 20:47:30 +02:00
xGinko
0a3cd80f9c make whitelist work like a whitelist 2024-07-30 02:33:34 +02:00
xGinko
0ff37cbd7d compile in lower version for backwards compatibility 2024-07-30 02:26:50 +02:00
xGinko
07d105e30e avoid auto-creation of alias references by configmaster 2024-07-30 02:20:32 +02:00
xGinko
aecd669638 remove usage of OldEnum for backwards compatibility
make reloadModules method simple again
2024-07-30 02:17:00 +02:00
xGinko
7969f75a3a close #7 2024-07-30 01:25:57 +02:00
xGinko
849d6fbaf5 update XSeries 2024-07-24 16:56:45 +02:00
xGinko
b0d1c42955 remove from cache on death 2024-07-24 16:51:36 +02:00
Ginko
159c03f3cd
Merge pull request #6 from bonn2/feat-profession-culling-whitelist 2024-07-19 22:36:48 +02:00
Brett Bonifas
106e1625cc Add villager profession culling whitelist 2024-07-19 01:06:59 -04:00
xGinko
c5e274daf9 remove unnecessary cast 2024-07-16 11:50:52 +02:00
xGinko
d0af45fc21 check if AVL is installed and disable plugin if yes 2024-07-16 11:41:05 +02:00
xGinko
4a775d5e7e up version for modrinth 2024-07-11 21:30:58 +02:00
xGinko
98b392d528 use invalidate instead of Map#remove 2024-07-09 14:13:17 +02:00
xGinko
f652cb1d1c update onDisable method 2024-07-09 04:39:28 +02:00
xGinko
b94e158465 refactor wrappercache 2024-07-09 04:24:23 +02:00
xGinko
c05ec30330 clean cache disable 2024-07-09 04:09:24 +02:00
xGinko
5b3687a062 only enable what should enable 2024-07-09 03:28:33 +02:00
xGinko
665e23ec58 more clear enable/disable§ 2024-07-09 03:25:44 +02:00
xGinko
d5de576591 improved reload 2024-07-09 03:24:17 +02:00
xGinko
ca563700b3 more command management 2024-07-09 03:21:03 +02:00
xGinko
20d426e315 use PluginCommand after all to stay reloadable 2024-07-09 02:50:18 +02:00
xGinko
7c56dfdb17 work out new command concept 2024-07-09 02:13:49 +02:00
xGinko
2cd6d0576a ratelimit chunk checks and fix scheduling 2024-07-08 19:42:31 +02:00
xGinko
65322c6caa schedule check for folia 1.20.4+ 2024-07-08 15:21:10 +02:00
xGinko
2fec1bcbd4 finish 1.21 compatibility 2024-07-08 15:09:48 +02:00
xGinko
d547628a55 replace folialib with morepaperlib 2024-07-08 15:05:38 +02:00
xGinko
77ff0a8921 dont import paper events 2024-07-08 14:36:09 +02:00
xGinko
9cc91619dc exclude some unused features from xseries 2024-07-04 15:06:51 +02:00
xGinko
3b4c6dc32e disable reflection logging 2024-07-04 15:04:28 +02:00
xGinko
0d24169283 downgrade folialib to avoid stackoverflow 2024-07-04 14:59:27 +02:00
xGinko
5d4e9e4021 inline 2024-07-04 13:10:29 +02:00
xGinko
7cf9e7d2de Invalid key. Must be [a-z0-9/._-]: levelCooldown 2024-07-04 13:09:01 +02:00
xGinko
ef8b6c884a improve 1.21 compatibility 2024-07-02 22:41:35 +02:00
xGinko
8f6fe7fa07 improve village chunklimit 2024-06-24 23:40:24 +02:00
xGinko
493a3d7fe7 update folialib 2024-06-24 22:25:24 +02:00
xGinko
28f9f13a5b update adventure 2024-06-24 22:25:07 +02:00
xGinko
620a0b5d48 fix disable subcmd 2024-06-24 22:24:21 +02:00
xGinko
bc7cffd77e semantics 2024-06-24 22:09:54 +02:00
xGinko
37636e5332 improve util 2024-06-24 22:06:20 +02:00
xGinko
e01b8b0462 use abstract class instead of interface 2024-06-12 15:21:43 +02:00
xGinko
5fecedf658 better logging 2024-05-28 03:01:08 +02:00
xGinko
b29f4afa65 fix broken getOptimizationType function 2024-05-14 01:42:09 +02:00
xGinko
11ef3eedac fix nametag optimization still consuming item when consumption is disabled
fix datahandlers returning wrong values when checking for cooldown millis
remove unused dependency
2024-05-11 12:14:43 +02:00
xGinko
9525a2b3ba fix nametag consumption disable 2024-05-10 21:29:19 +02:00
xGinko
aeb7367b0e close #5 2024-05-02 10:41:31 +02:00
xGinko
00daca70a7 optimize languagecachemap size 2024-04-28 21:04:54 +02:00
xGinko
034a270cfc last round of optimizations before release 2024-04-28 19:39:43 +02:00
xGinko
2ab9dbeaf1 compare unique identifier instead of entire worlds 2024-04-28 19:10:26 +02:00
xGinko
c7ee42f150 rename methods 2024-04-28 19:08:47 +02:00
xGinko
02b4675fa0 make sure we are in the same world 2024-04-28 19:07:39 +02:00
xGinko
9421da491b we can just use getters here 2024-04-28 19:05:58 +02:00
xGinko
f43f9898f1 fix a bug that would kick players with an IllegalArgumentException when using workstation optimization
improve logging for some modules
improve formatting of enums
2024-04-28 16:42:45 +02:00
Ginko
4582cabc31
Merge pull request #4 from xiaoyueyoqwq/master
Create zh_cn.yml
2024-04-23 13:21:13 +02:00
xiaoyueyoqwq
c7a0513589
Create zh_cn.yml
add Chinese Translate
为插件添加中文翻译
2024-04-22 19:47:52 +08:00
Ginko
7f002a5c97
Merge pull request #3 from jd07159/master
Add Korean language
2024-04-03 15:42:57 +02:00
jd07159
59bb34685f
Add Korean language 2024-04-03 16:41:21 +09:00
xGinko
67d3fcf373 more improvements and fixes 2024-03-31 18:47:10 +02:00
xGinko
a569505f1b performance improvements 2024-03-31 18:36:09 +02:00
xGinko
41d2bc4935 wrapper data accuracy fixes and improvements 2024-03-31 18:34:08 +02:00
xGinko
2d1a9a4fec new system for handling different plugin data 2024-03-31 17:16:15 +02:00
xGinko
ee866c70f2 remove cloud effect on no 2024-03-31 15:08:25 +02:00
xGinko
b1994806c2 fallback to deprecated method for now to fix NoSuchMethodError 2024-03-31 15:01:35 +02:00
xGinko
2f95176cf0 replace with clean stream calls 2024-03-28 13:38:48 +01:00
xGinko
09ab1e6477 further optimize lang reloading 2024-03-28 13:30:59 +01:00
xGinko
b5073d0361 temporarily store in sorted set for clean logging 2024-03-27 13:15:13 +01:00
xGinko
f4d6fac8c9 optimize reloadlang method 2024-03-27 12:40:58 +01:00
xGinko
0afb51b54d autoservice is not needed if we only register one service 2024-03-26 18:25:35 +01:00
xGinko
6d2307ae8d up version and fix groupId 2024-03-25 00:26:49 +01:00
xGinko
5068e59175 improve module logging 2024-03-23 00:40:29 +01:00
xGinko
005f26b2d3 minor change 2024-03-20 22:19:59 +01:00
xGinko
123c0d0787 reduce operation cost 2024-03-20 22:14:29 +01:00
xGinko
533031863f fix messages 2024-03-20 22:10:23 +01:00
xGinko
6272383dd4 change workstation optimization 2024-03-20 21:57:21 +01:00
xGinko
69881d7128 annotate parameter 2024-03-20 10:17:25 +01:00
xGinko
3d86639d4d annotate utils 2024-03-20 10:16:33 +01:00
xGinko
cb9e5e9553 cleanup some classes 2024-03-20 10:07:05 +01:00
Ginko
42a4ac9455
Update FUNDING.yml 2024-03-05 00:34:51 +01:00
xGinko
1ef3a5e43c improve dirty reloadability 2024-03-02 18:48:16 +01:00
xGinko
9448161aec relocate configmaster 2024-03-01 13:27:20 +01:00
xGinko
93c7cb4a31 upload progress 2024-02-28 14:50:37 +01:00
xGinko
c80f2c2eff fix comment 2024-02-22 12:47:26 +01:00
xGinko
28fb8b86ba now it works 2024-02-21 21:38:57 +01:00
xGinko
aadb1a00ba still no idea 2024-02-21 21:29:15 +01:00
xGinko
4024614f85 no idea 2024-02-21 21:20:31 +01:00
xGinko
b30f0856c3 improve color management 2024-02-21 15:00:38 +01:00
xGinko
39c71155f8 properly handle metrics 2024-02-21 14:53:50 +01:00
xGinko
acdb97685d remove unused method 2024-02-21 14:35:56 +01:00
xGinko
6a099ddfe3 fix backwards compatibility issues with MiniMessage and Components
downgrade to java 8 so 1.16 modded servers arent left out
2024-02-21 14:30:54 +01:00
xGinko
f4a5d2b9fd fix 1.16 compatibility 2024-02-19 01:16:42 +01:00
xGinko
0ff288ce84 bump version 2024-02-13 19:25:18 +01:00
xGinko
74bb0b0e9e switch restock timestamp and always format positive time 2024-02-13 19:14:38 +01:00
59 changed files with 3165 additions and 1734 deletions

2
.github/FUNDING.yml vendored
View File

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

100
pom.xml
View File

@ -4,9 +4,9 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>me.xginko.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>

View File

@ -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());
}
}

View File

@ -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();
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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());
}
}

View File

@ -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;

View File

@ -1,12 +1,12 @@
package me.xginko.villageroptimizer.commands.unoptimizevillagers;
import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.WrappedVillager;
import me.xginko.villageroptimizer.commands.VillagerOptimizerCommand;
import me.xginko.villageroptimizer.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;

View File

@ -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));
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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());
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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)) {

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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;
}
}
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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.");
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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)));
}
}
}

View File

@ -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();
}
}

View File

@ -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);
}

View File

@ -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);
}
}
}

View File

@ -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));
}
}
}

View File

@ -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);
}
}

View File

@ -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()));
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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);
});
}
}

View File

@ -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()));
}
}
}

View File

@ -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()));
}
}
}

View File

@ -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()));
}
}
}

View File

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

View File

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

View File

@ -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

View File

@ -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,

View File

@ -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) {}
}
}
}

View File

@ -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 +
"}";
}
}

View File

@ -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();
}
}

View File

@ -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;
};
}
}

View File

@ -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;
}
}

View File

@ -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));
}
}

View 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);
}
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1 @@
me.xginko.villageroptimizer.logging.ComponentLoggerProviderImpl

View 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% 블록 반경 내에서 사용 중인 주민을 찾을 수 없습니다.

View 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% 方块范围内找不到任何就业的村民。

View File

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