Compare commits

...

146 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
xGinko
afc1814e40 log removals by default 2024-02-09 01:16:19 +01:00
xGinko
3f8e651346 further improvement of logging messages 2024-02-09 01:12:43 +01:00
xGinko
01fed3258d improve location logging 2024-02-09 00:49:25 +01:00
xGinko
c53d38c6d7 fix config order 2024-02-09 00:44:33 +01:00
xGinko
21b7e998fb this is even better 2024-02-09 00:36:08 +01:00
xGinko
003240df00 make leashing logic more natural 2024-02-09 00:33:41 +01:00
xGinko
aa0eedab87 correct log message 2024-02-09 00:17:45 +01:00
xGinko
73eecd3f7f improve logging 2024-02-09 00:13:12 +01:00
xGinko
ed285be593 fix logger prefix 2024-02-08 23:35:40 +01:00
xGinko
c72ebcf628 1.16 compatibility fixes 2024-02-08 23:32:28 +01:00
xGinko
105c4591a8 make scheduler impl always available 2024-02-08 23:11:59 +01:00
xGinko
2a05cc4bc8 improve leashing logic 2024-02-08 23:07:38 +01:00
xGinko
b07490f9b4 improve readability 2024-02-08 22:24:59 +01:00
xGinko
5c6bba5c2d dont delegate to method 2024-02-08 22:16:30 +01:00
xGinko
e14c212dff finish up improvement to workstation optimization 2024-02-08 22:09:03 +01:00
xGinko
ed79060e7f add functionality and begin work on workstation optimization 2024-02-07 23:23:44 +01:00
xGinko
e2176fa1e3 improve keyring 2024-01-31 16:16:26 +01:00
xGinko
b9acbf9751 rename class 2024-01-31 15:42:32 +01:00
xGinko
ae55df88fb improve comments 2024-01-31 15:40:06 +01:00
xGinko
e3d33c6e79 suppress where fallback methods are implemented 2024-01-31 15:17:22 +01:00
xGinko
ace4030de7 catch throwable instead of exception to ensure no scary error 2024-01-31 15:00:28 +01:00
xGinko
3a1a5072c9 improve leashing logic 2024-01-31 14:55:49 +01:00
xGinko
a718e9daeb add ability to leash villagers 2024-01-31 14:47:38 +01:00
xGinko
63744345e2 only change optimization state once no longer trading
optimize plugin logging and scheduling
2024-01-30 14:13:25 +01:00
Ginko
73eef0b479
Merge pull request #1 from F3F5/patch-1
fix lang mistake
2024-01-25 16:52:05 +01:00
xGinko
f94b6743b1 clarify what this object is 2024-01-25 16:06:31 +01:00
xGinko
6c4b30916d even cleaner approach 2024-01-25 16:04:47 +01:00
xGinko
604f16b5df cleanup 2024-01-25 15:46:59 +01:00
MISHA
8906ddb64a
fix lang mistake 2024-01-25 18:18:04 +04:00
xGinko
305ccb0f0d initial support for parsing data from AVL 2024-01-25 15:12:19 +01:00
xGinko
3f27448d4a up version 2024-01-23 09:34:14 +01:00
xGinko
0069b09131 improve command input safety 2024-01-23 09:33:52 +01:00
xGinko
e9e2bfb48b minor optimizations 2024-01-23 09:21:10 +01:00
xGinko
325b55d874 important base files 2024-01-21 03:27:23 +01:00
xGinko
251b8896d4 Update README 2024-01-09 12:49:50 +01:00
xGinko
1524178788 rename variable 2024-01-09 12:43:49 +01:00
xGinko
b281734822 Dont try catch more than needed 2024-01-09 12:39:59 +01:00
xGinko
f8179ec975 make project use a single jar 2024-01-09 12:25:23 +01:00
xGinko
013b09bf1e make console sender reusable 2024-01-01 12:49:08 +01:00
xGinko
f68d745c40 improve language cache 2024-01-01 11:43:52 +01:00
xGinko
f45ac252c6 overall improvements 2023-12-31 23:06:05 +01:00
123 changed files with 4503 additions and 5569 deletions

1
.github/FUNDING.yml vendored Normal file
View File

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

26
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

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

View File

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

22
.github/workflows/maven-publish.yml vendored Normal file
View File

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

View File

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

View File

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

View File

@ -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;
VillagerCache(long expireAfterWriteSeconds) {
this.villagerCache = Caffeine.newBuilder().expireAfterWrite(Duration.ofSeconds(expireAfterWriteSeconds)).build();
}
public @NotNull ConcurrentMap<UUID, WrappedVillager> cacheMap() {
return this.villagerCache.asMap();
}
public @Nullable WrappedVillager get(@NotNull UUID uuid) {
WrappedVillager wrappedVillager = this.villagerCache.getIfPresent(uuid);
return wrappedVillager == null && Bukkit.getEntity(uuid) instanceof Villager villager ? this.add(villager) : wrappedVillager;
}
public @NotNull WrappedVillager getOrAdd(@NotNull Villager villager) {
WrappedVillager wrappedVillager = this.villagerCache.getIfPresent(villager.getUniqueId());
return wrappedVillager == null ? this.add(new WrappedVillager(villager)) : this.add(wrappedVillager);
}
public @NotNull WrappedVillager add(@NotNull WrappedVillager villager) {
this.villagerCache.put(villager.villager().getUniqueId(), villager);
return villager;
}
public @NotNull WrappedVillager add(@NotNull Villager villager) {
return add(new WrappedVillager(villager));
}
public boolean contains(@NotNull UUID uuid) {
return this.villagerCache.getIfPresent(uuid) != null;
}
public boolean contains(@NotNull WrappedVillager villager) {
return this.contains(villager.villager().getUniqueId());
}
public boolean contains(@NotNull Villager villager) {
return this.contains(villager.getUniqueId());
}
}

View File

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

View File

@ -1,188 +0,0 @@
package me.xginko.villageroptimizer;
import me.xginko.villageroptimizer.enums.Keys;
import me.xginko.villageroptimizer.enums.OptimizationType;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.entity.Villager;
import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.persistence.PersistentDataType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public final class WrappedVillager {
private final @NotNull Villager villager;
private final @NotNull PersistentDataContainer dataContainer;
WrappedVillager(@NotNull Villager villager) {
this.villager = villager;
this.dataContainer = villager.getPersistentDataContainer();
}
/**
* @return The villager inside the wrapper.
*/
public @NotNull Villager villager() {
return villager;
}
/**
* @return The data container inside the wrapper.
*/
public @NotNull PersistentDataContainer dataContainer() {
return dataContainer;
}
/**
* @return True if the villager is optimized by this plugin, otherwise false.
*/
public boolean isOptimized() {
return dataContainer.has(Keys.OPTIMIZATION_TYPE.key(), PersistentDataType.STRING);
}
/**
* @param cooldown_millis The configured cooldown in millis until the next optimization is allowed to occur.
* @return True if villager can be optimized again, otherwise false.
*/
public boolean canOptimize(final long cooldown_millis) {
return getLastOptimize() + cooldown_millis <= System.currentTimeMillis();
}
/**
* @param type OptimizationType the villager should be set to.
*/
public void setOptimization(OptimizationType type) {
if (type.equals(OptimizationType.NONE) && isOptimized()) {
dataContainer.remove(Keys.OPTIMIZATION_TYPE.key());
villager.setAware(true);
villager.setAI(true);
} else {
dataContainer.set(Keys.OPTIMIZATION_TYPE.key(), PersistentDataType.STRING, type.name());
villager.setAware(false);
}
}
/**
* @return The current OptimizationType of the villager.
*/
public @NotNull OptimizationType getOptimizationType() {
return isOptimized() ? OptimizationType.valueOf(dataContainer.get(Keys.OPTIMIZATION_TYPE.key(), PersistentDataType.STRING)) : OptimizationType.NONE;
}
/**
* Saves the system time in millis when the villager was last optimized.
*/
public void saveOptimizeTime() {
dataContainer.set(Keys.LAST_OPTIMIZE.key(), PersistentDataType.LONG, System.currentTimeMillis());
}
/**
* @return The system time in millis when the villager was last optimized, 0L if the villager was never optimized.
*/
public long getLastOptimize() {
return dataContainer.has(Keys.LAST_OPTIMIZE.key(), PersistentDataType.LONG) ? dataContainer.get(Keys.LAST_OPTIMIZE.key(), PersistentDataType.LONG) : 0L;
}
/**
* Here for convenience so the remaining millis since the last stored optimize time
* can be easily calculated.
* This enables new configured cooldowns to instantly apply instead of them being persistent.
*
* @param cooldown_millis The configured cooldown in milliseconds you want to check against.
* @return The time left in millis until the villager can be optimized again.
*/
public long getOptimizeCooldownMillis(final long cooldown_millis) {
return dataContainer.has(Keys.LAST_OPTIMIZE.key(), PersistentDataType.LONG) ? (System.currentTimeMillis() - (dataContainer.get(Keys.LAST_OPTIMIZE.key(), PersistentDataType.LONG) + cooldown_millis)) : cooldown_millis;
}
/**
* Here for convenience so the remaining millis since the last stored restock time
* can be easily calculated.
*
* @param cooldown_millis The configured cooldown in milliseconds you want to check against.
* @return True if the villager has been loaded long enough.
*/
public boolean canRestock(final long cooldown_millis) {
return getLastRestock() + cooldown_millis <= villager.getWorld().getFullTime();
}
/**
* Restock all trading recipes.
*/
public void restock() {
villager.getRecipes().forEach(recipe -> recipe.setUses(0));
}
/**
* Saves the time of the in-game world when the entity was last restocked.
*/
public void saveRestockTime() {
dataContainer.set(Keys.LAST_RESTOCK.key(), PersistentDataType.LONG, villager.getWorld().getFullTime());
}
/**
* @return The time of the in-game world when the entity was last restocked.
*/
public long getLastRestock() {
return dataContainer.has(Keys.LAST_RESTOCK.key(), PersistentDataType.LONG) ? dataContainer.get(Keys.LAST_RESTOCK.key(), PersistentDataType.LONG) : 0L;
}
public long getRestockCooldownMillis(final long cooldown_millis) {
return dataContainer.has(Keys.LAST_RESTOCK.key(), PersistentDataType.LONG) ? (villager.getWorld().getFullTime() - (dataContainer.get(Keys.LAST_RESTOCK.key(), PersistentDataType.LONG) + cooldown_millis)) : cooldown_millis;
}
/**
* @return The level between 1-5 calculated from the villagers experience.
*/
public int calculateLevel() {
// https://minecraft.fandom.com/wiki/Trading#Mechanics
int vilEXP = villager.getVillagerExperience();
if (vilEXP >= 250) return 5;
if (vilEXP >= 150) return 4;
if (vilEXP >= 70) return 3;
if (vilEXP >= 10) return 2;
return 1;
}
/**
* @param cooldown_millis The configured cooldown in milliseconds you want to check against.
* @return Whether the villager can be leveled up or not with the checked milliseconds
*/
public boolean canLevelUp(final long cooldown_millis) {
return getLastLevelUpTime() + cooldown_millis <= villager.getWorld().getFullTime();
}
/**
* Saves the time of the in-game world when the entity was last leveled up.
*/
public void saveLastLevelUp() {
dataContainer.set(Keys.LAST_LEVELUP.key(), PersistentDataType.LONG, villager.getWorld().getFullTime());
}
/**
* Here for convenience so the remaining millis since the last stored level-up time
* can be easily calculated.
*
* @return The time of the in-game world when the entity was last leveled up.
*/
public long getLastLevelUpTime() {
return dataContainer.has(Keys.LAST_LEVELUP.key(), PersistentDataType.LONG) ? dataContainer.get(Keys.LAST_LEVELUP.key(), PersistentDataType.LONG) : 0L;
}
public long getLevelCooldownMillis(final long cooldown_millis) {
return dataContainer.has(Keys.LAST_LEVELUP.key(), PersistentDataType.LONG) ? (villager.getWorld().getFullTime() - (dataContainer.get(Keys.LAST_LEVELUP.key(), PersistentDataType.LONG) + cooldown_millis)) : cooldown_millis;
}
public void memorizeName(final Component customName) {
dataContainer.set(Keys.LAST_OPTIMIZE_NAME.key(), PersistentDataType.STRING, MiniMessage.miniMessage().serialize(customName));
}
public @Nullable Component getMemorizedName() {
return dataContainer.has(Keys.LAST_OPTIMIZE_NAME.key(), PersistentDataType.STRING) ? MiniMessage.miniMessage().deserialize(dataContainer.get(Keys.LAST_OPTIMIZE_NAME.key(), PersistentDataType.STRING)) : null;
}
public void forgetName() {
dataContainer.remove(Keys.LAST_OPTIMIZE_NAME.key());
}
}

View File

@ -1,11 +0,0 @@
package me.xginko.villageroptimizer.commands;
import net.kyori.adventure.text.TextComponent;
import org.bukkit.command.CommandSender;
public abstract class SubCommand {
public abstract String getLabel();
public abstract TextComponent getDescription();
public abstract TextComponent getSyntax();
public abstract void perform(CommandSender sender, String[] args);
}

View File

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

View File

@ -1,79 +0,0 @@
package me.xginko.villageroptimizer.commands.villageroptimizer;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.commands.SubCommand;
import me.xginko.villageroptimizer.commands.VillagerOptimizerCommand;
import me.xginko.villageroptimizer.commands.villageroptimizer.subcommands.DisableSubCmd;
import me.xginko.villageroptimizer.commands.villageroptimizer.subcommands.ReloadSubCmd;
import me.xginko.villageroptimizer.commands.villageroptimizer.subcommands.VersionSubCmd;
import me.xginko.villageroptimizer.enums.Permissions;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
public class VillagerOptimizerCmd implements TabCompleter, VillagerOptimizerCommand {
private final List<SubCommand> subCommands = new ArrayList<>(3);
private final List<String> tabCompleter = new ArrayList<>(3);
public VillagerOptimizerCmd() {
subCommands.add(new ReloadSubCmd());
subCommands.add(new VersionSubCmd());
subCommands.add(new DisableSubCmd());
subCommands.forEach(subCommand -> tabCompleter.add(subCommand.getLabel()));
}
@Override
public String label() {
return "villageroptimizer";
}
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, String[] args) {
return args.length == 1 ? tabCompleter : null;
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
if (args.length > 0) {
boolean cmdExists = false;
for (SubCommand subCommand : subCommands) {
if (args[0].equalsIgnoreCase(subCommand.getLabel())) {
subCommand.perform(sender, args);
cmdExists = true;
break;
}
}
if (!cmdExists) sendCommandOverview(sender);
} else {
sendCommandOverview(sender);
}
return true;
}
private void sendCommandOverview(CommandSender sender) {
if (!sender.hasPermission(Permissions.Commands.RELOAD.get()) && !sender.hasPermission(Permissions.Commands.VERSION.get())) return;
sender.sendMessage(Component.text("-----------------------------------------------------").color(NamedTextColor.GRAY));
sender.sendMessage(Component.text("VillagerOptimizer Commands").color(VillagerOptimizer.plugin_style.color()));
sender.sendMessage(Component.text("-----------------------------------------------------").color(NamedTextColor.GRAY));
subCommands.forEach(subCommand -> sender.sendMessage(
subCommand.getSyntax().append(Component.text(" - ").color(NamedTextColor.DARK_GRAY)).append(subCommand.getDescription())));
sender.sendMessage(
Component.text("/optimizevillagers <blockradius>").color(VillagerOptimizer.plugin_style.color())
.append(Component.text(" - ").color(NamedTextColor.DARK_GRAY))
.append(Component.text("Optimize villagers in a radius").color(NamedTextColor.GRAY))
);
sender.sendMessage(
Component.text("/unoptmizevillagers <blockradius>").color(VillagerOptimizer.plugin_style.color())
.append(Component.text(" - ").color(NamedTextColor.DARK_GRAY))
.append(Component.text("Unoptimize villagers in a radius").color(NamedTextColor.GRAY))
);
sender.sendMessage(Component.text("-----------------------------------------------------").color(NamedTextColor.GRAY));
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,171 +0,0 @@
package me.xginko.villageroptimizer.modules;
import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.utils.LogUtil;
import org.bukkit.Chunk;
import org.bukkit.Server;
import org.bukkit.World;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Villager;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.CreatureSpawnEvent;
import org.bukkit.event.player.PlayerInteractEntityEvent;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.logging.Level;
public class VillagerChunkLimit implements VillagerOptimizerModule, Listener, Runnable {
private final Server server;
private final VillagerCache villagerCache;
private final List<Villager.Profession> non_optimized_removal_priority = new ArrayList<>(16);
private final List<Villager.Profession> optimized_removal_priority = new ArrayList<>(16);
private final long check_period;
private final int non_optimized_max_per_chunk, optimized_max_per_chunk;
private final boolean log_enabled;
protected VillagerChunkLimit() {
shouldEnable();
this.server = VillagerOptimizer.getInstance().getServer();
this.villagerCache = VillagerOptimizer.getCache();
Config config = VillagerOptimizer.getConfiguration();
config.addComment("villager-chunk-limit.enable", """
Checks chunks for too many villagers and removes excess villagers based on priority.""");
this.check_period = config.getInt("villager-chunk-limit.check-period-in-ticks", 600, """
Check all loaded chunks every X ticks. 1 second = 20 ticks\s
A shorter delay in between checks is more efficient but is also more resource intense.\s
A larger delay is less resource intense but could become inefficient.""");
this.log_enabled = config.getBoolean("villager-chunk-limit.log-removals", false);
this.non_optimized_max_per_chunk = config.getInt("villager-chunk-limit.unoptimized.max-per-chunk", 20,
"The maximum amount of unoptimized villagers per chunk.");
config.getList("villager-chunk-limit.unoptimized.removal-priority", List.of(
"NONE", "NITWIT", "SHEPHERD", "FISHERMAN", "BUTCHER", "CARTOGRAPHER", "LEATHERWORKER",
"FLETCHER", "MASON", "FARMER", "ARMORER", "TOOLSMITH", "WEAPONSMITH", "CLERIC", "LIBRARIAN"
), """
Professions that are in the top of the list are going to be scheduled for removal first.\s
Use enums from https://jd.papermc.io/paper/1.20/org/bukkit/entity/Villager.Profession.html"""
).forEach(configuredProfession -> {
try {
Villager.Profession profession = Villager.Profession.valueOf(configuredProfession);
this.non_optimized_removal_priority.add(profession);
} catch (IllegalArgumentException e) {
LogUtil.moduleLog(Level.WARNING, "villager-chunk-limit.unoptimized",
"Villager profession '"+configuredProfession+"' not recognized. " +
"Make sure you're using the correct profession enums from https://jd.papermc.io/paper/1.20/org/bukkit/entity/Villager.Profession.html.");
}
});
this.optimized_max_per_chunk = config.getInt("villager-chunk-limit.optimized.max-per-chunk", 60,
"The maximum amount of optimized villagers per chunk.");
config.getList("villager-chunk-limit.optimized.removal-priority", List.of(
"NONE", "NITWIT", "SHEPHERD", "FISHERMAN", "BUTCHER", "CARTOGRAPHER", "LEATHERWORKER",
"FLETCHER", "MASON", "FARMER", "ARMORER", "TOOLSMITH", "WEAPONSMITH", "CLERIC", "LIBRARIAN"
)).forEach(configuredProfession -> {
try {
Villager.Profession profession = Villager.Profession.valueOf(configuredProfession);
this.optimized_removal_priority.add(profession);
} catch (IllegalArgumentException e) {
LogUtil.moduleLog(Level.WARNING, "villager-chunk-limit.optimized",
"Villager profession '"+configuredProfession+"' not recognized. " +
"Make sure you're using the correct profession enums from https://jd.papermc.io/paper/1.20/org/bukkit/entity/Villager.Profession.html.");
}
});
}
@Override
public void enable() {
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
server.getPluginManager().registerEvents(this, plugin);
server.getScheduler().scheduleSyncRepeatingTask(plugin, this, check_period, check_period);
}
@Override
public boolean shouldEnable() {
return VillagerOptimizer.getConfiguration().getBoolean("villager-chunk-limit.enable", false);
}
@Override
public void run() {
for (World world : server.getWorlds()) {
for (Chunk chunk : world.getLoadedChunks()) {
this.manageVillagerCount(chunk);
}
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onCreatureSpawn(CreatureSpawnEvent event) {
Entity spawned = event.getEntity();
if (spawned.getType().equals(EntityType.VILLAGER)) {
this.manageVillagerCount(spawned.getChunk());
}
}
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
private void onInteract(PlayerInteractEntityEvent event) {
Entity clicked = event.getRightClicked();
if (clicked.getType().equals(EntityType.VILLAGER)) {
this.manageVillagerCount(clicked.getChunk());
}
}
private void manageVillagerCount(@NotNull Chunk chunk) {
// Collect all optimized and unoptimized villagers in that chunk
List<Villager> optimized_villagers = new ArrayList<>();
List<Villager> not_optimized_villagers = new ArrayList<>();
for (Entity entity : chunk.getEntities()) {
if (entity.getType().equals(EntityType.VILLAGER)) {
Villager villager = (Villager) entity;
if (villagerCache.getOrAdd(villager).isOptimized()) {
optimized_villagers.add(villager);
} else {
not_optimized_villagers.add(villager);
}
}
}
// Check if there are more unoptimized villagers in that chunk than allowed
final int not_optimized_villagers_too_many = not_optimized_villagers.size() - non_optimized_max_per_chunk;
if (not_optimized_villagers_too_many > 0) {
// Sort villagers by profession priority
not_optimized_villagers.sort(Comparator.comparingInt(villager -> {
final Villager.Profession profession = villager.getProfession();
return non_optimized_removal_priority.contains(profession) ? non_optimized_removal_priority.indexOf(profession) : Integer.MAX_VALUE;
}));
// Remove prioritized villagers that are too many
for (int i = 0; i < not_optimized_villagers_too_many; i++) {
Villager villager = not_optimized_villagers.get(i);
villager.remove();
if (log_enabled) LogUtil.moduleLog(Level.INFO, "villager-chunk-limit",
"Removed unoptimized villager of profession type '"+villager.getProfession().name()+"' at "+villager.getLocation()
);
}
}
// Check if there are more optimized villagers in that chunk than allowed
final int optimized_villagers_too_many = optimized_villagers.size() - optimized_max_per_chunk;
if (optimized_villagers_too_many > 0) {
// Sort villagers by profession priority
optimized_villagers.sort(Comparator.comparingInt(villager -> {
final Villager.Profession profession = villager.getProfession();
return optimized_removal_priority.contains(profession) ? optimized_removal_priority.indexOf(profession) : Integer.MAX_VALUE;
}));
// Remove prioritized villagers that are too many
for (int i = 0; i < optimized_villagers_too_many; i++) {
Villager villager = optimized_villagers.get(i);
villager.remove();
if (log_enabled) LogUtil.moduleLog(Level.INFO, "villager-chunk-limit",
"Removed optimized villager of profession type '"+villager.getProfession().name()+"' at "+villager.getLocation()
);
}
}
}
}

View File

@ -1,43 +0,0 @@
package me.xginko.villageroptimizer.modules;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.modules.gameplay.*;
import me.xginko.villageroptimizer.modules.optimization.OptimizeByBlock;
import me.xginko.villageroptimizer.modules.optimization.OptimizeByNametag;
import me.xginko.villageroptimizer.modules.optimization.OptimizeByWorkstation;
import org.bukkit.event.HandlerList;
import java.util.HashSet;
public interface VillagerOptimizerModule {
void enable();
boolean shouldEnable();
HashSet<VillagerOptimizerModule> modules = new HashSet<>();
static void reloadModules() {
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
HandlerList.unregisterAll(plugin);
plugin.getServer().getScheduler().cancelTasks(plugin);
modules.clear();
modules.add(new OptimizeByNametag());
modules.add(new OptimizeByBlock());
modules.add(new OptimizeByWorkstation());
modules.add(new RestockOptimizedTrades());
modules.add(new LevelOptimizedProfession());
modules.add(new RenameOptimizedVillagers());
modules.add(new MakeVillagersSpawnAdult());
modules.add(new PreventUnoptimizedTrading());
modules.add(new PreventOptimizedTargeting());
modules.add(new PreventOptimizedDamage());
modules.add(new VillagerChunkLimit());
modules.forEach(module -> {
if (module.shouldEnable()) module.enable();
});
}
}

View File

@ -1,84 +0,0 @@
package me.xginko.villageroptimizer.modules.gameplay;
import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.WrappedVillager;
import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import me.xginko.villageroptimizer.utils.CommonUtil;
import net.kyori.adventure.text.TextReplacementConfig;
import org.bukkit.entity.Player;
import org.bukkit.entity.Villager;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
public class LevelOptimizedProfession implements VillagerOptimizerModule, Listener {
private final VillagerOptimizer plugin;
private final VillagerCache villagerCache;
private final boolean notify_player;
private final long cooldown;
public LevelOptimizedProfession() {
shouldEnable();
this.plugin = VillagerOptimizer.getInstance();
this.villagerCache = VillagerOptimizer.getCache();
Config config = VillagerOptimizer.getConfiguration();
config.addComment("gameplay.level-optimized-profession", """
This is needed to allow optimized villagers to level up.\s
Temporarily enables the villagers AI to allow it to level up and then disables it again.""");
this.cooldown = config.getInt("gameplay.level-optimized-profession.level-check-cooldown-seconds", 5, """
Cooldown in seconds until the level of a villager will be checked and updated again.\s
Recommended to leave as is.""") * 1000L;
this.notify_player = config.getBoolean("gameplay.level-optimized-profession.notify-player", true,
"Tell players to wait when a villager is leveling up.");
}
@Override
public void enable() {
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@Override
public boolean shouldEnable() {
return true;
}
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
private void onTradeScreenClose(InventoryCloseEvent event) {
if (
event.getInventory().getType().equals(InventoryType.MERCHANT)
&& event.getInventory().getHolder() instanceof Villager villager
) {
WrappedVillager wVillager = villagerCache.getOrAdd(villager);
if (!wVillager.isOptimized()) return;
if (wVillager.canLevelUp(cooldown)) {
if (wVillager.calculateLevel() > villager.getVillagerLevel()) {
villager.addPotionEffect(new PotionEffect(PotionEffectType.SLOW, 120, 120, false, false));
villager.setAware(true);
plugin.getServer().getScheduler().scheduleSyncDelayedTask(plugin, () -> {
villager.setAware(false);
wVillager.saveLastLevelUp();
}, 100L);
}
} else {
if (notify_player) {
Player player = (Player) event.getPlayer();
final TextReplacementConfig timeLeft = TextReplacementConfig.builder()
.matchLiteral("%time%")
.replacement(CommonUtil.formatTime(wVillager.getLevelCooldownMillis(cooldown)))
.build();
VillagerOptimizer.getLang(player.locale()).villager_leveling_up.forEach(line -> player.sendMessage(line.replaceText(timeLeft)));
}
}
}
}
}

View File

@ -1,38 +0,0 @@
package me.xginko.villageroptimizer.modules.gameplay;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Villager;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.CreatureSpawnEvent;
public class MakeVillagersSpawnAdult implements VillagerOptimizerModule, Listener {
public MakeVillagersSpawnAdult() {}
@Override
public void enable() {
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@Override
public boolean shouldEnable() {
return VillagerOptimizer.getConfiguration().getBoolean("gameplay.villagers-spawn-as-adults.enable", false, """
Spawned villagers will immediately be adults.\s
This is to save some more resources as players don't have to keep unoptimized\s
villagers loaded because they have to wait for them to turn into adults before they can\s
optimize them.""");
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onVillagerSpawn(CreatureSpawnEvent event) {
if (event.getEntityType().equals(EntityType.VILLAGER)) {
Villager villager = (Villager) event.getEntity();
if (!villager.isAdult()) villager.setAdult();
}
}
}

View File

@ -1,65 +0,0 @@
package me.xginko.villageroptimizer.modules.gameplay;
import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import me.xginko.villageroptimizer.utils.LogUtil;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Villager;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageEvent;
import java.util.Arrays;
import java.util.HashSet;
public class PreventOptimizedDamage implements VillagerOptimizerModule, Listener {
private final VillagerCache villagerCache;
private final HashSet<EntityDamageEvent.DamageCause> damage_causes_to_cancel = new HashSet<>();
public PreventOptimizedDamage() {
shouldEnable();
this.villagerCache = VillagerOptimizer.getCache();
Config config = VillagerOptimizer.getConfiguration();
config.addComment("gameplay.prevent-damage-to-optimized.enable",
"Configure what kind of damage you want to cancel for optimized villagers here.");
config.getList("gameplay.prevent-damage-to-optimized.damage-causes-to-cancel",
Arrays.stream(EntityDamageEvent.DamageCause.values()).map(Enum::name).sorted().toList(), """
These are all current entries in the game. Remove what you do not need blocked.\s
If you want a description or need to add a previously removed type, refer to:\s
https://jd.papermc.io/paper/1.20/org/bukkit/event/entity/EntityDamageEvent.DamageCause.html"""
).forEach(configuredDamageCause -> {
try {
EntityDamageEvent.DamageCause damageCause = EntityDamageEvent.DamageCause.valueOf(configuredDamageCause);
this.damage_causes_to_cancel.add(damageCause);
} catch (IllegalArgumentException e) {
LogUtil.damageCauseNotRecognized("prevent-damage-to-optimized", configuredDamageCause);
}
});
}
@Override
public void enable() {
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@Override
public boolean shouldEnable() {
return VillagerOptimizer.getConfiguration().getBoolean("gameplay.prevent-damage-to-optimized.enable", true);
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onDamageByEntity(EntityDamageEvent event) {
if (
event.getEntityType().equals(EntityType.VILLAGER)
&& damage_causes_to_cancel.contains(event.getCause())
&& villagerCache.getOrAdd((Villager) event.getEntity()).isOptimized()
) {
event.setCancelled(true);
}
}
}

View File

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

View File

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

View File

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

View File

@ -1,74 +0,0 @@
package me.xginko.villageroptimizer.modules.gameplay;
import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.WrappedVillager;
import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.enums.Permissions;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import me.xginko.villageroptimizer.utils.CommonUtil;
import net.kyori.adventure.text.TextReplacementConfig;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.entity.Villager;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerInteractEntityEvent;
public class RestockOptimizedTrades implements VillagerOptimizerModule, Listener {
private final VillagerCache villagerCache;
private final long restock_delay_millis;
private final boolean log_enabled, notify_player;
public RestockOptimizedTrades() {
shouldEnable();
this.villagerCache = VillagerOptimizer.getCache();
Config config = VillagerOptimizer.getConfiguration();
config.addComment("gameplay.restock-optimized-trades", """
This is for automatic restocking of trades for optimized villagers. Optimized Villagers\s
don't have enough AI to restock their trades naturally, so this is here as a workaround.""");
this.restock_delay_millis = config.getInt("gameplay.restock-optimized-trades.delay-in-ticks", 1000,
"1 second = 20 ticks. There are 24.000 ticks in a single minecraft day.") * 50L;
this.notify_player = config.getBoolean("gameplay.restock-optimized-trades.notify-player", true,
"Sends the player a message when the trades were restocked on a clicked villager.");
this.log_enabled = config.getBoolean("gameplay.restock-optimized-trades.log", false);
}
@Override
public void enable() {
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@Override
public boolean shouldEnable() {
return true;
}
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
private void onInteract(PlayerInteractEntityEvent event) {
if (!event.getRightClicked().getType().equals(EntityType.VILLAGER)) return;
WrappedVillager wVillager = villagerCache.getOrAdd((Villager) event.getRightClicked());
if (!wVillager.isOptimized()) return;
Player player = event.getPlayer();
final boolean player_bypassing = player.hasPermission(Permissions.Bypass.RESTOCK_COOLDOWN.get());
if (wVillager.canRestock(restock_delay_millis) || player_bypassing) {
wVillager.restock();
wVillager.saveRestockTime();
if (notify_player && !player_bypassing) {
final TextReplacementConfig timeLeft = TextReplacementConfig.builder()
.matchLiteral("%time%")
.replacement(CommonUtil.formatTime(wVillager.getRestockCooldownMillis(restock_delay_millis)))
.build();
VillagerOptimizer.getLang(player.locale()).trades_restocked.forEach(line -> player.sendMessage(line.replaceText(timeLeft)));
}
if (log_enabled)
VillagerOptimizer.getLog().info("Restocked optimized villager at "+ wVillager.villager().getLocation());
}
}
}

View File

@ -1,193 +0,0 @@
package me.xginko.villageroptimizer.modules.optimization;
import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.WrappedVillager;
import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.enums.OptimizationType;
import me.xginko.villageroptimizer.enums.Permissions;
import me.xginko.villageroptimizer.events.VillagerOptimizeEvent;
import me.xginko.villageroptimizer.events.VillagerUnoptimizeEvent;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import me.xginko.villageroptimizer.utils.CommonUtil;
import me.xginko.villageroptimizer.utils.LogUtil;
import net.kyori.adventure.text.TextReplacementConfig;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.entity.Villager;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import java.util.HashSet;
import java.util.List;
public class OptimizeByBlock implements VillagerOptimizerModule, Listener {
private final VillagerCache villagerCache;
private final HashSet<Material> blocks_that_disable = new HashSet<>(4);
private final long cooldown;
private final double search_radius;
private final boolean only_while_sneaking, notify_player, log_enabled;
public OptimizeByBlock() {
shouldEnable();
this.villagerCache = VillagerOptimizer.getCache();
Config config = VillagerOptimizer.getConfiguration();
config.addComment("optimization-methods.block-optimization.enable", """
When enabled, the closest villager standing near a configured block being placed will be optimized.\s
If a configured block is broken nearby, the closest villager will become unoptimized again.""");
config.getList("optimization-methods.block-optimization.materials", List.of(
"LAPIS_BLOCK", "GLOWSTONE", "IRON_BLOCK"
), "Values here need to be valid bukkit Material enums for your server version."
).forEach(configuredMaterial -> {
try {
Material disableBlock = Material.valueOf(configuredMaterial);
this.blocks_that_disable.add(disableBlock);
} catch (IllegalArgumentException e) {
LogUtil.materialNotRecognized("block-optimization", configuredMaterial);
}
});
this.cooldown = config.getInt("optimization-methods.block-optimization.optimize-cooldown-seconds", 600, """
Cooldown in seconds until a villager can be optimized again by using specific blocks. \s
Here for configuration freedom. Recommended to leave as is to not enable any exploitable behavior.""") * 1000L;
this.search_radius = config.getDouble("optimization-methods.block-optimization.search-radius-in-blocks", 2.0, """
The radius in blocks a villager can be away from the player when he places an optimize block.\s
The closest unoptimized villager to the player will be optimized.""") / 2;
this.only_while_sneaking = config.getBoolean("optimization-methods.block-optimization.only-when-sneaking", true,
"Only optimize/unoptimize by workstation when player is sneaking during place or break.");
this.notify_player = config.getBoolean("optimization-methods.block-optimization.notify-player", true,
"Sends players a message when they successfully optimized or unoptimized a villager.");
this.log_enabled = config.getBoolean("optimization-methods.block-optimization.log", false);
}
@Override
public void enable() {
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@Override
public boolean shouldEnable() {
return VillagerOptimizer.getConfiguration().getBoolean("optimization-methods.block-optimization.enable", false);
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onBlockPlace(BlockPlaceEvent event) {
Block placed = event.getBlock();
if (!blocks_that_disable.contains(placed.getType())) return;
Player player = event.getPlayer();
if (!player.hasPermission(Permissions.Optimize.BLOCK.get())) return;
if (only_while_sneaking && !player.isSneaking()) return;
final Location blockLoc = placed.getLocation();
WrappedVillager closestOptimizableVillager = null;
double closestDistance = Double.MAX_VALUE;
for (Entity entity : blockLoc.getNearbyEntities(search_radius, search_radius, search_radius)) {
if (!entity.getType().equals(EntityType.VILLAGER)) continue;
Villager villager = (Villager) entity;
final Villager.Profession profession = villager.getProfession();
if (profession.equals(Villager.Profession.NONE) || profession.equals(Villager.Profession.NITWIT)) continue;
WrappedVillager wVillager = villagerCache.getOrAdd(villager);
final double distance = entity.getLocation().distance(blockLoc);
if (distance < closestDistance && wVillager.canOptimize(cooldown)) {
closestOptimizableVillager = wVillager;
closestDistance = distance;
}
}
if (closestOptimizableVillager == null) return;
if (closestOptimizableVillager.canOptimize(cooldown) || player.hasPermission(Permissions.Bypass.BLOCK_COOLDOWN.get())) {
VillagerOptimizeEvent optimizeEvent = new VillagerOptimizeEvent(closestOptimizableVillager, OptimizationType.BLOCK, player, event.isAsynchronous());
if (!optimizeEvent.callEvent()) return;
closestOptimizableVillager.setOptimization(optimizeEvent.getOptimizationType());
closestOptimizableVillager.saveOptimizeTime();
if (notify_player) {
final TextReplacementConfig vilProfession = TextReplacementConfig.builder()
.matchLiteral("%vil_profession%")
.replacement(closestOptimizableVillager.villager().getProfession().toString().toLowerCase())
.build();
final TextReplacementConfig placedMaterial = TextReplacementConfig.builder()
.matchLiteral("%blocktype%")
.replacement(placed.getType().toString().toLowerCase())
.build();
VillagerOptimizer.getLang(player.locale()).block_optimize_success.forEach(line -> player.sendMessage(line
.replaceText(vilProfession)
.replaceText(placedMaterial)
));
}
if (log_enabled)
VillagerOptimizer.getLog().info("Villager was optimized by block at "+closestOptimizableVillager.villager().getLocation());
} else {
if (notify_player) {
final TextReplacementConfig timeLeft = TextReplacementConfig.builder()
.matchLiteral("%time%")
.replacement(CommonUtil.formatTime(closestOptimizableVillager.getOptimizeCooldownMillis(cooldown)))
.build();
VillagerOptimizer.getLang(player.locale()).block_on_optimize_cooldown.forEach(line -> player.sendMessage(line.replaceText(timeLeft)));
}
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onBlockBreak(BlockBreakEvent event) {
Block broken = event.getBlock();
if (!blocks_that_disable.contains(broken.getType())) return;
Player player = event.getPlayer();
if (!player.hasPermission(Permissions.Optimize.BLOCK.get())) return;
if (only_while_sneaking && !player.isSneaking()) return;
final Location blockLoc = broken.getLocation();
WrappedVillager closestOptimizedVillager = null;
double closestDistance = Double.MAX_VALUE;
for (Entity entity : blockLoc.getNearbyEntities(search_radius, search_radius, search_radius)) {
if (!entity.getType().equals(EntityType.VILLAGER)) continue;
Villager villager = (Villager) entity;
WrappedVillager wVillager = villagerCache.getOrAdd(villager);
final double distance = entity.getLocation().distance(blockLoc);
if (distance < closestDistance && wVillager.isOptimized()) {
closestOptimizedVillager = wVillager;
closestDistance = distance;
}
}
if (closestOptimizedVillager == null) return;
VillagerUnoptimizeEvent unOptimizeEvent = new VillagerUnoptimizeEvent(closestOptimizedVillager, player, OptimizationType.BLOCK, event.isAsynchronous());
if (!unOptimizeEvent.callEvent()) return;
closestOptimizedVillager.setOptimization(OptimizationType.NONE);
if (notify_player) {
final TextReplacementConfig vilProfession = TextReplacementConfig.builder()
.matchLiteral("%vil_profession%")
.replacement(closestOptimizedVillager.villager().getProfession().toString().toLowerCase())
.build();
final TextReplacementConfig brokenMaterial = TextReplacementConfig.builder()
.matchLiteral("%blocktype%")
.replacement(broken.getType().toString().toLowerCase())
.build();
VillagerOptimizer.getLang(player.locale()).block_unoptimize_success.forEach(line -> player.sendMessage(line
.replaceText(vilProfession)
.replaceText(brokenMaterial)
));
}
if (log_enabled)
VillagerOptimizer.getLog().info("Villager unoptimized because nearby optimization block broken at: "+closestOptimizedVillager.villager().getLocation());
}
}

View File

@ -1,126 +0,0 @@
package me.xginko.villageroptimizer.modules.optimization;
import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.WrappedVillager;
import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.enums.OptimizationType;
import me.xginko.villageroptimizer.enums.Permissions;
import me.xginko.villageroptimizer.events.VillagerOptimizeEvent;
import me.xginko.villageroptimizer.events.VillagerUnoptimizeEvent;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import me.xginko.villageroptimizer.utils.CommonUtil;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextReplacementConfig;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.bukkit.Material;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.entity.Villager;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerInteractEntityEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import java.util.HashSet;
import java.util.List;
public class OptimizeByNametag implements VillagerOptimizerModule, Listener {
private final VillagerCache villagerCache;
private final HashSet<String> nametags = new HashSet<>(4);
private final long cooldown;
private final boolean consume_nametag, notify_player, log_enabled;
public OptimizeByNametag() {
shouldEnable();
this.villagerCache = VillagerOptimizer.getCache();
Config config = VillagerOptimizer.getConfiguration();
config.addComment("optimization-methods.nametag-optimization.enable", """
Enable optimization by naming villagers to one of the names configured below.\s
Nametag optimized villagers will be unoptimized again when they are renamed to something else.""");
this.nametags.addAll(config.getList("optimization-methods.nametag-optimization.names", List.of("Optimize", "DisableAI"),
"Names are case insensitive, capital letters won't matter.").stream().map(String::toLowerCase).toList());
this.consume_nametag = config.getBoolean("optimization-methods.nametag-optimization.nametags-get-consumed", true,
"Enable or disable consumption of the used nametag item.");
this.cooldown = config.getInt("optimization-methods.nametag-optimization.optimize-cooldown-seconds", 600, """
Cooldown in seconds until a villager can be optimized again using a nametag.\s
Here for configuration freedom. Recommended to leave as is to not enable any exploitable behavior.""") * 1000L;
this.notify_player = config.getBoolean("optimization-methods.nametag-optimization.notify-player", true,
"Sends players a message when they successfully optimized a villager.");
this.log_enabled = config.getBoolean("optimization-methods.nametag-optimization.log", false);
}
@Override
public void enable() {
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@Override
public boolean shouldEnable() {
return VillagerOptimizer.getConfiguration().getBoolean("optimization-methods.nametag-optimization.enable", true);
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onPlayerInteractEntity(PlayerInteractEntityEvent event) {
if (!event.getRightClicked().getType().equals(EntityType.VILLAGER)) return;
Player player = event.getPlayer();
if (!player.hasPermission(Permissions.Optimize.NAMETAG.get())) return;
ItemStack usedItem = player.getInventory().getItem(event.getHand());
if (usedItem == null || !usedItem.getType().equals(Material.NAME_TAG)) return;
ItemMeta meta = usedItem.getItemMeta();
if (!meta.hasDisplayName()) return;
// Get component name first, so we can manually name the villager when canceling the event to avoid item consumption.
Component newVillagerName = meta.displayName();
assert newVillagerName != null; // Legitimate since we checked for hasDisplayName()
final String name = PlainTextComponentSerializer.plainText().serialize(newVillagerName);
Villager villager = (Villager) event.getRightClicked();
WrappedVillager wVillager = villagerCache.getOrAdd(villager);
if (nametags.contains(name.toLowerCase())) {
if (wVillager.canOptimize(cooldown) || player.hasPermission(Permissions.Bypass.NAMETAG_COOLDOWN.get())) {
VillagerOptimizeEvent optimizeEvent = new VillagerOptimizeEvent(wVillager, OptimizationType.NAMETAG, player, event.isAsynchronous());
if (!optimizeEvent.callEvent()) return;
if (!consume_nametag) {
event.setCancelled(true);
villager.customName(newVillagerName);
}
wVillager.setOptimization(optimizeEvent.getOptimizationType());
wVillager.saveOptimizeTime();
if (notify_player)
VillagerOptimizer.getLang(player.locale()).nametag_optimize_success.forEach(player::sendMessage);
if (log_enabled)
VillagerOptimizer.getLog().info(player.getName() + " optimized a villager using nametag: '" + name + "'");
} else {
event.setCancelled(true);
if (notify_player) {
final TextReplacementConfig timeLeft = TextReplacementConfig.builder()
.matchLiteral("%time%")
.replacement(CommonUtil.formatTime(wVillager.getOptimizeCooldownMillis(cooldown)))
.build();
VillagerOptimizer.getLang(player.locale()).nametag_on_optimize_cooldown.forEach(line -> player.sendMessage(line.replaceText(timeLeft)));
}
}
} else {
if (wVillager.isOptimized()) {
VillagerUnoptimizeEvent unOptimizeEvent = new VillagerUnoptimizeEvent(wVillager, player, OptimizationType.NAMETAG, event.isAsynchronous());
if (!unOptimizeEvent.callEvent()) return;
wVillager.setOptimization(OptimizationType.NONE);
if (notify_player)
VillagerOptimizer.getLang(player.locale()).nametag_unoptimize_success.forEach(player::sendMessage);
if (log_enabled)
VillagerOptimizer.getLog().info(event.getPlayer().getName() + " disabled optimizations for a villager using nametag: '" + name + "'");
}
}
}
}

View File

@ -1,200 +0,0 @@
package me.xginko.villageroptimizer.modules.optimization;
import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.WrappedVillager;
import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.enums.OptimizationType;
import me.xginko.villageroptimizer.enums.Permissions;
import me.xginko.villageroptimizer.events.VillagerOptimizeEvent;
import me.xginko.villageroptimizer.events.VillagerUnoptimizeEvent;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import me.xginko.villageroptimizer.utils.CommonUtil;
import net.kyori.adventure.text.TextReplacementConfig;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.entity.Villager;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent;
public class OptimizeByWorkstation implements VillagerOptimizerModule, Listener {
private final VillagerCache villagerCache;
private final long cooldown;
private final double search_radius;
private final boolean only_while_sneaking, log_enabled, notify_player;
public OptimizeByWorkstation() {
shouldEnable();
this.villagerCache = VillagerOptimizer.getCache();
Config config = VillagerOptimizer.getConfiguration();
config.addComment("optimization-methods.workstation-optimization.enable", """
When enabled, the closest villager near a matching workstation being placed will be optimized.\s
If a nearby matching workstation is broken, the villager will become unoptimized again.""");
this.search_radius = config.getDouble("optimization-methods.workstation-optimization.search-radius-in-blocks", 2.0, """
The radius in blocks a villager can be away from the player when he places a workstation.\s
The closest unoptimized villager to the player will be optimized.""") / 2;
this.cooldown = config.getInt("optimization-methods.workstation-optimization.optimize-cooldown-seconds", 600, """
Cooldown in seconds until a villager can be optimized again using a workstation.\s
Here for configuration freedom. Recommended to leave as is to not enable any exploitable behavior.""") * 1000L;
this.only_while_sneaking = config.getBoolean("optimization-methods.workstation-optimization.only-when-sneaking", true,
"Only optimize/unoptimize by workstation when player is sneaking during place or break");
this.notify_player = config.getBoolean("optimization-methods.workstation-optimization.notify-player", true,
"Sends players a message when they successfully optimized a villager.");
this.log_enabled = config.getBoolean("optimization-methods.workstation-optimization.log", false);
}
@Override
public void enable() {
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@Override
public boolean shouldEnable() {
return VillagerOptimizer.getConfiguration().getBoolean("optimization-methods.workstation-optimization.enable", false);
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onBlockPlace(BlockPlaceEvent event) {
Block placed = event.getBlock();
Villager.Profession workstationProfession = getWorkstationProfession(placed.getType());
if (workstationProfession.equals(Villager.Profession.NONE)) return;
Player player = event.getPlayer();
if (!player.hasPermission(Permissions.Optimize.WORKSTATION.get())) return;
if (only_while_sneaking && !player.isSneaking()) return;
final Location workstationLoc = placed.getLocation();
WrappedVillager closestOptimizableVillager = null;
double closestDistance = Double.MAX_VALUE;
for (Entity entity : workstationLoc.getNearbyEntities(search_radius, search_radius, search_radius)) {
if (!entity.getType().equals(EntityType.VILLAGER)) continue;
Villager villager = (Villager) entity;
if (!villager.getProfession().equals(workstationProfession)) continue;
WrappedVillager wVillager = villagerCache.getOrAdd(villager);
final double distance = entity.getLocation().distance(workstationLoc);
if (distance < closestDistance && wVillager.canOptimize(cooldown)) {
closestOptimizableVillager = wVillager;
closestDistance = distance;
}
}
if (closestOptimizableVillager == null) return;
if (closestOptimizableVillager.canOptimize(cooldown) || player.hasPermission(Permissions.Bypass.WORKSTATION_COOLDOWN.get())) {
VillagerOptimizeEvent optimizeEvent = new VillagerOptimizeEvent(closestOptimizableVillager, OptimizationType.WORKSTATION, player, event.isAsynchronous());
if (!optimizeEvent.callEvent()) return;
closestOptimizableVillager.setOptimization(optimizeEvent.getOptimizationType());
closestOptimizableVillager.saveOptimizeTime();
if (notify_player) {
final TextReplacementConfig vilProfession = TextReplacementConfig.builder()
.matchLiteral("%vil_profession%")
.replacement(closestOptimizableVillager.villager().getProfession().toString().toLowerCase())
.build();
final TextReplacementConfig placedWorkstation = TextReplacementConfig.builder()
.matchLiteral("%workstation%")
.replacement(placed.getType().toString().toLowerCase())
.build();
VillagerOptimizer.getLang(player.locale()).workstation_optimize_success.forEach(line -> player.sendMessage(line
.replaceText(vilProfession)
.replaceText(placedWorkstation)
));
}
if (log_enabled)
VillagerOptimizer.getLog().info(player.getName() + " optimized a villager using workstation: '" + placed.getType().toString().toLowerCase() + "'");
} else {
if (notify_player) {
final TextReplacementConfig timeLeft = TextReplacementConfig.builder()
.matchLiteral("%time%")
.replacement(CommonUtil.formatTime(closestOptimizableVillager.getOptimizeCooldownMillis(cooldown)))
.build();
VillagerOptimizer.getLang(player.locale()).nametag_on_optimize_cooldown.forEach(line -> player.sendMessage(line
.replaceText(timeLeft)
));
}
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onBlockBreak(BlockBreakEvent event) {
Block broken = event.getBlock();
Villager.Profession workstationProfession = getWorkstationProfession(broken.getType());
if (workstationProfession.equals(Villager.Profession.NONE)) return;
Player player = event.getPlayer();
if (!player.hasPermission(Permissions.Optimize.WORKSTATION.get())) return;
if (only_while_sneaking && !player.isSneaking()) return;
final Location workstationLoc = broken.getLocation();
WrappedVillager closestOptimizedVillager = null;
double closestDistance = Double.MAX_VALUE;
for (Entity entity : workstationLoc.getNearbyEntities(search_radius, search_radius, search_radius)) {
if (!entity.getType().equals(EntityType.VILLAGER)) continue;
Villager villager = (Villager) entity;
if (!villager.getProfession().equals(workstationProfession)) continue;
WrappedVillager wVillager = villagerCache.getOrAdd(villager);
final double distance = entity.getLocation().distance(workstationLoc);
if (distance < closestDistance && wVillager.canOptimize(cooldown)) {
closestOptimizedVillager = wVillager;
closestDistance = distance;
}
}
if (closestOptimizedVillager == null) return;
VillagerUnoptimizeEvent unOptimizeEvent = new VillagerUnoptimizeEvent(closestOptimizedVillager, player, OptimizationType.WORKSTATION, event.isAsynchronous());
if (!unOptimizeEvent.callEvent()) return;
closestOptimizedVillager.setOptimization(OptimizationType.NONE);
if (notify_player) {
final TextReplacementConfig vilProfession = TextReplacementConfig.builder()
.matchLiteral("%vil_profession%")
.replacement(closestOptimizedVillager.villager().getProfession().toString().toLowerCase())
.build();
final TextReplacementConfig brokenWorkstation = TextReplacementConfig.builder()
.matchLiteral("%workstation%")
.replacement(broken.getType().toString().toLowerCase())
.build();
VillagerOptimizer.getLang(player.locale()).workstation_unoptimize_success.forEach(line -> player.sendMessage(line
.replaceText(vilProfession)
.replaceText(brokenWorkstation)
));
}
if (log_enabled)
VillagerOptimizer.getLog().info(player.getName() + " unoptimized a villager by breaking workstation: '" + broken.getType().toString().toLowerCase() + "'");
}
private Villager.Profession getWorkstationProfession(final Material workstation) {
return switch (workstation) {
case BARREL -> Villager.Profession.FISHERMAN;
case CARTOGRAPHY_TABLE -> Villager.Profession.CARTOGRAPHER;
case SMOKER -> Villager.Profession.BUTCHER;
case SMITHING_TABLE -> Villager.Profession.TOOLSMITH;
case GRINDSTONE -> Villager.Profession.WEAPONSMITH;
case BLAST_FURNACE -> Villager.Profession.ARMORER;
case CAULDRON -> Villager.Profession.LEATHERWORKER;
case BREWING_STAND -> Villager.Profession.CLERIC;
case COMPOSTER -> Villager.Profession.FARMER;
case FLETCHING_TABLE -> Villager.Profession.FLETCHER;
case LOOM -> Villager.Profession.SHEPHERD;
case LECTERN -> Villager.Profession.LIBRARIAN;
case STONECUTTER -> Villager.Profession.MASON;
default -> Villager.Profession.NONE;
};
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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;
VillagerCache(long expireAfterWriteSeconds) {
this.villagerCache = Caffeine.newBuilder().expireAfterWrite(Duration.ofSeconds(expireAfterWriteSeconds)).build();
}
public @NotNull ConcurrentMap<UUID, WrappedVillager> cacheMap() {
return this.villagerCache.asMap();
}
public @Nullable WrappedVillager get(@NotNull UUID uuid) {
WrappedVillager wrappedVillager = this.villagerCache.getIfPresent(uuid);
return wrappedVillager == null && Bukkit.getEntity(uuid) instanceof Villager villager ? this.add(villager) : wrappedVillager;
}
public @NotNull WrappedVillager getOrAdd(@NotNull Villager villager) {
WrappedVillager wrappedVillager = this.villagerCache.getIfPresent(villager.getUniqueId());
return wrappedVillager == null ? this.add(new WrappedVillager(villager)) : this.add(wrappedVillager);
}
public @NotNull WrappedVillager add(@NotNull WrappedVillager villager) {
this.villagerCache.put(villager.villager().getUniqueId(), villager);
return villager;
}
public @NotNull WrappedVillager add(@NotNull Villager villager) {
return add(new WrappedVillager(villager));
}
public boolean contains(@NotNull UUID uuid) {
return this.villagerCache.getIfPresent(uuid) != null;
}
public boolean contains(@NotNull WrappedVillager villager) {
return this.contains(villager.villager().getUniqueId());
}
public boolean contains(@NotNull Villager villager) {
return this.contains(villager.getUniqueId());
}
}

View File

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

View File

@ -1,192 +0,0 @@
package me.xginko.villageroptimizer;
import me.xginko.villageroptimizer.enums.Keys;
import me.xginko.villageroptimizer.enums.OptimizationType;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.entity.Villager;
import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.persistence.PersistentDataType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public final class WrappedVillager {
private final @NotNull Villager villager;
private final @NotNull PersistentDataContainer dataContainer;
WrappedVillager(@NotNull Villager villager) {
this.villager = villager;
this.dataContainer = villager.getPersistentDataContainer();
}
/**
* @return The villager inside the wrapper.
*/
public @NotNull Villager villager() {
return villager;
}
/**
* @return The data container inside the wrapper.
*/
public @NotNull PersistentDataContainer dataContainer() {
return dataContainer;
}
/**
* @return True if the villager is optimized by this plugin, otherwise false.
*/
public boolean isOptimized() {
return dataContainer.has(Keys.OPTIMIZATION_TYPE.key());
}
/**
* @param cooldown_millis The configured cooldown in millis until the next optimization is allowed to occur.
* @return True if villager can be optimized again, otherwise false.
*/
public boolean canOptimize(final long cooldown_millis) {
return getLastOptimize() + cooldown_millis <= System.currentTimeMillis();
}
/**
* @param type OptimizationType the villager should be set to.
*/
public void setOptimization(OptimizationType type) {
if (type.equals(OptimizationType.NONE) && isOptimized()) {
dataContainer.remove(Keys.OPTIMIZATION_TYPE.key());
villager.getScheduler().run(VillagerOptimizer.getInstance(), enableAI -> {
villager.setAware(true);
villager.setAI(true);
}, null);
} else {
dataContainer.set(Keys.OPTIMIZATION_TYPE.key(), PersistentDataType.STRING, type.name());
villager.getScheduler().run(VillagerOptimizer.getInstance(), disableAI -> {
villager.setAware(false);
}, null);
}
}
/**
* @return The current OptimizationType of the villager.
*/
public @NotNull OptimizationType getOptimizationType() {
return isOptimized() ? OptimizationType.valueOf(dataContainer.get(Keys.OPTIMIZATION_TYPE.key(), PersistentDataType.STRING)) : OptimizationType.NONE;
}
/**
* Saves the system time in millis when the villager was last optimized.
*/
public void saveOptimizeTime() {
dataContainer.set(Keys.LAST_OPTIMIZE.key(), PersistentDataType.LONG, System.currentTimeMillis());
}
/**
* @return The system time in millis when the villager was last optimized, 0L if the villager was never optimized.
*/
public long getLastOptimize() {
return dataContainer.has(Keys.LAST_OPTIMIZE.key(), PersistentDataType.LONG) ? dataContainer.get(Keys.LAST_OPTIMIZE.key(), PersistentDataType.LONG) : 0L;
}
/**
* Here for convenience so the remaining millis since the last stored optimize time
* can be easily calculated.
* This enables new configured cooldowns to instantly apply instead of them being persistent.
*
* @param cooldown_millis The configured cooldown in milliseconds you want to check against.
* @return The time left in millis until the villager can be optimized again.
*/
public long getOptimizeCooldownMillis(final long cooldown_millis) {
return dataContainer.has(Keys.LAST_OPTIMIZE.key(), PersistentDataType.LONG) ? (System.currentTimeMillis() - (dataContainer.get(Keys.LAST_OPTIMIZE.key(), PersistentDataType.LONG) + cooldown_millis)) : cooldown_millis;
}
/**
* Here for convenience so the remaining millis since the last stored restock time
* can be easily calculated.
*
* @param cooldown_millis The configured cooldown in milliseconds you want to check against.
* @return True if the villager has been loaded long enough.
*/
public boolean canRestock(final long cooldown_millis) {
return getLastRestock() + cooldown_millis <= villager.getWorld().getFullTime();
}
/**
* Restock all trading recipes.
*/
public void restock() {
villager.getRecipes().forEach(recipe -> recipe.setUses(0));
}
/**
* Saves the time of the in-game world when the entity was last restocked.
*/
public void saveRestockTime() {
dataContainer.set(Keys.LAST_RESTOCK.key(), PersistentDataType.LONG, villager.getWorld().getFullTime());
}
/**
* @return The time of the in-game world when the entity was last restocked.
*/
public long getLastRestock() {
return dataContainer.has(Keys.LAST_RESTOCK.key(), PersistentDataType.LONG) ? dataContainer.get(Keys.LAST_RESTOCK.key(), PersistentDataType.LONG) : 0L;
}
public long getRestockCooldownMillis(final long cooldown_millis) {
return dataContainer.has(Keys.LAST_RESTOCK.key(), PersistentDataType.LONG) ? (villager.getWorld().getFullTime() - (dataContainer.get(Keys.LAST_RESTOCK.key(), PersistentDataType.LONG) + cooldown_millis)) : cooldown_millis;
}
/**
* @return The level between 1-5 calculated from the villagers experience.
*/
public int calculateLevel() {
// https://minecraft.fandom.com/wiki/Trading#Mechanics
int vilEXP = villager.getVillagerExperience();
if (vilEXP >= 250) return 5;
if (vilEXP >= 150) return 4;
if (vilEXP >= 70) return 3;
if (vilEXP >= 10) return 2;
return 1;
}
/**
* @param cooldown_millis The configured cooldown in milliseconds you want to check against.
* @return Whether the villager can be leveled up or not with the checked milliseconds
*/
public boolean canLevelUp(final long cooldown_millis) {
return getLastLevelUpTime() + cooldown_millis <= villager.getWorld().getFullTime();
}
/**
* Saves the time of the in-game world when the entity was last leveled up.
*/
public void saveLastLevelUp() {
dataContainer.set(Keys.LAST_LEVELUP.key(), PersistentDataType.LONG, villager.getWorld().getFullTime());
}
/**
* Here for convenience so the remaining millis since the last stored level-up time
* can be easily calculated.
*
* @return The time of the in-game world when the entity was last leveled up.
*/
public long getLastLevelUpTime() {
return dataContainer.has(Keys.LAST_LEVELUP.key(), PersistentDataType.LONG) ? dataContainer.get(Keys.LAST_LEVELUP.key(), PersistentDataType.LONG) : 0L;
}
public long getLevelCooldownMillis(final long cooldown_millis) {
return dataContainer.has(Keys.LAST_LEVELUP.key(), PersistentDataType.LONG) ? (villager.getWorld().getFullTime() - (dataContainer.get(Keys.LAST_LEVELUP.key(), PersistentDataType.LONG) + cooldown_millis)) : cooldown_millis;
}
public void memorizeName(final Component customName) {
dataContainer.set(Keys.LAST_OPTIMIZE_NAME.key(), PersistentDataType.STRING, MiniMessage.miniMessage().serialize(customName));
}
public @Nullable Component getMemorizedName() {
return dataContainer.has(Keys.LAST_OPTIMIZE_NAME.key()) ? MiniMessage.miniMessage().deserialize(dataContainer.get(Keys.LAST_OPTIMIZE_NAME.key(), PersistentDataType.STRING)) : null;
}
public void forgetName() {
dataContainer.remove(Keys.LAST_OPTIMIZE_NAME.key());
}
}

View File

@ -1,11 +0,0 @@
package me.xginko.villageroptimizer.commands;
import net.kyori.adventure.text.TextComponent;
import org.bukkit.command.CommandSender;
public abstract class SubCommand {
public abstract String getLabel();
public abstract TextComponent getDescription();
public abstract TextComponent getSyntax();
public abstract void perform(CommandSender sender, String[] args);
}

View File

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

View File

@ -1,142 +0,0 @@
package me.xginko.villageroptimizer.commands.optimizevillagers;
import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.WrappedVillager;
import me.xginko.villageroptimizer.commands.VillagerOptimizerCommand;
import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.enums.OptimizationType;
import me.xginko.villageroptimizer.enums.Permissions;
import me.xginko.villageroptimizer.events.VillagerOptimizeEvent;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextReplacementConfig;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextDecoration;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.entity.Villager;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
public class OptVillagersRadius implements VillagerOptimizerCommand, TabCompleter {
private final List<String> tabCompletes = List.of("5", "10", "25", "50");
private final long cooldown;
private final int max_radius;
public OptVillagersRadius() {
Config config = VillagerOptimizer.getConfiguration();
this.max_radius = config.getInt("optimization-methods.commands.optimizevillagers.max-block-radius", 100);
this.cooldown = config.getInt("optimization-methods.commands.optimizevillagers.cooldown-seconds", 600, """
Cooldown in seconds until a villager can be optimized again using the command.\s
Here for configuration freedom. Recommended to leave as is to not enable any exploitable behavior.""") * 1000L;
}
@Override
public String label() {
return "optimizevillagers";
}
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
return args.length == 1 ? tabCompletes : null;
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
if (!(sender instanceof Player player)) {
sender.sendMessage(Component.text("This command can only be executed by a player.")
.color(NamedTextColor.RED).decorate(TextDecoration.BOLD));
return true;
}
if (!sender.hasPermission(Permissions.Commands.OPTIMIZE_RADIUS.get())) {
sender.sendMessage(VillagerOptimizer.getLang(sender).no_permission);
return true;
}
if (args.length != 1) {
VillagerOptimizer.getLang(player.locale()).command_specify_radius.forEach(player::sendMessage);
return true;
}
try {
int specifiedRadius = Integer.parseInt(args[0]);
if (specifiedRadius > max_radius) {
final TextReplacementConfig limit = TextReplacementConfig.builder()
.matchLiteral("%distance%")
.replacement(Integer.toString(max_radius))
.build();
VillagerOptimizer.getLang(player.locale()).command_radius_limit_exceed.forEach(line -> player.sendMessage(line.replaceText(limit)));
return true;
}
VillagerCache villagerCache = VillagerOptimizer.getCache();
int successCount = 0;
int failCount = 0;
final boolean player_has_cooldown_bypass = player.hasPermission(Permissions.Bypass.COMMAND_COOLDOWN.get());
for (Entity entity : player.getNearbyEntities(specifiedRadius, specifiedRadius, specifiedRadius)) {
if (!entity.getType().equals(EntityType.VILLAGER)) continue;
Villager villager = (Villager) entity;
Villager.Profession profession = villager.getProfession();
if (profession.equals(Villager.Profession.NITWIT) || profession.equals(Villager.Profession.NONE)) continue;
WrappedVillager wVillager = villagerCache.getOrAdd(villager);
if (player_has_cooldown_bypass || wVillager.canOptimize(cooldown)) {
VillagerOptimizeEvent optimizeEvent = new VillagerOptimizeEvent(wVillager, OptimizationType.COMMAND, player);
if (optimizeEvent.callEvent()) {
wVillager.setOptimization(optimizeEvent.getOptimizationType());
wVillager.saveOptimizeTime();
successCount++;
}
} else {
failCount++;
}
}
if (successCount <= 0 && failCount <= 0) {
final TextReplacementConfig radius = TextReplacementConfig.builder()
.matchLiteral("%radius%")
.replacement(Integer.toString(specifiedRadius))
.build();
VillagerOptimizer.getLang(player.locale()).command_no_villagers_nearby.forEach(line -> player.sendMessage(line.replaceText(radius)));
return true;
}
if (successCount > 0) {
final TextReplacementConfig success_amount = TextReplacementConfig.builder()
.matchLiteral("%amount%")
.replacement(Integer.toString(successCount))
.build();
final TextReplacementConfig radius = TextReplacementConfig.builder()
.matchLiteral("%radius%")
.replacement(Integer.toString(specifiedRadius))
.build();
VillagerOptimizer.getLang(player.locale()).command_optimize_success.forEach(line -> player.sendMessage(line
.replaceText(success_amount)
.replaceText(radius)
));
}
if (failCount > 0) {
final TextReplacementConfig alreadyOptimized = TextReplacementConfig.builder()
.matchLiteral("%amount%")
.replacement(Integer.toString(failCount))
.build();
VillagerOptimizer.getLang(player.locale()).command_optimize_fail.forEach(line -> player.sendMessage(line.replaceText(alreadyOptimized)));
}
} catch (NumberFormatException e) {
VillagerOptimizer.getLang(player.locale()).command_radius_invalid.forEach(player::sendMessage);
}
return true;
}
}

View File

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

View File

@ -1,79 +0,0 @@
package me.xginko.villageroptimizer.commands.villageroptimizer;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.commands.SubCommand;
import me.xginko.villageroptimizer.commands.VillagerOptimizerCommand;
import me.xginko.villageroptimizer.commands.villageroptimizer.subcommands.DisableSubCmd;
import me.xginko.villageroptimizer.commands.villageroptimizer.subcommands.ReloadSubCmd;
import me.xginko.villageroptimizer.commands.villageroptimizer.subcommands.VersionSubCmd;
import me.xginko.villageroptimizer.enums.Permissions;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
public class VillagerOptimizerCmd implements TabCompleter, VillagerOptimizerCommand {
private final List<SubCommand> subCommands = new ArrayList<>(3);
private final List<String> tabCompleter = new ArrayList<>(3);
public VillagerOptimizerCmd() {
subCommands.add(new ReloadSubCmd());
subCommands.add(new VersionSubCmd());
subCommands.add(new DisableSubCmd());
subCommands.forEach(subCommand -> tabCompleter.add(subCommand.getLabel()));
}
@Override
public String label() {
return "villageroptimizer";
}
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, String[] args) {
return args.length == 1 ? tabCompleter : null;
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
if (args.length > 0) {
boolean cmdExists = false;
for (SubCommand subCommand : subCommands) {
if (args[0].equalsIgnoreCase(subCommand.getLabel())) {
subCommand.perform(sender, args);
cmdExists = true;
break;
}
}
if (!cmdExists) sendCommandOverview(sender);
} else {
sendCommandOverview(sender);
}
return true;
}
private void sendCommandOverview(CommandSender sender) {
if (!sender.hasPermission(Permissions.Commands.RELOAD.get()) && !sender.hasPermission(Permissions.Commands.VERSION.get())) return;
sender.sendMessage(Component.text("-----------------------------------------------------").color(NamedTextColor.GRAY));
sender.sendMessage(Component.text("VillagerOptimizer Commands").color(VillagerOptimizer.plugin_style.color()));
sender.sendMessage(Component.text("-----------------------------------------------------").color(NamedTextColor.GRAY));
subCommands.forEach(subCommand -> sender.sendMessage(
subCommand.getSyntax().append(Component.text(" - ").color(NamedTextColor.DARK_GRAY)).append(subCommand.getDescription())));
sender.sendMessage(
Component.text("/optimizevillagers <blockradius>").color(VillagerOptimizer.plugin_style.color())
.append(Component.text(" - ").color(NamedTextColor.DARK_GRAY))
.append(Component.text("Optimize villagers in a radius").color(NamedTextColor.GRAY))
);
sender.sendMessage(
Component.text("/unoptmizevillagers <blockradius>").color(VillagerOptimizer.plugin_style.color())
.append(Component.text(" - ").color(NamedTextColor.DARK_GRAY))
.append(Component.text("Unoptimize villagers in a radius").color(NamedTextColor.GRAY))
);
sender.sendMessage(Component.text("-----------------------------------------------------").color(NamedTextColor.GRAY));
}
}

View File

@ -1,42 +0,0 @@
package me.xginko.villageroptimizer.commands.villageroptimizer.subcommands;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.commands.SubCommand;
import me.xginko.villageroptimizer.enums.Permissions;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.CommandSender;
public class DisableSubCmd extends SubCommand {
@Override
public String getLabel() {
return "disable";
}
@Override
public TextComponent getDescription() {
return Component.text("Disable all plugin tasks and listeners.").color(NamedTextColor.GRAY);
}
@Override
public TextComponent getSyntax() {
return Component.text("/villageroptimizer disable").color(VillagerOptimizer.plugin_style.color());
}
@Override
public void perform(CommandSender sender, String[] args) {
if (sender.hasPermission(Permissions.Commands.DISABLE.get())) {
sender.sendMessage(Component.text("Disabling VillagerOptimizer...").color(NamedTextColor.RED));
VillagerOptimizerModule.modules.forEach(VillagerOptimizerModule::disable);
VillagerOptimizerModule.modules.clear();
VillagerOptimizer.getCache().cacheMap().clear();
sender.sendMessage(Component.text("Disabled all plugin listeners and tasks.").color(NamedTextColor.GREEN));
sender.sendMessage(Component.text("You can enable the plugin again using the reload command.").color(NamedTextColor.YELLOW));
} else {
sender.sendMessage(VillagerOptimizer.getLang(sender).no_permission);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,182 +0,0 @@
package me.xginko.villageroptimizer.modules;
import io.papermc.paper.threadedregions.scheduler.ScheduledTask;
import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.utils.LogUtil;
import org.bukkit.Chunk;
import org.bukkit.Server;
import org.bukkit.World;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Villager;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.CreatureSpawnEvent;
import org.bukkit.event.player.PlayerInteractEntityEvent;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.logging.Level;
public class VillagerChunkLimit implements VillagerOptimizerModule, Listener {
private final VillagerCache villagerCache;
private ScheduledTask periodic_chunk_check;
private final List<Villager.Profession> non_optimized_removal_priority = new ArrayList<>(16);
private final List<Villager.Profession> optimized_removal_priority = new ArrayList<>(16);
private final long check_period;
private final int non_optimized_max_per_chunk, optimized_max_per_chunk;
private final boolean log_enabled, skip_unloaded_entity_chunks;
protected VillagerChunkLimit() {
shouldEnable();
this.villagerCache = VillagerOptimizer.getCache();
Config config = VillagerOptimizer.getConfiguration();
config.addComment("villager-chunk-limit.enable", """
Checks chunks for too many villagers and removes excess villagers based on priority.""");
this.check_period = config.getInt("villager-chunk-limit.check-period-in-ticks", 600, """
Check all loaded chunks every X ticks. 1 second = 20 ticks\s
A shorter delay in between checks is more efficient but is also more resource intense.\s
A larger delay is less resource intense but could become inefficient.""");
this.skip_unloaded_entity_chunks = config.getBoolean("villager-chunk-limit.skip-if-chunk-has-not-loaded-entities", true,
"Does not check chunks that don't have their entities loaded.");
this.log_enabled = config.getBoolean("villager-chunk-limit.log-removals", false);
this.non_optimized_max_per_chunk = config.getInt("villager-chunk-limit.unoptimized.max-per-chunk", 20,
"The maximum amount of unoptimized villagers per chunk.");
config.getList("villager-chunk-limit.unoptimized.removal-priority", List.of(
"NONE", "NITWIT", "SHEPHERD", "FISHERMAN", "BUTCHER", "CARTOGRAPHER", "LEATHERWORKER",
"FLETCHER", "MASON", "FARMER", "ARMORER", "TOOLSMITH", "WEAPONSMITH", "CLERIC", "LIBRARIAN"
), """
Professions that are in the top of the list are going to be scheduled for removal first.\s
Use enums from https://jd.papermc.io/paper/1.20/org/bukkit/entity/Villager.Profession.html"""
).forEach(configuredProfession -> {
try {
Villager.Profession profession = Villager.Profession.valueOf(configuredProfession);
this.non_optimized_removal_priority.add(profession);
} catch (IllegalArgumentException e) {
LogUtil.moduleLog(Level.WARNING, "villager-chunk-limit.unoptimized",
"Villager profession '"+configuredProfession+"' not recognized. " +
"Make sure you're using the correct profession enums from https://jd.papermc.io/paper/1.20/org/bukkit/entity/Villager.Profession.html.");
}
});
this.optimized_max_per_chunk = config.getInt("villager-chunk-limit.optimized.max-per-chunk", 60,
"The maximum amount of optimized villagers per chunk.");
config.getList("villager-chunk-limit.optimized.removal-priority", List.of(
"NONE", "NITWIT", "SHEPHERD", "FISHERMAN", "BUTCHER", "CARTOGRAPHER", "LEATHERWORKER",
"FLETCHER", "MASON", "FARMER", "ARMORER", "TOOLSMITH", "WEAPONSMITH", "CLERIC", "LIBRARIAN"
)).forEach(configuredProfession -> {
try {
Villager.Profession profession = Villager.Profession.valueOf(configuredProfession);
this.optimized_removal_priority.add(profession);
} catch (IllegalArgumentException e) {
LogUtil.moduleLog(Level.WARNING, "villager-chunk-limit.optimized",
"Villager profession '"+configuredProfession+"' not recognized. " +
"Make sure you're using the correct profession enums from https://jd.papermc.io/paper/1.20/org/bukkit/entity/Villager.Profession.html.");
}
});
}
@Override
public void enable() {
final VillagerOptimizer plugin = VillagerOptimizer.getInstance();
final Server server = plugin.getServer();
server.getPluginManager().registerEvents(this, plugin);
this.periodic_chunk_check = server.getGlobalRegionScheduler().runAtFixedRate(plugin, periodic_chunk_check -> {
for (World world : server.getWorlds()) {
for (Chunk chunk : world.getLoadedChunks()) {
plugin.getServer().getRegionScheduler().run(
plugin, world, chunk.getX(), chunk.getZ(), check_chunk -> this.manageVillagerCount(chunk)
);
}
}
}, check_period, check_period);
}
@Override
public boolean shouldEnable() {
return VillagerOptimizer.getConfiguration().getBoolean("villager-chunk-limit.enable", false);
}
@Override
public void disable() {
HandlerList.unregisterAll(this);
if (periodic_chunk_check != null) periodic_chunk_check.cancel();
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onCreatureSpawn(CreatureSpawnEvent event) {
Entity spawned = event.getEntity();
if (spawned.getType().equals(EntityType.VILLAGER)) {
this.manageVillagerCount(spawned.getChunk());
}
}
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
private void onInteract(PlayerInteractEntityEvent event) {
Entity clicked = event.getRightClicked();
if (clicked.getType().equals(EntityType.VILLAGER)) {
this.manageVillagerCount(clicked.getChunk());
}
}
private void manageVillagerCount(@NotNull Chunk chunk) {
if (skip_unloaded_entity_chunks && !chunk.isEntitiesLoaded()) return;
// Collect all optimized and unoptimized villagers in that chunk
List<Villager> optimized_villagers = new ArrayList<>();
List<Villager> not_optimized_villagers = new ArrayList<>();
for (Entity entity : chunk.getEntities()) {
if (entity.getType().equals(EntityType.VILLAGER)) {
Villager villager = (Villager) entity;
if (villagerCache.getOrAdd(villager).isOptimized()) {
optimized_villagers.add(villager);
} else {
not_optimized_villagers.add(villager);
}
}
}
// Check if there are more unoptimized villagers in that chunk than allowed
final int not_optimized_villagers_too_many = not_optimized_villagers.size() - non_optimized_max_per_chunk;
if (not_optimized_villagers_too_many > 0) {
// Sort villagers by profession priority
not_optimized_villagers.sort(Comparator.comparingInt(villager -> {
final Villager.Profession profession = villager.getProfession();
return non_optimized_removal_priority.contains(profession) ? non_optimized_removal_priority.indexOf(profession) : Integer.MAX_VALUE;
}));
// Remove prioritized villagers that are too many
for (int i = 0; i < not_optimized_villagers_too_many; i++) {
Villager villager = not_optimized_villagers.get(i);
villager.remove();
if (log_enabled) LogUtil.moduleLog(Level.INFO, "villager-chunk-limit",
"Removed unoptimized villager of profession type '"+villager.getProfession().name()+"' at "+villager.getLocation()
);
}
}
// Check if there are more optimized villagers in that chunk than allowed
final int optimized_villagers_too_many = optimized_villagers.size() - optimized_max_per_chunk;
if (optimized_villagers_too_many > 0) {
// Sort villagers by profession priority
optimized_villagers.sort(Comparator.comparingInt(villager -> {
final Villager.Profession profession = villager.getProfession();
return optimized_removal_priority.contains(profession) ? optimized_removal_priority.indexOf(profession) : Integer.MAX_VALUE;
}));
// Remove prioritized villagers that are too many
for (int i = 0; i < optimized_villagers_too_many; i++) {
Villager villager = optimized_villagers.get(i);
villager.remove();
if (log_enabled) LogUtil.moduleLog(Level.INFO, "villager-chunk-limit",
"Removed optimized villager of profession type '"+villager.getProfession().name()+"' at "+villager.getLocation()
);
}
}
}
}

View File

@ -1,40 +0,0 @@
package me.xginko.villageroptimizer.modules;
import me.xginko.villageroptimizer.modules.gameplay.*;
import me.xginko.villageroptimizer.modules.optimization.OptimizeByBlock;
import me.xginko.villageroptimizer.modules.optimization.OptimizeByNametag;
import me.xginko.villageroptimizer.modules.optimization.OptimizeByWorkstation;
import java.util.HashSet;
public interface VillagerOptimizerModule {
void enable();
void disable();
boolean shouldEnable();
HashSet<VillagerOptimizerModule> modules = new HashSet<>();
static void reloadModules() {
modules.forEach(VillagerOptimizerModule::disable);
modules.clear();
modules.add(new OptimizeByNametag());
modules.add(new OptimizeByBlock());
modules.add(new OptimizeByWorkstation());
modules.add(new RestockOptimizedTrades());
modules.add(new LevelOptimizedProfession());
modules.add(new RenameOptimizedVillagers());
modules.add(new MakeVillagersSpawnAdult());
modules.add(new PreventUnoptimizedTrading());
modules.add(new PreventOptimizedTargeting());
modules.add(new PreventOptimizedDamage());
modules.add(new VillagerChunkLimit());
modules.forEach(module -> {
if (module.shouldEnable()) module.enable();
});
}
}

View File

@ -1,91 +0,0 @@
package me.xginko.villageroptimizer.modules.gameplay;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.WrappedVillager;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import me.xginko.villageroptimizer.utils.CommonUtil;
import net.kyori.adventure.text.TextReplacementConfig;
import org.bukkit.entity.Player;
import org.bukkit.entity.Villager;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
public class LevelOptimizedProfession implements VillagerOptimizerModule, Listener {
private final VillagerOptimizer plugin;
private final VillagerCache villagerCache;
private final boolean notify_player;
private final long cooldown;
public LevelOptimizedProfession() {
shouldEnable();
this.plugin = VillagerOptimizer.getInstance();
this.villagerCache = VillagerOptimizer.getCache();
Config config = VillagerOptimizer.getConfiguration();
config.addComment("gameplay.level-optimized-profession", """
This is needed to allow optimized villagers to level up.\s
Temporarily enables the villagers AI to allow it to level up and then disables it again.""");
this.cooldown = config.getInt("gameplay.level-optimized-profession.level-check-cooldown-seconds", 5, """
Cooldown in seconds until the level of a villager will be checked and updated again.\s
Recommended to leave as is.""") * 1000L;
this.notify_player = config.getBoolean("gameplay.level-optimized-profession.notify-player", true,
"Tell players to wait when a villager is leveling up.");
}
@Override
public void enable() {
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@Override
public void disable() {
HandlerList.unregisterAll(this);
}
@Override
public boolean shouldEnable() {
return true;
}
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
private void onTradeScreenClose(InventoryCloseEvent event) {
if (
event.getInventory().getType().equals(InventoryType.MERCHANT)
&& event.getInventory().getHolder() instanceof Villager villager
) {
WrappedVillager wVillager = villagerCache.getOrAdd(villager);
if (!wVillager.isOptimized()) return;
if (wVillager.canLevelUp(cooldown)) {
if (wVillager.calculateLevel() > villager.getVillagerLevel()) {
villager.getScheduler().run(plugin, enableAI -> {
villager.addPotionEffect(new PotionEffect(PotionEffectType.SLOW, 120, 120, false, false));
villager.setAware(true);
}, null);
villager.getScheduler().runDelayed(plugin, disableAI -> {
villager.setAware(false);
wVillager.saveLastLevelUp();
}, null, 100L);
}
} else {
if (notify_player) {
Player player = (Player) event.getPlayer();
final TextReplacementConfig timeLeft = TextReplacementConfig.builder()
.matchLiteral("%time%")
.replacement(CommonUtil.formatTime(wVillager.getLevelCooldownMillis(cooldown)))
.build();
VillagerOptimizer.getLang(player.locale()).villager_leveling_up.forEach(line -> player.sendMessage(line.replaceText(timeLeft)));
}
}
}
}
}

View File

@ -1,86 +0,0 @@
package me.xginko.villageroptimizer.modules.gameplay;
import io.papermc.paper.event.entity.EntityPushedByEntityAttackEvent;
import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import me.xginko.villageroptimizer.utils.LogUtil;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Villager;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageEvent;
import java.util.Arrays;
import java.util.HashSet;
public class PreventOptimizedDamage implements VillagerOptimizerModule, Listener {
private final VillagerCache villagerCache;
private final HashSet<EntityDamageEvent.DamageCause> damage_causes_to_cancel = new HashSet<>();
private final boolean push;
public PreventOptimizedDamage() {
shouldEnable();
this.villagerCache = VillagerOptimizer.getCache();
Config config = VillagerOptimizer.getConfiguration();
config.addComment("gameplay.prevent-damage-to-optimized.enable",
"Configure what kind of damage you want to cancel for optimized villagers here.");
this.push = config.getBoolean("gameplay.prevent-damage-to-optimized.prevent-push-from-attack", true,
"Prevents optimized villagers from getting pushed by an attacking entity");
config.getList("gameplay.prevent-damage-to-optimized.damage-causes-to-cancel",
Arrays.stream(EntityDamageEvent.DamageCause.values()).map(Enum::name).sorted().toList(), """
These are all current entries in the game. Remove what you do not need blocked.\s
If you want a description or need to add a previously removed type, refer to:\s
https://jd.papermc.io/paper/1.20/org/bukkit/event/entity/EntityDamageEvent.DamageCause.html"""
).forEach(configuredDamageCause -> {
try {
EntityDamageEvent.DamageCause damageCause = EntityDamageEvent.DamageCause.valueOf(configuredDamageCause);
this.damage_causes_to_cancel.add(damageCause);
} catch (IllegalArgumentException e) {
LogUtil.damageCauseNotRecognized("prevent-damage-to-optimized", configuredDamageCause);
}
});
}
@Override
public void enable() {
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@Override
public void disable() {
HandlerList.unregisterAll(this);
}
@Override
public boolean shouldEnable() {
return VillagerOptimizer.getConfiguration().getBoolean("gameplay.prevent-damage-to-optimized.enable", true);
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onDamageByEntity(EntityDamageEvent event) {
if (
event.getEntityType().equals(EntityType.VILLAGER)
&& damage_causes_to_cancel.contains(event.getCause())
&& villagerCache.getOrAdd((Villager) event.getEntity()).isOptimized()
) {
event.setCancelled(true);
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onPushByEntityAttack(EntityPushedByEntityAttackEvent event) {
if (
push
&& event.getEntityType().equals(EntityType.VILLAGER)
&& villagerCache.getOrAdd((Villager) event.getEntity()).isOptimized()
) {
event.setCancelled(true);
}
}
}

View File

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

View File

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

View File

@ -1,80 +0,0 @@
package me.xginko.villageroptimizer.modules.gameplay;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.enums.Permissions;
import me.xginko.villageroptimizer.WrappedVillager;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import me.xginko.villageroptimizer.utils.CommonUtil;
import net.kyori.adventure.text.TextReplacementConfig;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.entity.Villager;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerInteractEntityEvent;
public class RestockOptimizedTrades implements VillagerOptimizerModule, Listener {
private final VillagerCache villagerCache;
private final long restock_delay_millis;
private final boolean log_enabled, notify_player;
public RestockOptimizedTrades() {
shouldEnable();
this.villagerCache = VillagerOptimizer.getCache();
Config config = VillagerOptimizer.getConfiguration();
config.addComment("gameplay.restock-optimized-trades", """
This is for automatic restocking of trades for optimized villagers. Optimized Villagers\s
don't have enough AI to restock their trades naturally, so this is here as a workaround.""");
this.restock_delay_millis = config.getInt("gameplay.restock-optimized-trades.delay-in-ticks", 1000,
"1 second = 20 ticks. There are 24.000 ticks in a single minecraft day.") * 50L;
this.notify_player = config.getBoolean("gameplay.restock-optimized-trades.notify-player", true,
"Sends the player a message when the trades were restocked on a clicked villager.");
this.log_enabled = config.getBoolean("gameplay.restock-optimized-trades.log", false);
}
@Override
public void enable() {
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@Override
public void disable() {
HandlerList.unregisterAll(this);
}
@Override
public boolean shouldEnable() {
return true;
}
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
private void onInteract(PlayerInteractEntityEvent event) {
if (!event.getRightClicked().getType().equals(EntityType.VILLAGER)) return;
WrappedVillager wVillager = villagerCache.getOrAdd((Villager) event.getRightClicked());
if (!wVillager.isOptimized()) return;
Player player = event.getPlayer();
final boolean player_bypassing = player.hasPermission(Permissions.Bypass.RESTOCK_COOLDOWN.get());
if (wVillager.canRestock(restock_delay_millis) || player_bypassing) {
wVillager.restock();
wVillager.saveRestockTime();
if (notify_player && !player_bypassing) {
final TextReplacementConfig timeLeft = TextReplacementConfig.builder()
.matchLiteral("%time%")
.replacement(CommonUtil.formatTime(wVillager.getRestockCooldownMillis(restock_delay_millis)))
.build();
VillagerOptimizer.getLang(player.locale()).trades_restocked.forEach(line -> player.sendMessage(line.replaceText(timeLeft)));
}
if (log_enabled)
VillagerOptimizer.getLog().info("Restocked optimized villager at "+ wVillager.villager().getLocation());
}
}
}

View File

@ -1,200 +0,0 @@
package me.xginko.villageroptimizer.modules.optimization;
import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.WrappedVillager;
import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.enums.OptimizationType;
import me.xginko.villageroptimizer.enums.Permissions;
import me.xginko.villageroptimizer.events.VillagerOptimizeEvent;
import me.xginko.villageroptimizer.events.VillagerUnoptimizeEvent;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import me.xginko.villageroptimizer.utils.CommonUtil;
import me.xginko.villageroptimizer.utils.LogUtil;
import net.kyori.adventure.text.TextReplacementConfig;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.entity.Villager;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import java.util.HashSet;
import java.util.List;
public class OptimizeByBlock implements VillagerOptimizerModule, Listener {
private final VillagerCache villagerCache;
private final HashSet<Material> blocks_that_disable = new HashSet<>(4);
private final long cooldown;
private final double search_radius;
private final boolean only_while_sneaking, notify_player, log_enabled;
public OptimizeByBlock() {
shouldEnable();
this.villagerCache = VillagerOptimizer.getCache();
Config config = VillagerOptimizer.getConfiguration();
config.addComment("optimization-methods.block-optimization.enable", """
When enabled, the closest villager standing near a configured block being placed will be optimized.\s
If a configured block is broken nearby, the closest villager will become unoptimized again.""");
config.getList("optimization-methods.block-optimization.materials", List.of(
"LAPIS_BLOCK", "GLOWSTONE", "IRON_BLOCK"
), "Values here need to be valid bukkit Material enums for your server version."
).forEach(configuredMaterial -> {
try {
Material disableBlock = Material.valueOf(configuredMaterial);
this.blocks_that_disable.add(disableBlock);
} catch (IllegalArgumentException e) {
LogUtil.materialNotRecognized("block-optimization", configuredMaterial);
}
});
this.cooldown = config.getInt("optimization-methods.block-optimization.optimize-cooldown-seconds", 600, """
Cooldown in seconds until a villager can be optimized again by using specific blocks. \s
Here for configuration freedom. Recommended to leave as is to not enable any exploitable behavior.""") * 1000L;
this.search_radius = config.getDouble("optimization-methods.block-optimization.search-radius-in-blocks", 2.0, """
The radius in blocks a villager can be away from the player when he places an optimize block.\s
The closest unoptimized villager to the player will be optimized.""") / 2;
this.only_while_sneaking = config.getBoolean("optimization-methods.block-optimization.only-when-sneaking", true,
"Only optimize/unoptimize by workstation when player is sneaking during place or break.");
this.notify_player = config.getBoolean("optimization-methods.block-optimization.notify-player", true,
"Sends players a message when they successfully optimized or unoptimized a villager.");
this.log_enabled = config.getBoolean("optimization-methods.block-optimization.log", false);
}
@Override
public void enable() {
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@Override
public void disable() {
HandlerList.unregisterAll(this);
}
@Override
public boolean shouldEnable() {
return VillagerOptimizer.getConfiguration().getBoolean("optimization-methods.block-optimization.enable", false);
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onBlockPlace(BlockPlaceEvent event) {
Block placed = event.getBlock();
if (!blocks_that_disable.contains(placed.getType())) return;
Player player = event.getPlayer();
if (!player.hasPermission(Permissions.Optimize.BLOCK.get())) return;
if (only_while_sneaking && !player.isSneaking()) return;
final Location blockLoc = placed.getLocation();
WrappedVillager closestOptimizableVillager = null;
double closestDistance = Double.MAX_VALUE;
for (Entity entity : blockLoc.getNearbyEntities(search_radius, search_radius, search_radius)) {
if (!entity.getType().equals(EntityType.VILLAGER)) continue;
Villager villager = (Villager) entity;
final Villager.Profession profession = villager.getProfession();
if (profession.equals(Villager.Profession.NONE) || profession.equals(Villager.Profession.NITWIT)) continue;
WrappedVillager wVillager = villagerCache.getOrAdd(villager);
final double distance = entity.getLocation().distance(blockLoc);
if (distance < closestDistance && wVillager.canOptimize(cooldown)) {
closestOptimizableVillager = wVillager;
closestDistance = distance;
}
}
if (closestOptimizableVillager == null) return;
if (closestOptimizableVillager.canOptimize(cooldown) || player.hasPermission(Permissions.Bypass.BLOCK_COOLDOWN.get())) {
VillagerOptimizeEvent optimizeEvent = new VillagerOptimizeEvent(closestOptimizableVillager, OptimizationType.BLOCK, player, event.isAsynchronous());
if (!optimizeEvent.callEvent()) return;
closestOptimizableVillager.setOptimization(optimizeEvent.getOptimizationType());
closestOptimizableVillager.saveOptimizeTime();
if (notify_player) {
final TextReplacementConfig vilProfession = TextReplacementConfig.builder()
.matchLiteral("%vil_profession%")
.replacement(closestOptimizableVillager.villager().getProfession().toString().toLowerCase())
.build();
final TextReplacementConfig placedMaterial = TextReplacementConfig.builder()
.matchLiteral("%blocktype%")
.replacement(placed.getType().toString().toLowerCase())
.build();
VillagerOptimizer.getLang(player.locale()).block_optimize_success.forEach(line -> player.sendMessage(line
.replaceText(vilProfession)
.replaceText(placedMaterial)
));
}
if (log_enabled)
VillagerOptimizer.getLog().info("Villager was optimized by block at "+closestOptimizableVillager.villager().getLocation());
} else {
closestOptimizableVillager.villager().shakeHead();
if (notify_player) {
final TextReplacementConfig timeLeft = TextReplacementConfig.builder()
.matchLiteral("%time%")
.replacement(CommonUtil.formatTime(closestOptimizableVillager.getOptimizeCooldownMillis(cooldown)))
.build();
VillagerOptimizer.getLang(player.locale()).block_on_optimize_cooldown.forEach(line -> player.sendMessage(line.replaceText(timeLeft)));
}
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onBlockBreak(BlockBreakEvent event) {
Block broken = event.getBlock();
if (!blocks_that_disable.contains(broken.getType())) return;
Player player = event.getPlayer();
if (!player.hasPermission(Permissions.Optimize.BLOCK.get())) return;
if (only_while_sneaking && !player.isSneaking()) return;
final Location blockLoc = broken.getLocation();
WrappedVillager closestOptimizedVillager = null;
double closestDistance = Double.MAX_VALUE;
for (Entity entity : blockLoc.getNearbyEntities(search_radius, search_radius, search_radius)) {
if (!entity.getType().equals(EntityType.VILLAGER)) continue;
Villager villager = (Villager) entity;
WrappedVillager wVillager = villagerCache.getOrAdd(villager);
final double distance = entity.getLocation().distance(blockLoc);
if (distance < closestDistance && wVillager.isOptimized()) {
closestOptimizedVillager = wVillager;
closestDistance = distance;
}
}
if (closestOptimizedVillager == null) return;
VillagerUnoptimizeEvent unOptimizeEvent = new VillagerUnoptimizeEvent(closestOptimizedVillager, player, OptimizationType.BLOCK, event.isAsynchronous());
if (!unOptimizeEvent.callEvent()) return;
closestOptimizedVillager.setOptimization(OptimizationType.NONE);
if (notify_player) {
final TextReplacementConfig vilProfession = TextReplacementConfig.builder()
.matchLiteral("%vil_profession%")
.replacement(closestOptimizedVillager.villager().getProfession().toString().toLowerCase())
.build();
final TextReplacementConfig brokenMaterial = TextReplacementConfig.builder()
.matchLiteral("%blocktype%")
.replacement(broken.getType().toString().toLowerCase())
.build();
VillagerOptimizer.getLang(player.locale()).block_unoptimize_success.forEach(line -> player.sendMessage(line
.replaceText(vilProfession)
.replaceText(brokenMaterial)
));
}
if (log_enabled)
VillagerOptimizer.getLog().info("Villager unoptimized because nearby optimization block broken at: "+closestOptimizedVillager.villager().getLocation());
}
}

View File

@ -1,133 +0,0 @@
package me.xginko.villageroptimizer.modules.optimization;
import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.WrappedVillager;
import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.enums.OptimizationType;
import me.xginko.villageroptimizer.enums.Permissions;
import me.xginko.villageroptimizer.events.VillagerOptimizeEvent;
import me.xginko.villageroptimizer.events.VillagerUnoptimizeEvent;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import me.xginko.villageroptimizer.utils.CommonUtil;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextReplacementConfig;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.bukkit.Material;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.entity.Villager;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerInteractEntityEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import java.util.HashSet;
import java.util.List;
public class OptimizeByNametag implements VillagerOptimizerModule, Listener {
private final VillagerCache villagerCache;
private final HashSet<String> nametags = new HashSet<>(4);
private final long cooldown;
private final boolean consume_nametag, notify_player, log_enabled;
public OptimizeByNametag() {
shouldEnable();
this.villagerCache = VillagerOptimizer.getCache();
Config config = VillagerOptimizer.getConfiguration();
config.addComment("optimization-methods.nametag-optimization.enable", """
Enable optimization by naming villagers to one of the names configured below.\s
Nametag optimized villagers will be unoptimized again when they are renamed to something else.""");
this.nametags.addAll(config.getList("optimization-methods.nametag-optimization.names", List.of("Optimize", "DisableAI"),
"Names are case insensitive, capital letters won't matter.").stream().map(String::toLowerCase).toList());
this.consume_nametag = config.getBoolean("optimization-methods.nametag-optimization.nametags-get-consumed", true,
"Enable or disable consumption of the used nametag item.");
this.cooldown = config.getInt("optimization-methods.nametag-optimization.optimize-cooldown-seconds", 600, """
Cooldown in seconds until a villager can be optimized again using a nametag.\s
Here for configuration freedom. Recommended to leave as is to not enable any exploitable behavior.""") * 1000L;
this.notify_player = config.getBoolean("optimization-methods.nametag-optimization.notify-player", true,
"Sends players a message when they successfully optimized a villager.");
this.log_enabled = config.getBoolean("optimization-methods.nametag-optimization.log", false);
}
@Override
public void enable() {
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@Override
public void disable() {
HandlerList.unregisterAll(this);
}
@Override
public boolean shouldEnable() {
return VillagerOptimizer.getConfiguration().getBoolean("optimization-methods.nametag-optimization.enable", true);
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onPlayerInteractEntity(PlayerInteractEntityEvent event) {
if (!event.getRightClicked().getType().equals(EntityType.VILLAGER)) return;
Player player = event.getPlayer();
if (!player.hasPermission(Permissions.Optimize.NAMETAG.get())) return;
ItemStack usedItem = player.getInventory().getItem(event.getHand());
if (!usedItem.getType().equals(Material.NAME_TAG)) return;
ItemMeta meta = usedItem.getItemMeta();
if (!meta.hasDisplayName()) return;
// Get component name first, so we can manually name the villager when canceling the event to avoid item consumption.
Component newVillagerName = meta.displayName();
assert newVillagerName != null; // Legitimate since we checked for hasDisplayName()
final String name = PlainTextComponentSerializer.plainText().serialize(newVillagerName);
Villager villager = (Villager) event.getRightClicked();
WrappedVillager wVillager = villagerCache.getOrAdd(villager);
if (nametags.contains(name.toLowerCase())) {
if (wVillager.canOptimize(cooldown) || player.hasPermission(Permissions.Bypass.NAMETAG_COOLDOWN.get())) {
VillagerOptimizeEvent optimizeEvent = new VillagerOptimizeEvent(wVillager, OptimizationType.NAMETAG, player, event.isAsynchronous());
if (!optimizeEvent.callEvent()) return;
if (!consume_nametag) {
event.setCancelled(true);
villager.customName(newVillagerName);
}
wVillager.setOptimization(optimizeEvent.getOptimizationType());
wVillager.saveOptimizeTime();
if (notify_player)
VillagerOptimizer.getLang(player.locale()).nametag_optimize_success.forEach(player::sendMessage);
if (log_enabled)
VillagerOptimizer.getLog().info(player.getName() + " optimized a villager using nametag: '" + name + "'");
} else {
event.setCancelled(true);
villager.shakeHead();
if (notify_player) {
final TextReplacementConfig timeLeft = TextReplacementConfig.builder()
.matchLiteral("%time%")
.replacement(CommonUtil.formatTime(wVillager.getOptimizeCooldownMillis(cooldown)))
.build();
VillagerOptimizer.getLang(player.locale()).nametag_on_optimize_cooldown.forEach(line -> player.sendMessage(line.replaceText(timeLeft)));
}
}
} else {
if (wVillager.isOptimized()) {
VillagerUnoptimizeEvent unOptimizeEvent = new VillagerUnoptimizeEvent(wVillager, player, OptimizationType.NAMETAG, event.isAsynchronous());
if (!unOptimizeEvent.callEvent()) return;
wVillager.setOptimization(OptimizationType.NONE);
if (notify_player)
VillagerOptimizer.getLang(player.locale()).nametag_unoptimize_success.forEach(player::sendMessage);
if (log_enabled)
VillagerOptimizer.getLog().info(event.getPlayer().getName() + " disabled optimizations for a villager using nametag: '" + name + "'");
}
}
}
}

View File

@ -1,207 +0,0 @@
package me.xginko.villageroptimizer.modules.optimization;
import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.WrappedVillager;
import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.enums.OptimizationType;
import me.xginko.villageroptimizer.enums.Permissions;
import me.xginko.villageroptimizer.events.VillagerOptimizeEvent;
import me.xginko.villageroptimizer.events.VillagerUnoptimizeEvent;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import me.xginko.villageroptimizer.utils.CommonUtil;
import net.kyori.adventure.text.TextReplacementConfig;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.entity.Villager;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent;
public class OptimizeByWorkstation implements VillagerOptimizerModule, Listener {
private final VillagerCache villagerCache;
private final long cooldown;
private final double search_radius;
private final boolean only_while_sneaking, log_enabled, notify_player;
public OptimizeByWorkstation() {
shouldEnable();
this.villagerCache = VillagerOptimizer.getCache();
Config config = VillagerOptimizer.getConfiguration();
config.addComment("optimization-methods.workstation-optimization.enable", """
When enabled, the closest villager near a matching workstation being placed will be optimized.\s
If a nearby matching workstation is broken, the villager will become unoptimized again.""");
this.search_radius = config.getDouble("optimization-methods.workstation-optimization.search-radius-in-blocks", 2.0, """
The radius in blocks a villager can be away from the player when he places a workstation.\s
The closest unoptimized villager to the player will be optimized.""") / 2;
this.cooldown = config.getInt("optimization-methods.workstation-optimization.optimize-cooldown-seconds", 600, """
Cooldown in seconds until a villager can be optimized again using a workstation.\s
Here for configuration freedom. Recommended to leave as is to not enable any exploitable behavior.""") * 1000L;
this.only_while_sneaking = config.getBoolean("optimization-methods.workstation-optimization.only-when-sneaking", true,
"Only optimize/unoptimize by workstation when player is sneaking during place or break");
this.notify_player = config.getBoolean("optimization-methods.workstation-optimization.notify-player", true,
"Sends players a message when they successfully optimized a villager.");
this.log_enabled = config.getBoolean("optimization-methods.workstation-optimization.log", false);
}
@Override
public void enable() {
VillagerOptimizer plugin = VillagerOptimizer.getInstance();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@Override
public void disable() {
HandlerList.unregisterAll(this);
}
@Override
public boolean shouldEnable() {
return VillagerOptimizer.getConfiguration().getBoolean("optimization-methods.workstation-optimization.enable", false);
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onBlockPlace(BlockPlaceEvent event) {
Block placed = event.getBlock();
Villager.Profession workstationProfession = getWorkstationProfession(placed.getType());
if (workstationProfession.equals(Villager.Profession.NONE)) return;
Player player = event.getPlayer();
if (!player.hasPermission(Permissions.Optimize.WORKSTATION.get())) return;
if (only_while_sneaking && !player.isSneaking()) return;
final Location workstationLoc = placed.getLocation();
WrappedVillager closestOptimizableVillager = null;
double closestDistance = Double.MAX_VALUE;
for (Entity entity : workstationLoc.getNearbyEntities(search_radius, search_radius, search_radius)) {
if (!entity.getType().equals(EntityType.VILLAGER)) continue;
Villager villager = (Villager) entity;
if (!villager.getProfession().equals(workstationProfession)) continue;
WrappedVillager wVillager = villagerCache.getOrAdd(villager);
final double distance = entity.getLocation().distance(workstationLoc);
if (distance < closestDistance && wVillager.canOptimize(cooldown)) {
closestOptimizableVillager = wVillager;
closestDistance = distance;
}
}
if (closestOptimizableVillager == null) return;
if (closestOptimizableVillager.canOptimize(cooldown) || player.hasPermission(Permissions.Bypass.WORKSTATION_COOLDOWN.get())) {
VillagerOptimizeEvent optimizeEvent = new VillagerOptimizeEvent(closestOptimizableVillager, OptimizationType.WORKSTATION, player, event.isAsynchronous());
if (!optimizeEvent.callEvent()) return;
closestOptimizableVillager.setOptimization(optimizeEvent.getOptimizationType());
closestOptimizableVillager.saveOptimizeTime();
if (notify_player) {
final TextReplacementConfig vilProfession = TextReplacementConfig.builder()
.matchLiteral("%vil_profession%")
.replacement(closestOptimizableVillager.villager().getProfession().toString().toLowerCase())
.build();
final TextReplacementConfig placedWorkstation = TextReplacementConfig.builder()
.matchLiteral("%workstation%")
.replacement(placed.getType().toString().toLowerCase())
.build();
VillagerOptimizer.getLang(player.locale()).workstation_optimize_success.forEach(line -> player.sendMessage(line
.replaceText(vilProfession)
.replaceText(placedWorkstation)
));
}
if (log_enabled)
VillagerOptimizer.getLog().info(player.getName() + " optimized a villager using workstation: '" + placed.getType().toString().toLowerCase() + "'");
} else {
closestOptimizableVillager.villager().shakeHead();
if (notify_player) {
final TextReplacementConfig timeLeft = TextReplacementConfig.builder()
.matchLiteral("%time%")
.replacement(CommonUtil.formatTime(closestOptimizableVillager.getOptimizeCooldownMillis(cooldown)))
.build();
VillagerOptimizer.getLang(player.locale()).nametag_on_optimize_cooldown.forEach(line -> player.sendMessage(line
.replaceText(timeLeft)
));
}
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onBlockBreak(BlockBreakEvent event) {
Block broken = event.getBlock();
Villager.Profession workstationProfession = getWorkstationProfession(broken.getType());
if (workstationProfession.equals(Villager.Profession.NONE)) return;
Player player = event.getPlayer();
if (!player.hasPermission(Permissions.Optimize.WORKSTATION.get())) return;
if (only_while_sneaking && !player.isSneaking()) return;
final Location workstationLoc = broken.getLocation();
WrappedVillager closestOptimizedVillager = null;
double closestDistance = Double.MAX_VALUE;
for (Entity entity : workstationLoc.getNearbyEntities(search_radius, search_radius, search_radius)) {
if (!entity.getType().equals(EntityType.VILLAGER)) continue;
Villager villager = (Villager) entity;
if (!villager.getProfession().equals(workstationProfession)) continue;
WrappedVillager wVillager = villagerCache.getOrAdd(villager);
final double distance = entity.getLocation().distance(workstationLoc);
if (distance < closestDistance && wVillager.canOptimize(cooldown)) {
closestOptimizedVillager = wVillager;
closestDistance = distance;
}
}
if (closestOptimizedVillager == null) return;
VillagerUnoptimizeEvent unOptimizeEvent = new VillagerUnoptimizeEvent(closestOptimizedVillager, player, OptimizationType.WORKSTATION, event.isAsynchronous());
if (!unOptimizeEvent.callEvent()) return;
closestOptimizedVillager.setOptimization(OptimizationType.NONE);
if (notify_player) {
final TextReplacementConfig vilProfession = TextReplacementConfig.builder()
.matchLiteral("%vil_profession%")
.replacement(closestOptimizedVillager.villager().getProfession().toString().toLowerCase())
.build();
final TextReplacementConfig brokenWorkstation = TextReplacementConfig.builder()
.matchLiteral("%workstation%")
.replacement(broken.getType().toString().toLowerCase())
.build();
VillagerOptimizer.getLang(player.locale()).workstation_unoptimize_success.forEach(line -> player.sendMessage(line
.replaceText(vilProfession)
.replaceText(brokenWorkstation)
));
}
if (log_enabled)
VillagerOptimizer.getLog().info(player.getName() + " unoptimized a villager by breaking workstation: '" + broken.getType().toString().toLowerCase() + "'");
}
private Villager.Profession getWorkstationProfession(final Material workstation) {
return switch (workstation) {
case BARREL -> Villager.Profession.FISHERMAN;
case CARTOGRAPHY_TABLE -> Villager.Profession.CARTOGRAPHER;
case SMOKER -> Villager.Profession.BUTCHER;
case SMITHING_TABLE -> Villager.Profession.TOOLSMITH;
case GRINDSTONE -> Villager.Profession.WEAPONSMITH;
case BLAST_FURNACE -> Villager.Profession.ARMORER;
case CAULDRON -> Villager.Profession.LEATHERWORKER;
case BREWING_STAND -> Villager.Profession.CLERIC;
case COMPOSTER -> Villager.Profession.FARMER;
case FLETCHING_TABLE -> Villager.Profession.FLETCHER;
case LOOM -> Villager.Profession.SHEPHERD;
case LECTERN -> Villager.Profession.LIBRARIAN;
case STONECUTTER -> Villager.Profession.MASON;
default -> Villager.Profession.NONE;
};
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

125
pom.xml
View File

@ -4,20 +4,17 @@
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.0.1</version>
<modules>
<module>VillagerOptimizer-1.20.2</module>
<module>VillagerOptimizer-1.16.5</module>
</modules>
<packaging>pom</packaging>
<version>1.7.0</version>
<packaging>jar</packaging>
<name>VillagerOptimizer</name>
<description>Combat heavy villager lag by letting players optimize their trading halls.</description>
<url>https://github.com/xGinko/VillagerOptimizer</url>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
@ -26,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>
@ -43,17 +40,51 @@
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<finalName>${project.parent.artifactId}-${project.parent.version}--${project.artifactId}</finalName>
<relocations>
<relocation>
<pattern>com.github.benmanes.caffeine</pattern>
<shadedPattern>me.xginko.villageroptimizer.libs.caffeine</shadedPattern>
</relocation>
<relocation>
<pattern>org.bstats</pattern>
<shadedPattern>me.xginko.villageroptimizer.libs.bstats</shadedPattern>
</relocation>
<relocation>
<pattern>net.kyori</pattern>
<shadedPattern>me.xginko.villageroptimizer.libs.kyori</shadedPattern>
</relocation>
<relocation>
<pattern>io.github.thatsmusic99.configurationmaster</pattern>
<shadedPattern>me.xginko.villageroptimizer.libs.configmaster</shadedPattern>
</relocation>
<relocation>
<pattern>org.reflections</pattern>
<shadedPattern>me.xginko.villageroptimizer.libs.reflections</shadedPattern>
</relocation>
<relocation>
<pattern>com.cryptomorin.xseries</pattern>
<shadedPattern>me.xginko.villageroptimizer.libs.xseries</shadedPattern>
</relocation>
<relocation>
<pattern>space.arim.morepaperlib</pattern>
<shadedPattern>me.xginko.villageroptimizer.libs.morepaperlib</shadedPattern>
</relocation>
</relocations>
<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>
<createDependencyReducedPom>false</createDependencyReducedPom>
</configuration>
</execution>
</executions>
@ -69,36 +100,94 @@
<repositories>
<repository>
<id>papermc</id>
<url>https://papermc.io/repo/repository/maven-public/</url>
<id>papermc-repo</id>
<url>https://repo.papermc.io/repository/maven-public/</url>
</repository>
<repository>
<id>sonatype</id>
<url>https://oss.sonatype.org/content/groups/public/</url>
</repository>
<repository>
<id>configmaster-repo</id>
<url>https://ci.pluginwiki.us/plugin/repository/everything/</url>
</repository>
<repository>
<id>morepaperlib-repo</id>
<url>https://mvn-repo.arim.space/lesser-gpl3/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>io.papermc.paper</groupId>
<artifactId>paper-api</artifactId>
<version>1.20.4-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.23.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.reflections</groupId>
<artifactId>reflections</artifactId>
<version>0.10.2</version>
</dependency>
<!-- Adventure API for easier cross-version compatibility -->
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-platform-bukkit</artifactId>
<version>4.3.3</version>
</dependency>
<!-- Adventure MiniMessage for parsing fancy tags in lang files -->
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-minimessage</artifactId>
<version>4.17.0</version>
</dependency>
<!-- Needed to actually display colors in ComponentLogger -->
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-ansi</artifactId>
<version>4.17.0</version>
</dependency>
<!-- Adventure ComponentLogger for colorful slf4j logging -->
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-logger-slf4j</artifactId>
<version>4.17.0</version>
</dependency>
<!-- Bukkit bStats -->
<dependency>
<groupId>org.bstats</groupId>
<artifactId>bstats-bukkit</artifactId>
<version>3.0.2</version>
<scope>compile</scope>
</dependency>
<!-- Enhanced config.yml manager -->
<dependency>
<groupId>com.github.thatsmusic99</groupId>
<artifactId>ConfigurationMaster-API</artifactId>
<version>v2.0.0-rc.1</version>
<scope>compile</scope>
</dependency>
<!-- 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>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

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

View File

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

View File

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

View File

@ -1,20 +1,19 @@
package me.xginko.villageroptimizer.commands.optimizevillagers;
import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.WrappedVillager;
import me.xginko.villageroptimizer.commands.VillagerOptimizerCommand;
import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.enums.OptimizationType;
import me.xginko.villageroptimizer.enums.Permissions;
import me.xginko.villageroptimizer.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;
import net.kyori.adventure.text.format.TextDecoration;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
@ -22,79 +21,90 @@ 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, TabCompleter {
public class OptVillagersRadius extends VillagerOptimizerCommand {
private final List<String> tabCompletes = List.of("5", "10", "25", "50");
private final long cooldown;
private final int max_radius;
public OptVillagersRadius() {
Config config = VillagerOptimizer.getConfiguration();
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 ? tabCompletes : null;
}
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;
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
if (!(sender instanceof Player player)) {
sender.sendMessage(Component.text("This command can only be executed by a player.")
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;
}
if (!sender.hasPermission(Permissions.Commands.OPTIMIZE_RADIUS.get())) {
sender.sendMessage(VillagerOptimizer.getLang(sender).no_permission);
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;
}
try {
int specifiedRadius = Integer.parseInt(args[0]);
final int specifiedRadius = Integer.parseInt(args[0]);
// Turn negative numbers into positive ones
final int safeRadius = (int) Math.sqrt(specifiedRadius * specifiedRadius);
if (specifiedRadius > max_radius) {
if (safeRadius == 0) {
VillagerOptimizer.getLang(player.locale()).command_radius_invalid
.forEach(line -> KyoriUtil.sendMessage(sender, line));
return true;
}
if (safeRadius > max_radius) {
final TextReplacementConfig limit = TextReplacementConfig.builder()
.matchLiteral("%distance%")
.replacement(Integer.toString(max_radius))
.build();
VillagerOptimizer.getLang(player.locale()).command_radius_limit_exceed.forEach(line -> player.sendMessage(line.replaceText(limit)));
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(Permissions.Bypass.COMMAND_COOLDOWN.get());
for (Entity entity : player.getNearbyEntities(specifiedRadius, specifiedRadius, specifiedRadius)) {
for (Entity entity : player.getNearbyEntities(safeRadius, safeRadius, safeRadius)) {
if (!entity.getType().equals(EntityType.VILLAGER)) continue;
Villager villager = (Villager) entity;
Villager.Profession profession = villager.getProfession();
if (profession.equals(Villager.Profession.NITWIT) || profession.equals(Villager.Profession.NONE)) continue;
WrappedVillager wVillager = villagerCache.getOrAdd(villager);
WrappedVillager wVillager = VillagerOptimizer.wrappers().get(villager, WrappedVillager::new);
if (player_has_cooldown_bypass || wVillager.canOptimize(cooldown)) {
VillagerOptimizeEvent optimizeEvent = new VillagerOptimizeEvent(wVillager, OptimizationType.COMMAND, player);
if (optimizeEvent.callEvent()) {
wVillager.setOptimization(optimizeEvent.getOptimizationType());
wVillager.setOptimizationType(optimizeEvent.getOptimizationType());
wVillager.saveOptimizeTime();
successCount++;
}
@ -106,9 +116,10 @@ public class OptVillagersRadius implements VillagerOptimizerCommand, TabComplete
if (successCount <= 0 && failCount <= 0) {
final TextReplacementConfig radius = TextReplacementConfig.builder()
.matchLiteral("%radius%")
.replacement(Integer.toString(specifiedRadius))
.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;
}
@ -119,22 +130,22 @@ public class OptVillagersRadius implements VillagerOptimizerCommand, TabComplete
.build();
final TextReplacementConfig radius = TextReplacementConfig.builder()
.matchLiteral("%radius%")
.replacement(Integer.toString(specifiedRadius))
.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,19 +1,18 @@
package me.xginko.villageroptimizer.commands.unoptimizevillagers;
import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.WrappedVillager;
import me.xginko.villageroptimizer.commands.VillagerOptimizerCommand;
import me.xginko.villageroptimizer.enums.OptimizationType;
import me.xginko.villageroptimizer.enums.Permissions;
import me.xginko.villageroptimizer.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;
import net.kyori.adventure.text.format.TextDecoration;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
@ -21,72 +20,84 @@ 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, TabCompleter {
public class UnOptVillagersRadius extends VillagerOptimizerCommand {
private final List<String> tabCompletes = List.of("5", "10", "25", "50");
private final int max_radius;
public UnOptVillagersRadius() {
this.max_radius = VillagerOptimizer.getConfiguration().getInt("optimization-methods.commands.unoptimizevillagers.max-block-radius", 100);
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 ? tabCompletes : null;
}
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;
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
if (!(sender instanceof Player player)) {
sender.sendMessage(Component.text("This command can only be executed by a player.")
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;
}
if (!sender.hasPermission(Permissions.Commands.UNOPTIMIZE_RADIUS.get())) {
sender.sendMessage(VillagerOptimizer.getLang(sender).no_permission);
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;
}
try {
int specifiedRadius = Integer.parseInt(args[0]);
final int specifiedRadius = Integer.parseInt(args[0]);
// Turn negative numbers into positive ones
final int safeRadius = (int) Math.sqrt(specifiedRadius * specifiedRadius);
if (specifiedRadius > max_radius) {
if (safeRadius == 0) {
VillagerOptimizer.getLang(player.locale()).command_radius_invalid
.forEach(line -> KyoriUtil.sendMessage(sender, line));
return true;
}
if (safeRadius > max_radius) {
final TextReplacementConfig limit = TextReplacementConfig.builder()
.matchLiteral("%distance%")
.replacement(Integer.toString(max_radius))
.build();
VillagerOptimizer.getLang(player.locale()).command_radius_limit_exceed.forEach(line -> player.sendMessage(line.replaceText(limit)));
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(specifiedRadius, specifiedRadius, specifiedRadius)) {
for (Entity entity : player.getNearbyEntities(safeRadius, safeRadius, safeRadius)) {
if (!entity.getType().equals(EntityType.VILLAGER)) continue;
Villager villager = (Villager) entity;
Villager.Profession profession = villager.getProfession();
if (profession.equals(Villager.Profession.NITWIT) || profession.equals(Villager.Profession.NONE)) continue;
WrappedVillager wVillager = villagerCache.getOrAdd(villager);
WrappedVillager wVillager = VillagerOptimizer.wrappers().get(villager, WrappedVillager::new);
if (wVillager.isOptimized()) {
VillagerUnoptimizeEvent unOptimizeEvent = new VillagerUnoptimizeEvent(wVillager, player, OptimizationType.COMMAND);
if (unOptimizeEvent.callEvent()) {
wVillager.setOptimization(OptimizationType.NONE);
wVillager.setOptimizationType(OptimizationType.NONE);
successCount++;
}
}
@ -95,9 +106,10 @@ public class UnOptVillagersRadius implements VillagerOptimizerCommand, TabComple
if (successCount <= 0) {
final TextReplacementConfig radius = TextReplacementConfig.builder()
.matchLiteral("%radius%")
.replacement(Integer.toString(specifiedRadius))
.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%")
@ -105,15 +117,14 @@ public class UnOptVillagersRadius implements VillagerOptimizerCommand, TabComple
.build();
final TextReplacementConfig radius = TextReplacementConfig.builder()
.matchLiteral("%radius%")
.replacement(Integer.toString(specifiedRadius))
.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

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

View File

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

View File

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

View File

@ -0,0 +1,80 @@
package me.xginko.villageroptimizer.commands.villageroptimizer.subcommands;
import io.papermc.paper.plugin.configuration.PluginMeta;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.commands.SubCommand;
import me.xginko.villageroptimizer.struct.enums.Permissions;
import me.xginko.villageroptimizer.utils.KyoriUtil;
import me.xginko.villageroptimizer.utils.Util;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.plugin.PluginDescriptionFile;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collections;
import java.util.List;
public class VersionSubCmd extends SubCommand {
public VersionSubCmd() {
super(
"version",
Component.text("/villageroptimizer version").color(Util.PL_COLOR),
Component.text("Show the plugin version.").color(NamedTextColor.GRAY)
);
}
@Override
public @Nullable List<String> onTabComplete(
@NotNull CommandSender sender, @NotNull Command command, @NotNull String commandLabel, @NotNull String[] args
) {
return Collections.emptyList();
}
@Override
public boolean onCommand(
@NotNull CommandSender sender, @NotNull Command command, @NotNull String commandLabel, @NotNull String[] args
) {
if (!sender.hasPermission(Permissions.Commands.VERSION.get())) {
KyoriUtil.sendMessage(sender, VillagerOptimizer.getLang(sender).no_permission);
return true;
}
String name, version, website, author;
try {
final PluginMeta pluginMeta = VillagerOptimizer.getInstance().getPluginMeta();
name = pluginMeta.getName();
version = pluginMeta.getVersion();
website = pluginMeta.getWebsite();
author = pluginMeta.getAuthors().get(0);
} catch (Throwable versionIncompatible) {
final PluginDescriptionFile pluginYML = VillagerOptimizer.getInstance().getDescription();
name = pluginYML.getName();
version = pluginYML.getVersion();
website = pluginYML.getWebsite();
author = pluginYML.getAuthors().get(0);
}
KyoriUtil.sendMessage(sender, Component.newline()
.append(
Component.text(name + " " + version)
.style(Util.PL_STYLE)
.clickEvent(ClickEvent.openUrl(website))
)
.append(Component.text(" by ").color(NamedTextColor.GRAY))
.append(
Component.text(author)
.color(NamedTextColor.WHITE)
.clickEvent(ClickEvent.openUrl("https://github.com/xGinko"))
)
.append(Component.newline())
);
return true;
}
}

View File

@ -0,0 +1,130 @@
package me.xginko.villageroptimizer.config;
import io.github.thatsmusic99.configurationmaster.api.ConfigFile;
import me.xginko.villageroptimizer.VillagerOptimizer;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.time.Duration;
import java.util.List;
import java.util.Locale;
public class Config {
private final @NotNull ConfigFile config;
public final @NotNull Locale default_lang;
public final @NotNull Duration cache_keep_time;
public final boolean auto_lang, support_other_plugins;
public Config() throws Exception {
// Load config.yml with ConfigMaster
this.config = ConfigFile.loadConfig(new File(VillagerOptimizer.getInstance().getDataFolder(), "config.yml"));
structureConfig();
this.default_lang = Locale.forLanguageTag(
getString("general.default-language", "en_us",
"The default language that will be used if auto-language is false\n" +
"or no matching language file was found.")
.replace("_", "-"));
this.auto_lang = getBoolean("general.auto-language", true,
"If set to true, will display messages based on client language");
this.cache_keep_time = Duration.ofSeconds(Math.max(1, getInt("general.cache-keep-time-seconds", 30,
"The amount of time in seconds a villager will be kept in the plugin's cache.")));
this.support_other_plugins = getBoolean("general.support-avl-villagers", false,
"Enable if you have previously used AntiVillagerLag\n" +
"(https://www.spigotmc.org/resources/antivillagerlag.102949/).\n" +
"Tries to read pre-existing info like optimization state so players\n" +
"don't need to reoptimize their villagers.");
}
public void saveConfig() {
try {
this.config.save();
} catch (Throwable throwable) {
VillagerOptimizer.logger().error("Failed to save config file!", throwable);
}
}
private void structureConfig() {
this.config.addDefault("config-version", 1.00);
this.createTitledSection("General", "general");
this.createTitledSection("Optimization", "optimization-methods");
this.config.addDefault("optimization-methods.commands.unoptimizevillagers", null);
this.config.addComment("optimization-methods.commands",
"If you want to disable commands, negate the following permissions:\n" +
"villageroptimizer.cmd.optimize\n" +
"villageroptimizer.cmd.unoptimize");
this.config.addDefault("optimization-methods.nametag-optimization.enable", true);
this.createTitledSection("Villager Chunk Limit", "villager-chunk-limit");
this.createTitledSection("Gameplay", "gameplay");
this.config.addDefault("gameplay.prevent-trading-with-unoptimized.enable", false);
this.config.addDefault("gameplay.restock-optimized-trades", null);
this.config.addDefault("gameplay.level-optimized-profession", null);
this.config.addDefault("gameplay.unoptimize-on-job-loose.enable", true);
this.config.addDefault("gameplay.villagers-can-be-leashed.enable", true);
this.config.addDefault("gameplay.villagers-spawn-as-adults.enable", false);
this.config.addDefault("gameplay.rename-optimized-villagers.enable", false);
this.config.addDefault("gameplay.prevent-entities-from-targeting-optimized.enable", true);
this.config.addDefault("gameplay.prevent-damage-to-optimized.enable", true);
}
public void createTitledSection(@NotNull String title, @NotNull String path) {
this.config.addSection(title);
this.config.addDefault(path, null);
}
public @NotNull ConfigFile master() {
return this.config;
}
public boolean getBoolean(@NotNull String path, boolean def, @NotNull String comment) {
this.config.addDefault(path, def, comment);
return this.config.getBoolean(path, def);
}
public boolean getBoolean(@NotNull String path, boolean def) {
this.config.addDefault(path, def);
return this.config.getBoolean(path, def);
}
public @NotNull String getString(@NotNull String path, @NotNull String def, @NotNull String comment) {
this.config.addDefault(path, def, comment);
return this.config.getString(path, def);
}
public @NotNull String getString(@NotNull String path, @NotNull String def) {
this.config.addDefault(path, def);
return this.config.getString(path, def);
}
public double getDouble(@NotNull String path, @NotNull Double def, @NotNull String comment) {
this.config.addDefault(path, def, comment);
return this.config.getDouble(path, def);
}
public double getDouble(@NotNull String path, @NotNull Double def) {
this.config.addDefault(path, def);
return this.config.getDouble(path, def);
}
public int getInt(@NotNull String path, int def, @NotNull String comment) {
this.config.addDefault(path, def, comment);
return this.config.getInteger(path, def);
}
public int getInt(@NotNull String path, int def) {
this.config.addDefault(path, def);
return this.config.getInteger(path, def);
}
public @NotNull <T> List<T> getList(@NotNull String path, @NotNull List<T> def, @NotNull String comment) {
this.config.addDefault(path, def, comment);
return this.config.getList(path);
}
public @NotNull <T> List<T> getList(@NotNull String path, @NotNull List<T> def) {
this.config.addDefault(path, def);
return this.config.getList(path);
}
}

View File

@ -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().severe("Unable to create lang directory.");
// Check if the file already exists and save the one from the plugins resources folder if it does not
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/" + locale + ".yml", false);
// Finally load the lang file with configmaster
// Finally, load the lang file with configmaster
this.lang = ConfigFile.loadConfig(langYML);
// General
this.no_permission = getTranslation("messages.no-permission",
"<red>You don't have permission to use this command.");
this.trades_restocked = getListTranslation("messages.trades-restocked",
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 {
lang.save();
} catch (Exception e) {
VillagerOptimizer.getLog().severe("Failed to save language file: "+ lang.getFile().getName() +" - " + e.getLocalizedMessage());
this.lang.save();
} catch (Throwable throwable) {
VillagerOptimizer.logger().error("Failed to save language file: " + langYML.getName(), throwable);
}
}
public @NotNull Component getTranslation(@NotNull String path, @NotNull String defaultTranslation) {
lang.addDefault(path, defaultTranslation);
return MiniMessage.miniMessage().deserialize(lang.getString(path, defaultTranslation));
this.lang.addDefault(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) {
lang.addDefault(path, defaultTranslation, comment);
return MiniMessage.miniMessage().deserialize(lang.getString(path, defaultTranslation));
}
public @NotNull List<Component> getListTranslation(@NotNull String path, @NotNull List<String> defaultTranslation) {
lang.addDefault(path, defaultTranslation);
return lang.getStringList(path).stream().map(MiniMessage.miniMessage()::deserialize).toList();
}
public @NotNull List<Component> getListTranslation(@NotNull String path, @NotNull List<String> defaultTranslation, @NotNull String comment) {
lang.addDefault(path, defaultTranslation, comment);
return lang.getStringList(path).stream().map(MiniMessage.miniMessage()::deserialize).toList();
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,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;
@ -13,28 +13,37 @@ public class VillagerOptimizeEvent extends Event implements Cancellable {
private static final @NotNull HandlerList handlers = new HandlerList();
private final @NotNull WrappedVillager wrappedVillager;
private @NotNull OptimizationType type;
private @NotNull OptimizationType optimizationType;
private final @Nullable Player whoOptimised;
private boolean isCancelled = false;
public VillagerOptimizeEvent(@NotNull WrappedVillager wrappedVillager, @NotNull OptimizationType type, @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;
if (type.equals(OptimizationType.NONE)) {
if (optimizationType.equals(OptimizationType.NONE)) {
throw new IllegalArgumentException("OptimizationType can't be NONE.");
} else {
this.type = type;
this.optimizationType = optimizationType;
}
}
public VillagerOptimizeEvent(@NotNull WrappedVillager wrappedVillager, @NotNull OptimizationType type, @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 (type.equals(OptimizationType.NONE)) {
if (optimizationType.equals(OptimizationType.NONE)) {
throw new IllegalArgumentException("OptimizationType can't be NONE.");
} else {
this.type = type;
this.optimizationType = optimizationType;
}
}
@ -43,14 +52,14 @@ public class VillagerOptimizeEvent extends Event implements Cancellable {
}
public @NotNull OptimizationType getOptimizationType() {
return type;
return optimizationType;
}
public void setOptimizationType(@NotNull OptimizationType type) throws IllegalArgumentException {
if (type.equals(OptimizationType.NONE)) {
public void setOptimizationType(@NotNull OptimizationType optimizationType) throws IllegalArgumentException {
if (optimizationType.equals(OptimizationType.NONE)) {
throw new IllegalArgumentException("OptimizationType can't be NONE.");
} else {
this.type = type;
this.optimizationType = optimizationType;
}
}

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

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

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

View File

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

View File

@ -0,0 +1,83 @@
package me.xginko.villageroptimizer.modules.gameplay;
import com.cryptomorin.xseries.XEntityType;
import com.cryptomorin.xseries.XMaterial;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
import me.xginko.villageroptimizer.utils.LocationUtil;
import me.xginko.villageroptimizer.wrapper.WrappedVillager;
import org.bukkit.GameMode;
import org.bukkit.entity.Player;
import org.bukkit.entity.Villager;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.PlayerLeashEntityEvent;
import org.bukkit.event.player.PlayerInteractEntityEvent;
import org.bukkit.inventory.ItemStack;
public class EnableLeashingVillagers extends VillagerOptimizerModule implements Listener {
private final boolean only_optimized, log_enabled;
public EnableLeashingVillagers() {
super("gameplay.villagers-can-be-leashed");
config.master().addComment(configPath + ".enable",
"Enable leashing of villagers, enabling players to easily move villagers to where they want them to be.");
this.only_optimized = config.getBoolean(configPath + ".only-optimized", false,
"If set to true, only optimized villagers can be leashed.");
this.log_enabled = config.getBoolean(configPath + ".log", false);
}
@Override
public void enable() {
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@Override
public void disable() {
HandlerList.unregisterAll(this);
}
@Override
public boolean shouldEnable() {
return config.getBoolean(configPath + ".enable", false);
}
@SuppressWarnings("deprecation")
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onLeash(PlayerInteractEntityEvent event) {
if (event.getRightClicked().getType() != XEntityType.VILLAGER.get()) return;
final Player player = event.getPlayer();
final ItemStack handItem = player.getInventory().getItem(event.getHand());
if (handItem == null || handItem.getType() != XMaterial.LEAD.parseMaterial()) return;
final Villager villager = (Villager) event.getRightClicked();
if (villager.isLeashed()) return;
if (only_optimized && !wrapperCache.get(villager, WrappedVillager::new).isOptimized()) return;
event.setCancelled(true); // Cancel the event, so we don't interact with the villager
// Call event for compatibility with other plugins, constructing non deprecated if available
PlayerLeashEntityEvent leashEvent;
try {
leashEvent = new PlayerLeashEntityEvent(villager, player, player, event.getHand());
} catch (Throwable versionIncompatible) {
leashEvent = new PlayerLeashEntityEvent(villager, player, player);
}
// If canceled by any plugin, do nothing
if (!leashEvent.callEvent()) return;
scheduling.entitySpecificScheduler(villager).run(leash -> {
// Legitimate to not use entities from the event object since they are final in PlayerLeashEntityEvent
if (!villager.setLeashHolder(player)) return;
if (player.getGameMode().equals(GameMode.SURVIVAL))
handItem.subtract(1); // Manually consume for survival players
if (log_enabled) {
info(player.getName() + " leashed a villager at " + LocationUtil.toString(villager.getLocation()));
}
}, null);
}
}

View File

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

View File

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

View File

@ -1,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().equals(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

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

View File

@ -1,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().equals(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().equals(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().equals(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

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

View File

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

View File

@ -0,0 +1,43 @@
package me.xginko.villageroptimizer.modules.gameplay;
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 extends VillagerOptimizerModule implements Listener {
public UnoptimizeOnJobLoose() {
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() {
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@Override
public void disable() {
HandlerList.unregisterAll(this);
}
@Override
public boolean shouldEnable() {
return config.getBoolean(configPath + ".enable", true);
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onJobReset(VillagerCareerChangeEvent event) {
if (event.getReason() != VillagerCareerChangeEvent.ChangeReason.LOSING_JOB) return;
final WrappedVillager wrapped = wrapperCache.get(event.getEntity(), WrappedVillager::new);
if (wrapped.isOptimized()) {
wrapped.setOptimizationType(OptimizationType.NONE);
}
}
}

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

@ -0,0 +1,218 @@
package me.xginko.villageroptimizer.modules.optimization;
import com.cryptomorin.xseries.XMaterial;
import me.xginko.villageroptimizer.VillagerOptimizer;
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.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.Player;
import org.bukkit.entity.Villager;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import java.time.Duration;
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 extends VillagerOptimizerModule implements Listener {
private final Set<Material> blocks_that_disable;
private final long cooldown_millis;
private final double search_radius;
private final boolean only_while_sneaking, notify_player, log_enabled;
public OptimizeByBlock() {
super("optimization-methods.block-optimization");
config.master().addComment(configPath + ".enable",
"When enabled, the closest villager standing near a configured block being placed will be optimized.\n" +
"If a configured block is broken nearby, the closest villager will become unoptimized again.");
List<String> defaults = Stream.of(XMaterial.LAPIS_BLOCK, XMaterial.GLOWSTONE, XMaterial.IRON_BLOCK)
.filter(XMaterial::isSupported)
.map(Enum::name)
.collect(Collectors.toList());
this.blocks_that_disable = config.getList(configPath + ".materials", defaults,
"Values here need to be valid bukkit Material enums for your server version.")
.stream()
.map(configuredMaterial -> {
try {
return Material.valueOf(configuredMaterial);
} catch (IllegalArgumentException e) {
warn("Material '" + configuredMaterial + "' not recognized. Please use correct Material enums from: " +
"https://jd.papermc.io/paper/1.20/org/bukkit/Material.html");
return null;
}
})
.filter(Objects::nonNull)
.collect(Collectors.toCollection(() -> EnumSet.noneOf(Material.class)));
this.cooldown_millis = TimeUnit.SECONDS.toMillis(
config.getInt(configPath + ".optimize-cooldown-seconds", 600,
"Cooldown in seconds until a villager can be optimized again by using specific blocks.\n" +
"Here for configuration freedom. Recommended to leave as is to not enable any exploitable behavior."));
this.search_radius = config.getDouble(configPath + ".search-radius-in-blocks", 2.0,
"The radius in blocks a villager can be away from the player when he places an optimize block.\n" +
"The closest unoptimized villager to the player will be optimized.") / 2;
this.only_while_sneaking = config.getBoolean(configPath + ".only-when-sneaking", true,
"Only optimize/unoptimize by block when player is sneaking during place or break.");
this.notify_player = config.getBoolean(configPath + ".notify-player", true,
"Sends players a message when they successfully optimized or unoptimized a villager.");
this.log_enabled = config.getBoolean(configPath + ".log", false);
}
@Override
public void enable() {
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@Override
public void disable() {
HandlerList.unregisterAll(this);
}
@Override
public boolean shouldEnable() {
return config.getBoolean(configPath + ".enable", false);
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onBlockPlace(BlockPlaceEvent event) {
final Block placed = event.getBlock();
if (!blocks_that_disable.contains(placed.getType())) return;
final Player player = event.getPlayer();
if (!player.hasPermission(Permissions.Optimize.BLOCK.get())) return;
if (only_while_sneaking && !player.isSneaking()) return;
final Location blockLoc = placed.getLocation().toCenterLocation();
WrappedVillager closestOptimizableVillager = null;
double closestDistance = Double.MAX_VALUE;
for (Villager villager : blockLoc.getNearbyEntitiesByType(Villager.class, search_radius)) {
final Villager.Profession profession = villager.getProfession();
if (profession.equals(Villager.Profession.NONE) || profession.equals(Villager.Profession.NITWIT)) continue;
final double distance = LocationUtil.relDistance3DSquared(villager.getLocation(), blockLoc);
if (distance >= closestDistance) continue;
final WrappedVillager wVillager = wrapperCache.get(villager, WrappedVillager::new);
if (wVillager.canOptimize(cooldown_millis)) {
closestOptimizableVillager = wVillager;
closestDistance = distance;
}
}
if (closestOptimizableVillager == null) return;
if (closestOptimizableVillager.canOptimize(cooldown_millis) || player.hasPermission(Permissions.Bypass.BLOCK_COOLDOWN.get())) {
VillagerOptimizeEvent optimizeEvent = new VillagerOptimizeEvent(
closestOptimizableVillager,
OptimizationType.BLOCK,
player,
event.isAsynchronous()
);
if (!optimizeEvent.callEvent()) return;
closestOptimizableVillager.setOptimizationType(optimizeEvent.getOptimizationType());
closestOptimizableVillager.saveOptimizeTime();
if (notify_player) {
final TextReplacementConfig vilProfession = TextReplacementConfig.builder()
.matchLiteral("%vil_profession%")
.replacement(Util.toNiceString(closestOptimizableVillager.villager.getProfession()))
.build();
final TextReplacementConfig placedMaterial = TextReplacementConfig.builder()
.matchLiteral("%blocktype%")
.replacement(Util.toNiceString(placed.getType()))
.build();
VillagerOptimizer.getLang(player.locale()).block_optimize_success
.forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(vilProfession).replaceText(placedMaterial)));
}
if (log_enabled) {
info(player.getName() + " optimized villager at " +
LocationUtil.toString(closestOptimizableVillager.villager.getLocation()));
}
} else {
closestOptimizableVillager.sayNo();
if (notify_player) {
final TextReplacementConfig timeLeft = TextReplacementConfig.builder()
.matchLiteral("%time%")
.replacement(Util.formatDuration(Duration.ofMillis(closestOptimizableVillager.getOptimizeCooldownMillis(cooldown_millis))))
.build();
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) {
final Block broken = event.getBlock();
if (!blocks_that_disable.contains(broken.getType())) return;
final Player player = event.getPlayer();
if (!player.hasPermission(Permissions.Optimize.BLOCK.get())) return;
if (only_while_sneaking && !player.isSneaking()) return;
final Location blockLoc = broken.getLocation().toCenterLocation();
WrappedVillager closestOptimizedVillager = null;
double closestDistance = Double.MAX_VALUE;
for (Villager villager : blockLoc.getNearbyEntitiesByType(Villager.class, search_radius)) {
final double distance = LocationUtil.relDistance3DSquared(villager.getLocation(), blockLoc);
if (distance >= closestDistance) continue;
final WrappedVillager wVillager = wrapperCache.get(villager, WrappedVillager::new);
if (wVillager.isOptimized()) {
closestOptimizedVillager = wVillager;
closestDistance = distance;
}
}
if (closestOptimizedVillager == null) return;
VillagerUnoptimizeEvent unOptimizeEvent = new VillagerUnoptimizeEvent(
closestOptimizedVillager,
player,
OptimizationType.BLOCK,
event.isAsynchronous()
);
if (!unOptimizeEvent.callEvent()) return;
closestOptimizedVillager.setOptimizationType(OptimizationType.NONE);
if (notify_player) {
final TextReplacementConfig vilProfession = TextReplacementConfig.builder()
.matchLiteral("%vil_profession%")
.replacement(Util.toNiceString(closestOptimizedVillager.villager.getProfession()))
.build();
final TextReplacementConfig brokenMaterial = TextReplacementConfig.builder()
.matchLiteral("%blocktype%")
.replacement(Util.toNiceString(broken.getType()))
.build();
VillagerOptimizer.getLang(player.locale()).block_unoptimize_success
.forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(vilProfession).replaceText(brokenMaterial)));
}
if (log_enabled) {
info(player.getName() + " unoptimized villager using " + Util.toNiceString(broken.getType()) +
LocationUtil.toString(closestOptimizedVillager.villager.getLocation()));
}
}
}

View File

@ -0,0 +1,158 @@
package me.xginko.villageroptimizer.modules.optimization;
import com.cryptomorin.xseries.XEntityType;
import com.cryptomorin.xseries.XMaterial;
import me.xginko.villageroptimizer.VillagerOptimizer;
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.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.ChatColor;
import org.bukkit.GameMode;
import org.bukkit.entity.Player;
import org.bukkit.entity.Villager;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerInteractEntityEvent;
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.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
public class OptimizeByNametag extends VillagerOptimizerModule implements Listener {
private final Set<String> nametags;
private final long cooldown;
private final boolean consume_nametag, notify_player, log_enabled;
public OptimizeByNametag() {
super("optimization-methods.nametag-optimization");
config.master().addComment(configPath + ".enable",
"Enable optimization by naming villagers to one of the names configured below.\n" +
"Nametag optimized villagers will be unoptimized again when they are renamed to something else.");
this.nametags = config.getList(configPath + ".names", Arrays.asList("Optimize", "DisableAI"),
"Names are case insensitive, capital letters won't matter.")
.stream().map(String::toLowerCase).collect(Collectors.toCollection(HashSet::new));
this.consume_nametag = config.getBoolean(configPath + ".nametags-get-consumed", true,
"Enable or disable consumption of the used nametag item.");
this.cooldown = TimeUnit.SECONDS.toMillis(
config.getInt(configPath + ".optimize-cooldown-seconds", 600,
"Cooldown in seconds until a villager can be optimized again using a nametag.\n" +
"Here for configuration freedom. Recommended to leave as is to not enable any exploitable behavior."));
this.notify_player = config.getBoolean(configPath + ".notify-player", true,
"Sends players a message when they successfully optimized a villager.");
this.log_enabled = config.getBoolean(configPath + ".log", false);
}
@Override
public void enable() {
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@Override
public void disable() {
HandlerList.unregisterAll(this);
}
@Override
public boolean shouldEnable() {
return config.getBoolean(configPath + ".enable", true);
}
@SuppressWarnings("deprecation")
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onPlayerInteractEntity(PlayerInteractEntityEvent event) {
if (event.getRightClicked().getType() != XEntityType.VILLAGER.get()) return;
final Player player = event.getPlayer();
if (!player.hasPermission(Permissions.Optimize.NAMETAG.get())) return;
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;
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 (!optimizeEvent.callEvent()) return;
wrapped.setOptimizationType(optimizeEvent.getOptimizationType());
wrapped.saveOptimizeTime();
if (!consume_nametag && player.getGameMode() == GameMode.SURVIVAL) {
player.getInventory().addItem(usedItem.asOne());
}
if (notify_player) {
VillagerOptimizer.getLang(player.locale()).nametag_optimize_success
.forEach(line -> KyoriUtil.sendMessage(player, line));
}
if (log_enabled) {
info(player.getName() + " optimized villager using nametag '" + nameTagPlainText + "' at " +
LocationUtil.toString(wrapped.villager.getLocation()));
}
} else {
event.setCancelled(true);
wrapped.sayNo();
if (notify_player) {
final TextReplacementConfig timeLeft = TextReplacementConfig.builder()
.matchLiteral("%time%")
.replacement(Util.formatDuration(Duration.ofMillis(wrapped.getOptimizeCooldownMillis(cooldown))))
.build();
VillagerOptimizer.getLang(player.locale()).nametag_on_optimize_cooldown
.forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(timeLeft)));
}
}
} else {
if (wrapped.isOptimized()) {
VillagerUnoptimizeEvent unOptimizeEvent = new VillagerUnoptimizeEvent(
wrapped,
player,
OptimizationType.NAMETAG,
event.isAsynchronous()
);
if (!unOptimizeEvent.callEvent()) return;
wrapped.setOptimizationType(OptimizationType.NONE);
if (!consume_nametag && player.getGameMode() == GameMode.SURVIVAL) {
player.getInventory().addItem(usedItem.asOne());
}
if (notify_player) {
VillagerOptimizer.getLang(player.locale()).nametag_unoptimize_success
.forEach(line -> KyoriUtil.sendMessage(player, line));
}
if (log_enabled) {
info(player.getName() + " unoptimized villager using nametag '" + nameTagPlainText + "' at " +
LocationUtil.toString(wrapped.villager.getLocation()));
}
}
}
}
}

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