Compare commits

...

48 Commits

Author SHA1 Message Date
PretzelJohn 7e8baf8a6c Merge remote-tracking branch 'origin/master' 2023-06-29 15:06:02 -04:00
PretzelJohn dfb4238c42 add 1.20.1 support 2023-06-29 15:05:46 -04:00
PretzelJohn 3522d5099b
Merge pull request #8 from HarvelsX/master
Fix IndexOutOfBoundsException: use Iterables utils for get or default;
2023-06-08 13:17:29 -04:00
HarvelsX 071129fe16
Fix IndexOutOfBoundsException: use Iterables utils for get or default; 2023-05-11 12:31:28 +03:00
PretzelJohn e804329777
Merge pull request #7 from HarvelsX/master
Fix replacing items in NBT data:
2023-05-10 13:02:31 -04:00
HarvelsX 1fe04c1b5e
Fix replacing items in NBT data:
It is not enough to change the material and quantity, apparently there are preset NBT data;
2023-05-09 03:04:45 +03:00
PretzelJohn 5f279ea12e
Merge pull request #6 from mastercake10/master
make config option `IgnoreHeldItems` work by removing Enum#ordinal
2023-04-19 14:39:13 -04:00
mastercake10 411a0abc66 make config option `IgnoreHeldItems` work by removing Enum#ordinal 2023-04-19 20:30:18 +02:00
PretzelJohn 542eb20e8a Version 1.6.3:
* Slightly better default config
2023-03-20 21:19:18 -04:00
PretzelJohn 71fc8bd4f2 Version 1.6.3:
* Added left and right matching for trade overrides
2023-03-20 19:34:01 -04:00
PretzelJohn 03f8d1b84c Version 1.6.3:
* Fixed case sensitivity for disabled items and professions in config
* Updated version
2023-03-20 19:02:43 -04:00
PretzelJohn b8b14941df Version 1.6.2:
* Updated Item NBT API to 2.11.2
* Updated SQLite to 3.40.1.0
2023-03-16 19:14:42 -04:00
PretzelJohn 75df051d08 Version 1.6.1:
* Added disabled professions feature
* Added disabled trades feature (completely removes certain trades)
2023-03-16 19:12:04 -04:00
PretzelJohn 58d1b24ff0 Version 1.6.0:
* Added compatibility with ODailyQuests plugin
* Fixed potential bug on 1.14 with equipment slot
* Updated to NBT-API 2.11.1
2023-02-18 11:09:23 -05:00
PretzelJohn f13fac70fc Version 1.5.9:
* Added 1.19.3 support
* Updated dependencies
2023-01-01 18:56:45 -05:00
PretzelJohn ccaefbbac7 Version 1.5.8:
* Added config option to disable VTL (leaving vanilla trading) in certain worlds
2022-07-03 15:15:14 -04:00
PretzelJohn 65a8bdf267 Version 1.5.7:
* Add support for 1.19 items (updated NBT-API)
* Converted NBT-API dependency from copied code to shaded via maven
2022-06-14 22:04:57 -04:00
PretzelJohn 61bcea37b9 Version 1.5.6:
* Added "ghast_spawn_egg" to IgnoreHeldItems list in config.yml (supports SafariNet by default) - Thanks to Turjoy9
* Added "name_tag" to IgnoreHeldItems list in config.yml (fixes nametagging villagers with professions) - Thanks to Turjoy9
* Added a few more comments in VillagerTradeLimiter.java
2022-05-21 08:34:23 -04:00
PretzelJohn 8a46c9ab1c
Merge pull request #3 from Turjoy9/master
Disabling nametag by default so that name can be applied to villagers with professions.
2022-05-21 02:21:12 -04:00
Turjoy9 18e47efef4
Update config.yml
Disabling nametag by default so that name can be applied to villagers with professions.
2022-05-21 12:10:46 +06:00
PretzelJohn ec76d44e0d Version 1.5.6-pre1:
* Updated version number to 1.5.6
2022-05-02 10:12:10 -04:00
PretzelJohn 3513e97253
Merge pull request #2 from Turjoy9/master
Update config.yml
2022-05-02 10:09:53 -04:00
Turjoy9 b06f9c9871
Update config.yml 2022-05-02 19:57:54 +06:00
Turjoy9 4097d9d8f3
Update config.yml 2022-05-02 19:48:37 +06:00
Turjoy9 af2cd73737
Update config.yml
Adding support for Safarinet plugin by default.
2022-05-02 19:38:51 +06:00
PretzelJohn d328a575aa Version 1.5.5:
* Fixed MaxDemand bug when you buy something around 1500+ times back-to-back
2022-05-01 08:59:38 -04:00
PretzelJohn 2d97903db6 Version 1.5.5:
* Hopefully fixed MaxDemand bug when you buy something around 1500+ times back-to-back
2022-04-30 01:57:06 -04:00
PretzelJohn 030974f065 Version 1.5.4:
* Fixed IgnoreHeldItems again
2022-04-23 20:53:52 -04:00
PretzelJohn a3995b6adb Version 1.5.4:
* Fixed IgnoreHeldItems typo
2022-04-23 18:44:01 -04:00
PretzelJohn 72e2423741 Version 1.5.3-pre2:
* Add support for other plugins that cancel the PlayerInteractEntityEvent. No longer runs the interaction code when the event has already been cancelled.
* Add support for other plugins that use special items. For example, SafariNet captures villagers when a player uses a custom spawn egg on them. No longer runs the interaction code when the player is holding one of the items listed in config.yml
2022-04-10 21:32:26 -04:00
PretzelJohn 40c31a13c7 Version 1.5.3-pre1:
* Prevent moving items to/from invsee gui
* Barrier item in invsee gui now closes gui when clicked
2022-03-11 08:56:18 -05:00
PretzelJohn ea98a911d3 Version 1.5.2:
* Added support for 1.18.2 by updating NBT-API internal dependency
2022-03-05 09:01:42 -05:00
PretzelJohn f4a8ecd21c Version 1.5.1:
* Fixed an NBT error and missing items when config.yml had uppercase material names

To-do:
* Add enchantments, data, etc. to ingredients and result
* Add config editor GUI in-game
2022-01-22 05:05:06 -05:00
PretzelJohn dd49756014 Version 1.5.1:
* Fixed an NPE that occurred when a villager has no offers

To-do:
* Add enchantments, data, etc. to ingredients and result
* Add config editor GUI in-game
2022-01-21 21:08:24 -05:00
PretzelJohn afacf4adc8 Version 1.5.1:
* Add toggleable Shopkeepers support
* Made Citizens support toggleable

To-do:
* Add enchantments, data, etc. to ingredients and result
* Add config editor GUI in-game
2022-01-21 20:56:44 -05:00
PretzelJohn 5cc49188a7
Update README.md 2022-01-15 17:09:59 -05:00
PretzelJohn 8720e840e2 Version 1.5.0-pre4:
* Add per-villager restock cooldowns

To-do:
* Add enchantments, data, etc. to ingredients and result
* Add config editor GUI in-game
2022-01-15 12:10:28 -05:00
PretzelJohn 3aba0ff1c7 Version 1.5.0-pre3:
* Added global setting for per-player cooldowns

To-do:
* Add per-villager restock cooldowns
* Add enchantments, data, etc. to ingredients and result
* Add config editor GUI in-game
2022-01-15 09:46:56 -05:00
PretzelJohn 0e63df608f * Update readme to document new features 2022-01-11 04:53:06 -05:00
PretzelJohn e708187b33 Version 1.5.0-pre2:
* Add per-player restock cooldowns
* Finished commenting code

TO-DO:
* Add enchantment, custom model data, names, and lores to ingredient & result settings?
* GUI Editor
2022-01-11 04:22:52 -05:00
PretzelJohn 5110befd30 Version 1.5.0-pre1:
* Added /vtl see <player> command to see the trade prices of another player
* Added /vtl invsee command to see the inventory of a villager
* Added customizable help, success, error messages
* Added result slot options, so people can change the currency and amount of the resulting item from a trade
* Massive code rewrite for better sustainability and reliability
* Fixed villager inventory dupe bug: food in a villager's inventory would multiply 16+ times each time a player traded with the villager
* Smoothed out hero of the village, so the player never loses their original potion effect when trading with villagers

TO-DO:
* Add long-term restock cooldown
* Add enchantment, custom model data, names, and lores to ingredient & result settings?
* GUI Editor
2022-01-07 03:57:59 -05:00
PretzelJohn 446e677336 Version 1.4.4:
* Added feature to increase discounts by setting MaxDiscount to a number greater than 1.0
2021-12-23 02:17:49 -05:00
PretzelJohn b950195383 Version 1.4.2:
* Fix support for 1.16.5 and below

Version 1.4.3:
* Add global MaxUses setting (thanks to @Kid on Discord)
2021-12-21 01:56:02 -05:00
PretzelJohn c380de9800 Version 1.4.1:
* Fix disabled trade glitch
2021-12-16 16:33:21 -05:00
PretzelJohn 98be139384 Version 1.4.0:
* Added 1.18 and 1.18.1 support
* Migrated to NBT-API (thanks Jowcey!)
* Added MaxUses setting (thanks Jowcey!)
* Added fixed price and currency settings for both ingredients (Item1 and Item2) of a trade (thanks Jowcey!)
* Fixed config updater (thanks tchristofferson!)
* Updated readme
2021-12-15 15:30:01 -05:00
PretzelJohn 1d680d581d
Merge pull request #1 from Jowcey/master
New Features + 1.18 Update
2021-12-13 15:07:35 -05:00
Jowcey 47982a6e28 New Features + 1.18 Update 2021-12-13 16:37:00 +00:00
PretzelJohn 589efcbfdb Version 1.3.0:
* Added per-world disabled trading
* Added per-item disabled trading
2021-09-16 01:35:21 -04:00
37 changed files with 2423 additions and 719 deletions

3
.gitignore vendored
View File

@ -7,4 +7,5 @@ bin
.classpath
*.iml
/.idea/
/.git/
/.git/
/out/

155
README.md
View File

@ -2,51 +2,9 @@
<h6>by PretzelJohn</h6>
<h2>Description:</h2>
<p>This Minecraft plugin limits the villager trade deals that players can get when they cure a zombie villager.</p>
<br>
<h2>Commands:</h2>
<table>
<tr>
<th>Command</th>
<th>Alias</th>
<th>Description</th>
</tr>
<tr>
<td><code>/villagertradelimiter</code></td>
<td><code>/vtl</code></td>
<td>shows a help message</td>
</tr>
<tr>
<td><code>/villagertradelimiter reload</code></td>
<td><code>/vtl reload</code></td>
<td>reloads config.yml</td>
</tr>
</table><br>
<h2>Permissions:</h2>
<table>
<tr>
<th>Permission</th>
<th>Description</th>
<th>Default User(s)</th>
</tr>
<tr>
<td>villagertradelimiter.*</td>
<td>Allows players to use <code>/vtl</code> and <code>/vtl reload</code></td>
<td>OP</td>
</tr>
<tr>
<td>villagertradelimiter.use</td>
<td>Allows players to use <code>/vtl</code></td>
<td>OP</td>
</tr>
<tr>
<td>villagertradelimiter.reload</td>
<td>Allows players to reload config.yml and messages.yml</td>
<td>OP</td>
</tr>
</table><br>
<p>This Minecraft plugin limits the villager trade deals that players can get.<br/>Supports Spigot, Paper, and Purpur servers from 1.14.1 to the current version.<br/>Click <a href="https://www.spigotmc.org/resources/87210/">here</a> to see this plugin on Spigot.</p>
<p>Some information has moved to the <a href="https://github.com/PretzelJohn/VillagerTradeLimiter/wiki">Wiki</a>!</p>
<br/>
<h2>Config:</h2>
<ul>
@ -61,13 +19,68 @@
<td><code>bStats:</code></td>
<td>This helps me keep track of what server versions are being used. Please leave this set to true.</td>
</tr>
<tr>
<td><code>database.mysql:</code></td>
<td>Whether to use MySQL for the database (true) or SQLite (false)</td>
</tr>
<tr>
<td><code>database.host:</code></td>
<td>The IP address or domain name of your MySQL server. If the MySQL database is on the same server as your Minecraft server, leave this as <code>127.0.0.1</code></td>
</tr>
<tr>
<td><code>database.port:</code></td>
<td>The port number of your MySQL server. Usually <code>3306</code>.</td>
</tr>
<tr>
<td><code>database.database:</code></td>
<td>The name of your MySQL database, or schema. You must create a database (schema) before using this plugin!</td>
</tr>
<tr>
<td><code>database.username:</code></td>
<td>The username to access your MySQL database.</td>
</tr>
<tr>
<td><code>database.password:</code></td>
<td>The password to access your MySQL database.</td>
</tr>
<tr>
<td><code>database.encoding:</code></td>
<td>If your MySQL database uses an encoding other than <code>utf8</code>, change this.</td>
</tr>
<tr>
<td><code>database.useSSL:</code></td>
<td>If your MySQL database can use SSL connections, set this to <code>true</code>!</td>
</tr>
<tr>
<td><code>IgnoreCitizens:</code></td>
<td>Whether to ignore Citizens NPCs from the Citizens plugin. If set to true, Citizens NPCs won't be affected by this plugin.</td>
</tr>
<tr>
<td><code>IgnoreShopkeepers:</code></td>
<td>Whether to ignore Shopkeepers NPCs from the Shopkeepers plugin. If set to true, Shopkeepers NPCs won't be affected by this plugin.</td>
</tr>
<tr>
<td><code>IgnoreHeldItems:</code></td>
<td>A list of item types where, when the player interacts with a villager while holding one of these items, VTL doesn't affect the interaction. This is used for compatibility with other plugins, like the custom spawn eggs in <a href="https://www.spigotmc.org/resources/%E2%9C%85-safarinet-premium-mob-catcher-plugin.9732/">SafariNet</a>.<br><b>Options:</b>
<ul>
<li>Add material names for special items used by other plugins</li>
<li>Remove all list items and set to [] to disable this feature</li>
</ul>
</td>
</tr>
<tr>
<td><code>DisableTrading:</code></td>
<td>Set this to true if you want to completely disable ALL villager trading.</td>
<td>Whether to disable all villager trading for all worlds, some worlds, or no worlds.<br/><strong>Options:</strong>
<ul>
<li>Add world names for worlds that you want to completely disable ALL villager trading.</li>
<li>Set to true to disable trading in all worlds.</li>
<li>Set to false or [] to disable this feature.</li>
</ul>
</td>
</tr>
<tr>
<td><code>MaxHeroLevel:</code></td>
<td>The maximum level of the "Hero of the Village" (HotV) effect that a player can have. This limits HotV price decreases. Options:
<td>The maximum level of the "Hero of the Village" (HotV) effect that a player can have. This limits HotV price decreases.<br/><strong>Options:</strong>
<ul>
<li>Set to -1 to disable this feature and keep vanilla behavior</li>
<li>Set to a number between 0 and 5 to set the maximum HotV effect level players can have</li>
@ -76,27 +89,37 @@
</tr>
<tr>
<td><code>MaxDiscount:</code></td>
<td>The maximum discount (%) you can get from trading/healing zombie villagers. This limits reputation-based price decreases. Options:
<td>The maximum discount (%) you can get from trading/healing zombie villagers. This limits reputation-based price decreases.<br/><strong>Options:</strong>
<ul>
<li>Set to -1.0 to disable this feature and keep vanilla behavior</li>
<li>Set to a number between 0.0 and 1.0 to set the maximum discount a player can get. (NOTE: 30% = 0.3)</li>
<li>Set to a number between 0.0 and 1.0 to limit the maximum discount a player can get. (NOTE: 30% = 0.3)</li>
<li>Set to a number above 1.0 to increase the maximum discount a player can get. (NOTE: 250% = 2.5)</li>
</ul>
</td>
</tr>
<tr>
<td><code>MaxDemand:</code></td>
<td>The maximum demand for all items. This limits demand-based price increases. Options:
<td>The maximum demand for all items. This limits demand-based price increases.<br/><strong>Options:</strong>
<ul>
<li>Set to -1 to disable this feature and keep vanilla behavior</li>
<li>Set to 0 or higher to set the maximum demand for all items</li>
</ul><br>
WARNING: The previous demand information cannot be recovered if it was higher than the MaxDemand.
<strong>WARNING:</strong> The previous demand information cannot be recovered if it was higher than the MaxDemand.
</td>
</tr>
<tr>
<td><code>MaxUses:</code></td>
<td>The maximum number of times a player can make any trade before a villager is out of stock.<br/><strong>Options:</strong>
<ul>
<li>Set to -1 to disable this feature and keep vanilla behavior</li>
<li>Set to 0 or higher to change the maximum number of uses for all items</li>
</ul>
</td>
</tr>
</table>
</li>
<li>
<p>Per-item settings: (<code>Overrides:</code>)</p>
<p>Per-item override settings: (<code>Overrides:</code>)</p>
<table>
<tr>
<th>Setting</th>
@ -106,17 +129,37 @@
<td><code>&lt;item_name&gt;:</code></td>
<td>Override the global settings by adding as many of these as you need. Enchanted books must follow the format of <code>name_level</code> (mending_1). All other items must follow the format of <code>item_name</code> (stone_bricks).</td>
</tr>
<tr>
<td><code>.Disabled:</code></td>
<td>Disables any trade that contains the item (true/false)</td>
</tr>
<tr>
<td><code>.MaxDiscount:</code></td>
<td>Sets the maximum discount for this item</td>
<td>Sets the maximum discount for this item (-1.0, or between 0.0 to 1.0)</td>
</tr>
<tr>
<td><code>.MaxDemand:</code></td>
<td>Sets the maximum demand for this item</td>
<td>Sets the maximum demand for this item (-1, or 0+)</td>
</tr>
<tr>
<td><code>.MaxUses:</code></td>
<td>Sets the maximum number of times a player can make the trade before the villager is out of stock</td>
</tr>
<tr>
<td><code>.Cooldown:</code></td>
<td>Sets the time between restocks for the trade, and applies to ALL villagers. Once the player reaches the <code>MaxUses</code>, the cooldown begins. The trade is disabled for all villagers until the cooldown expires.<br><strong>Format:</strong> &lt;Number&gt;&lt;interval&gt;<br><strong>Examples:</strong> 30s = 30 seconds, 5m = 5 minutes, 4h = 4 hours, 7d = 7 days</td>
</tr>
<tr>
<td><code>.Item1.Material:</code><br><code>.Item2.Material:</code><br><code>.Result.Material:</code></td>
<td>Sets the material of the 1st, 2nd, or result item in the trade<br><strong>WARNING:</strong> This cannot be undone!</td>
</tr>
<tr>
<td><code>.Item1.Amount:</code><br><code>.Item2.Amount:</code><br><code>.Result.Amount:</code></td>
<td>Sets the amount of the 1st, 2nd, or result item in the trade<br><strong>WARNING:</strong> This cannot be undone!</td>
</tr>
</table>
</li>
<li>
<p>For the default config.yml, see: <code>src/main/resources/config.yml</code></p>
<p>For the default config.yml, see <code>src/main/resources/config.yml</code></p>
</li>
</ul>
</ul>

View File

@ -0,0 +1,96 @@
<?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/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.pretzel.dev</groupId>
<artifactId>VillagerTradeLimiter</artifactId>
<version>1.6.4</version>
<build>
<sourceDirectory>src</sourceDirectory>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<plugin>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.1</version>
<executions>
<execution>
<id>shade</id>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
<configuration>
<relocations>
<relocation>
<pattern>de.tr7zw.changeme.nbtapi</pattern>
<shadedPattern>com.pretzel.dev.villagertradelimiter.nms</shadedPattern>
</relocation>
</relocations>
</configuration>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spigotmc-repo</id>
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
</repository>
<repository>
<id>codemc-repo</id>
<url>https://repo.codemc.org/repository/maven-public/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.20-R0.1-SNAPSHOT</version>
<scope>provided</scope>
<exclusions>
<exclusion>
<artifactId>guava</artifactId>
<groupId>com.google.guava</groupId>
</exclusion>
<exclusion>
<artifactId>gson</artifactId>
<groupId>com.google.code.gson</groupId>
</exclusion>
<exclusion>
<artifactId>joml</artifactId>
<groupId>org.joml</groupId>
</exclusion>
<exclusion>
<artifactId>bungeecord-chat</artifactId>
<groupId>net.md-5</groupId>
</exclusion>
<exclusion>
<artifactId>snakeyaml</artifactId>
<groupId>org.yaml</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.40.1.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
<properties>
<java.version>1.8</java.version>
</properties>
</project>

57
pom.xml
View File

@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.pretzel.dev</groupId>
<artifactId>VillagerTradeLimiter</artifactId>
<version>1.2.1</version>
<version>1.6.4</version>
<properties>
<java.version>1.8</java.version>
@ -21,6 +21,28 @@
<target>${java.version}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.1</version>
<executions>
<execution>
<id>shade</id>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
<configuration>
<relocations>
<relocation>
<pattern>de.tr7zw.changeme.nbtapi</pattern>
<shadedPattern>com.pretzel.dev.villagertradelimiter.nms</shadedPattern>
</relocation>
</relocations>
</configuration>
</plugin>
</plugins>
</build>
@ -29,18 +51,41 @@
<id>spigotmc-repo</id>
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
</repository>
<repository>
<id>codemc-repo</id>
<url>https://repo.codemc.org/repository/maven-public/</url>
<layout>default</layout>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot</artifactId>
<version>1.17-R0.1-SNAPSHOT</version>
<artifactId>spigot-api</artifactId>
<version>1.20-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.16.5-R0.1-SNAPSHOT</version>
<groupId>de.tr7zw</groupId>
<artifactId>item-nbt-api</artifactId>
<version>2.11.3</version>
</dependency>
<dependency>
<groupId>de.tr7zw</groupId>
<artifactId>functional-annotations</artifactId>
<version>0.1-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.40.1.0</version>
<scope>provided</scope>
</dependency>
</dependencies>

View File

@ -1,28 +1,47 @@
package com.pretzel.dev.villagertradelimiter;
import com.pretzel.dev.villagertradelimiter.lib.CommandBase;
import com.pretzel.dev.villagertradelimiter.lib.ConfigUpdater;
import com.pretzel.dev.villagertradelimiter.commands.CommandManager;
import com.pretzel.dev.villagertradelimiter.commands.CommandBase;
import com.pretzel.dev.villagertradelimiter.data.PlayerData;
import com.pretzel.dev.villagertradelimiter.database.DatabaseManager;
import com.pretzel.dev.villagertradelimiter.listeners.InventoryListener;
import com.pretzel.dev.villagertradelimiter.listeners.VillagerListener;
import com.pretzel.dev.villagertradelimiter.settings.ConfigUpdater;
import com.pretzel.dev.villagertradelimiter.lib.Metrics;
import com.pretzel.dev.villagertradelimiter.lib.Util;
import com.pretzel.dev.villagertradelimiter.listeners.PlayerListener;
import com.pretzel.dev.villagertradelimiter.settings.Lang;
import com.pretzel.dev.villagertradelimiter.settings.Settings;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.File;
import java.io.IOException;
import java.util.*;
public class VillagerTradeLimiter extends JavaPlugin {
public static final String PLUGIN_NAME = "VillagerTradeLimiter";
public static final String PREFIX = ChatColor.GOLD+"["+PLUGIN_NAME+"] ";
private static final int BSTATS_ID = 9829;
//Settings
private FileConfiguration cfg;
private Lang lang;
private CommandManager commandManager;
private DatabaseManager databaseManager;
private PlayerListener playerListener;
private HashMap<UUID, PlayerData> playerData;
//Initial plugin load/unload
/** Initial plugin load/unload */
public void onEnable() {
//Initialize instance variables
this.cfg = null;
this.commandManager = new CommandManager(this);
this.playerData = new HashMap<>();
//Copy default settings & load settings
this.getConfig().options().copyDefaults();
@ -38,51 +57,70 @@ public class VillagerTradeLimiter extends JavaPlugin {
Util.consoleMsg(PREFIX+PLUGIN_NAME+" is running!");
}
//Loads or reloads config.yml settings
/** Save database on plugin stop, server stop */
public void onDisable() {
for(UUID uuid : playerData.keySet()) {
this.databaseManager.savePlayer(uuid, false);
}
this.playerData.clear();
}
/** Loads or reloads config.yml and messages.yml */
public void loadSettings() {
//Load config.yml
final String mainPath = this.getDataFolder().getPath()+"/";
final File file = new File(mainPath, "config.yml");
ConfigUpdater updater = new ConfigUpdater(this.getTextResource("config.yml"), file);
this.cfg = updater.updateConfig(file, PREFIX);
try {
ConfigUpdater.update(this, "config.yml", file, Collections.singletonList("Overrides"));
} catch (IOException e) {
Util.errorMsg(e);
}
this.cfg = YamlConfiguration.loadConfiguration(file);
this.lang = new Lang(this, this.getTextResource("messages.yml"), mainPath);
//Load/reload database manager
if(this.databaseManager == null) this.databaseManager = new DatabaseManager(this);
else onDisable();
this.databaseManager.load();
}
/** Load and initialize the bStats class with the plugin id */
private void loadBStats() {
if(this.cfg.getBoolean("bStats", true)) new Metrics(this, 9829);
}
//Registers plugin commands
private void registerCommands() {
final String reloaded = Util.replaceColors("&eVillagerTradeLimiter &ahas been reloaded!");
final CommandBase vtl = new CommandBase("villagertradelimiter", "villagertradelimiter.use", p -> this.help(p));
vtl.addSub(new CommandBase("reload", "villagertradelimiter.reload", p -> {
loadSettings();
if(p != null) p.sendMessage(reloaded);
}));
this.getCommand("villagertradelimiter").setExecutor(vtl);
this.getCommand("villagertradelimiter").setTabCompleter(vtl);
}
//Registers plugin listeners
private void registerListeners() {
this.getServer().getPluginManager().registerEvents(new PlayerListener(this), this);
}
// ------------------------- Commands -------------------------
private void help(final Player p) {
if(p != null) {
if(!p.hasPermission("villagertradelimiter.use") && !p.hasPermission("villagertradelimiter.*")) return;
p.sendMessage(ChatColor.GREEN+"VillagerTradeLimiter commands:");
p.sendMessage(ChatColor.AQUA+"/vtl "+ChatColor.WHITE+"- shows this help message");
Util.sendIfPermitted("villagertradelimiter.reload", ChatColor.AQUA+"/vtl reload "+ChatColor.WHITE+"- reloads config.yml", p);
} else {
Util.consoleMsg(ChatColor.GREEN+"VillagerTradeLimiter commands:");
Util.consoleMsg(ChatColor.AQUA+"/vtl "+ChatColor.WHITE+"- shows this help message");
Util.consoleMsg(ChatColor.AQUA+"/vtl reload "+ChatColor.WHITE+"- reloads config.yml");
if(this.cfg.getBoolean("bStats", true)) {
new Metrics(this, BSTATS_ID);
}
}
/** Registers plugin commands */
private void registerCommands() {
final CommandBase cmd = this.commandManager.getCommands();
this.getCommand("villagertradelimiter").setExecutor(cmd);
this.getCommand("villagertradelimiter").setTabCompleter(cmd);
}
/** Registers plugin listeners */
private void registerListeners() {
final Settings settings = new Settings(this);
this.playerListener = new PlayerListener(this, settings);
this.getServer().getPluginManager().registerEvents(this.playerListener, this);
this.getServer().getPluginManager().registerEvents(new InventoryListener(this, settings), this);
this.getServer().getPluginManager().registerEvents(new VillagerListener(this, settings), this);
}
// ------------------------- Getters -------------------------
//Returns the settings from config.yml
/** @return the settings from config.yml */
public FileConfiguration getCfg() { return this.cfg; }
/** @param path the key you want the value for
* @return a language setting from messages.yml */
public String getLang(final String path) { return this.lang.get(path); }
/** @return this plugin's player listener */
public PlayerListener getPlayerListener() { return this.playerListener; }
/** @return a player's data container */
public HashMap<UUID, PlayerData> getPlayerData() { return this.playerData; }
/** @return the invsee inventory's barrier block */
public ItemStack getBarrier() { return this.commandManager.getBarrier(); }
}

View File

@ -1,5 +1,8 @@
package com.pretzel.dev.villagertradelimiter.lib;
package com.pretzel.dev.villagertradelimiter.commands;
import com.pretzel.dev.villagertradelimiter.lib.Callback;
import com.pretzel.dev.villagertradelimiter.lib.Util;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
@ -15,6 +18,11 @@ public class CommandBase implements CommandExecutor, TabCompleter {
private final Callback<Player> callback;
private final ArrayList<CommandBase> subs;
/**
* @param name The name of the command
* @param permission The permission required to use the command
* @param callback The callback that is called when the command is executed
*/
public CommandBase(String name, String permission, Callback<Player> callback) {
this.name = name;
this.permission = permission;
@ -22,6 +30,10 @@ public class CommandBase implements CommandExecutor, TabCompleter {
this.subs = new ArrayList<>();
}
/**
* @param command The child command to add
* @return The given child command
*/
public CommandBase addSub(CommandBase command) {
this.subs.add(command);
return command;
@ -32,8 +44,8 @@ public class CommandBase implements CommandExecutor, TabCompleter {
final Player player = (sender instanceof Player ? (Player)sender : null);
if(player != null && !player.hasPermission(this.permission) && !this.permission.isEmpty()) return false;
if(args.length == 0) {
this.callback.call(player);
if(args.length == 0 || (args.length == 1 && subs.size() == 0)) {
this.callback.call(player, args);
return true;
}
@ -54,6 +66,7 @@ public class CommandBase implements CommandExecutor, TabCompleter {
final List<String> list = new ArrayList<>();
if(args.length == 0) return null;
if(args.length == 1) {
if(subs.size() == 0) return getPlayerList();
for(CommandBase cmd : subs)
if(player.hasPermission(cmd.getPermission()))
list.add(cmd.getName());
@ -67,12 +80,27 @@ public class CommandBase implements CommandExecutor, TabCompleter {
return list;
}
/**
* @param args The arguments to be copied
* @return The copied arguments
*/
private static String[] getCopy(final String[] args) {
String[] res = new String[args.length-1];
System.arraycopy(args, 1, res, 0, res.length);
return res;
}
/** @return The current online player list */
private static List<String> getPlayerList() {
final List<String> players = new ArrayList<>();
for(Player p : Bukkit.getOnlinePlayers())
players.add(p.getName());
return players;
}
/** @return The name of this command */
public String getName() { return this.name; }
/** @return The permission required to use this command */
public String getPermission() { return this.permission; }
}

View File

@ -0,0 +1,152 @@
package com.pretzel.dev.villagertradelimiter.commands;
import com.pretzel.dev.villagertradelimiter.VillagerTradeLimiter;
import com.pretzel.dev.villagertradelimiter.lib.Util;
import net.md_5.bungee.api.chat.ClickEvent;
import net.md_5.bungee.api.chat.HoverEvent;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.chat.hover.content.Text;
import org.bukkit.*;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.entity.Villager;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.util.Vector;
import java.util.Arrays;
public class CommandManager {
private final VillagerTradeLimiter instance;
private final ItemStack barrier;
/** @param instance The instance of VillagerTradeLimiter.java */
public CommandManager(final VillagerTradeLimiter instance) {
this.instance = instance;
this.barrier = new ItemStack(Material.BARRIER, 1);
ItemMeta meta = barrier.getItemMeta();
if(meta != null) {
meta.setDisplayName(ChatColor.RED+"Close");
meta.setLore(Arrays.asList(ChatColor.GRAY+"Click to close", ChatColor.GRAY+"this inventory."));
}
barrier.setItemMeta(meta);
}
/** @return The root command node, to be registered by the plugin */
public CommandBase getCommands() {
//Adds the /vtl command
final CommandBase cmd = new CommandBase("villagertradelimiter", "villagertradelimiter.use", (p, args) -> showHelp(p, "help"));
//Adds the /vtl reload command
cmd.addSub(new CommandBase("reload", "villagertradelimiter.reload", (p,args) -> {
//Reload the config and lang
instance.loadSettings();
Util.sendMsg(instance.getLang("common.reloaded"), p);
}));
//Adds the /vtl see <player> command
cmd.addSub(new CommandBase("see", "villagertradelimiter.see", (p,args) -> {
//Check if the command was issued via console
if(p == null) {
Util.sendMsg(instance.getLang("common.noconsole"), p);
return;
}
//Checks if there are enough arguments
if(args.length < 1) {
Util.sendMsg(instance.getLang("common.noargs"), p);
return;
}
//Get the closest villager. If a nearby villager wasn't found, send the player an error message
Entity closestEntity = getClosestEntity(p);
if(closestEntity == null) return;
//Gets the other player by name, using the first argument of the command
OfflinePlayer otherPlayer = Bukkit.getOfflinePlayer(args[0]);
if(!otherPlayer.isOnline() && !otherPlayer.hasPlayedBefore()) {
Util.sendMsg(instance.getLang("see.noplayer").replace("{player}", args[0]), p);
return;
}
//Open the other player's trade view for the calling player
Util.sendMsg(instance.getLang("see.success").replace("{player}", args[0]), p);
instance.getPlayerListener().see((Villager)closestEntity, p, otherPlayer);
}));
//Adds the /vtl invsee command
cmd.addSub(new CommandBase("invsee", "villagertradelimiter.invsee", (p, args) -> {
//Check if the command was issued via console
if(p == null) {
Util.sendMsg(instance.getLang("common.noconsole"), p);
return;
}
//Get the closest villager. If a nearby villager wasn't found, send the player an error message
Entity closestEntity = getClosestEntity(p);
if(closestEntity == null) return;
//Open the villager's inventory view for the calling player
final Villager closestVillager = (Villager)closestEntity;
final Inventory inventory = Bukkit.createInventory(null, 9, "Villager Inventory");
for(ItemStack item : closestVillager.getInventory().getContents()) {
if(item == null) continue;
inventory.addItem(item.clone());
}
inventory.setItem(8, barrier);
p.openInventory(inventory);
}));
return cmd;
}
/**
* @param player The player to get the closest entity for
* @return The closest entity to the player, that the player is looking at
*/
private Entity getClosestEntity(final Player player) {
Entity closestEntity = null;
double closestDistance = Double.MAX_VALUE;
for(Entity entity : player.getNearbyEntities(10, 10, 10)) {
if(entity instanceof Villager) {
Location eye = player.getEyeLocation();
Vector toEntity = ((Villager) entity).getEyeLocation().toVector().subtract(eye.toVector());
double dot = toEntity.normalize().dot(eye.getDirection());
double distance = eye.distance(((Villager)entity).getEyeLocation());
if(dot > 0.99D && distance < closestDistance) {
closestEntity = entity;
closestDistance = distance;
}
}
}
if(closestEntity == null) {
Util.sendMsg(instance.getLang("see.novillager"), player);
}
return closestEntity;
}
/**
* Sends an interactive help message to a player via chat
* @param p The player to show the help message to
* @param key The key of the help message to show (in messages.yml)
*/
public void showHelp(final Player p, final String key) {
for(String line : instance.getLang(key).split("\n")) {
int i = line.indexOf("]");
final String[] tokens = line.substring(i+1).split(";");
if(p == null) Util.consoleMsg(tokens[0]);
else {
final TextComponent text = new TextComponent(tokens[0]);
if(tokens.length > 1) text.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text(tokens[0]+"\n"+tokens[1])));
text.setClickEvent(new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, ChatColor.stripColor(tokens[0])));
if(p.hasPermission(line.substring(1, i))) p.spigot().sendMessage(text);
}
}
}
/** @return The barrier item */
public ItemStack getBarrier() {
return this.barrier;
}
}

View File

@ -0,0 +1,62 @@
package com.pretzel.dev.villagertradelimiter.data;
import com.pretzel.dev.villagertradelimiter.lib.Util;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
public class Cooldown {
private static final SimpleDateFormat FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH);
private enum Interval {
s(1L),
m(60L),
h(3600L),
d(86400L),
w(604800L);
final long factor;
Interval(long factor) {
this.factor = factor;
}
}
/**
* @param cooldownStr The cooldown time as written in config.yml (7d, 30s, 5m, etc)
* @return The cooldown time in seconds
*/
public static long parseCooldown(final String cooldownStr) {
if(cooldownStr.equals("0")) return 0;
try {
long time = Long.parseLong(cooldownStr.substring(0, cooldownStr.length()-1));
String interval = cooldownStr.substring(cooldownStr.length()-1).toLowerCase();
return time * Interval.valueOf(interval).factor;
} catch (Exception e) {
Util.errorMsg(e);
}
return 0;
}
/**
* @param date The date to format
* @return The date as a 'yyyy-MM-dd HH:mm:ss' formatted string
*/
public static String formatTime(Date date) {
return FORMAT.format(date);
}
/**
* @param timeStr The string to format, in 'yyyy-MM-dd HH:mm:ss' format
* @return The date that the string represents
*/
public static Date parseTime(final String timeStr) {
try {
return FORMAT.parse(timeStr);
} catch (ParseException e) {
Util.errorMsg(e);
}
return null;
}
}

View File

@ -0,0 +1,24 @@
package com.pretzel.dev.villagertradelimiter.data;
import com.pretzel.dev.villagertradelimiter.wrappers.VillagerWrapper;
import java.util.HashMap;
public class PlayerData {
private final HashMap<String, String> tradingCooldowns;
private VillagerWrapper tradingVillager;
public PlayerData() {
this.tradingCooldowns = new HashMap<>();
this.tradingVillager = null;
}
/** @return The map of items to timestamps for the player's trading history */
public HashMap<String, String> getTradingCooldowns() { return this.tradingCooldowns; }
/** @param tradingVillager The villager that this player is currently trading with */
public void setTradingVillager(final VillagerWrapper tradingVillager) { this.tradingVillager = tradingVillager; }
/** @return The villager that this player is currently trading with */
public VillagerWrapper getTradingVillager() { return this.tradingVillager; }
}

View File

@ -0,0 +1,65 @@
package com.pretzel.dev.villagertradelimiter.database;
import com.pretzel.dev.villagertradelimiter.lib.Callback;
import com.pretzel.dev.villagertradelimiter.lib.Util;
import org.bukkit.Bukkit;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.plugin.java.JavaPlugin;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
public abstract class Database {
protected final JavaPlugin instance;
public Database(final JavaPlugin instance) {
this.instance = instance;
}
//Tests a DataSource
public void test() {
try {
try (Connection conn = this.getSource().getConnection()) {
if (!conn.isValid(1000)) throw new SQLException("Could not connect to database!");
else Util.consoleMsg("Connected to database!");
}
} catch (SQLException e) {
Util.consoleMsg("Could not connect to database!");
}
}
//Executes a statement or query in the database
public ArrayList<String> execute(final String sql, boolean query) {
try(Connection conn = this.getSource().getConnection(); PreparedStatement statement = conn.prepareStatement(sql)) {
if(query) {
final ResultSet result = statement.executeQuery();
int columns = result.getMetaData().getColumnCount();
final ArrayList<String> res = new ArrayList<>();
while(result.next()){
String row = "";
for(int j = 0; j < columns; j++)
row += result.getString(j+1)+(j < columns-1?",":"");
res.add(row);
}
return res;
} else statement.execute();
} catch (SQLException e) {
Util.errorMsg(e);
}
return null;
}
public void execute(final String sql, boolean query, final Callback<ArrayList<String>> callback) {
Bukkit.getScheduler().runTaskAsynchronously(this.instance, () -> {
final ArrayList<String> result = execute(sql, query);
if(callback != null) Bukkit.getScheduler().runTask(this.instance, () -> callback.call(result));
});
}
public abstract void load(final ConfigurationSection cfg);
public abstract boolean isMySQL();
protected abstract DataSource getSource();
}

View File

@ -0,0 +1,107 @@
package com.pretzel.dev.villagertradelimiter.database;
import com.pretzel.dev.villagertradelimiter.VillagerTradeLimiter;
import com.pretzel.dev.villagertradelimiter.data.Cooldown;
import com.pretzel.dev.villagertradelimiter.data.PlayerData;
import com.pretzel.dev.villagertradelimiter.lib.Util;
import org.bukkit.Bukkit;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.entity.Villager;
import java.time.Instant;
import java.util.Date;
import java.util.UUID;
public class DatabaseManager {
private static final String CREATE_TABLE_COOLDOWN =
"CREATE TABLE IF NOT EXISTS vtl_cooldown("+
"uuid CHAR(36) NOT NULL,"+
"item VARCHAR(255) NOT NULL,"+
"time TEXT NOT NULL,"+
"PRIMARY KEY(uuid, item));";
private static final String SELECT_ITEMS = "SELECT * FROM vtl_cooldown;";
private static final String INSERT_ITEM = "INSERT OR IGNORE INTO vtl_cooldown(uuid,item,time) VALUES?;"; //INSERT IGNORE INTO for MySQL
private static final String DELETE_ITEMS = "DELETE FROM vtl_cooldown WHERE uuid='?';";
private final VillagerTradeLimiter instance;
private Database database;
public DatabaseManager(final VillagerTradeLimiter instance) {
this.instance = instance;
}
public void load() {
final ConfigurationSection cfg = instance.getCfg().getConfigurationSection("database");
if(cfg == null) {
Util.consoleMsg("Database settings missing from config.yml!");
this.database = null;
return;
}
boolean mysql = cfg.getBoolean("mysql", false);
if(this.database != null && ((mysql && this.database.isMySQL()) || (!mysql && !this.database.isMySQL()))) this.database.load(cfg);
else this.database = (mysql?new MySQL(instance, cfg):new SQLite(instance));
this.database.execute(CREATE_TABLE_COOLDOWN, false);
//Loads all the data
this.database.execute(SELECT_ITEMS, true, (result,args) -> {
if(result != null) {
for(String row : result) {
final String[] tokens = row.split(",");
UUID uuid = UUID.fromString(tokens[0]);
String item = tokens[1];
final Date date = Cooldown.parseTime(tokens[2]);
if(date == null) continue;
long time = date.getTime();
PlayerData data = instance.getPlayerData().get(uuid);
if(data == null) {
data = new PlayerData();
instance.getPlayerData().put(uuid, data);
}
String key = (Bukkit.getEntity(uuid) instanceof Villager ? "Restock" : "Cooldown");
String cooldownStr = instance.getCfg().getString(key, "0");
cooldownStr = instance.getCfg().getString("Overrides."+item+"."+key, cooldownStr);
long cooldown = Cooldown.parseCooldown(cooldownStr);
final Date now = Date.from(Instant.now());
if(cooldown != 0 && now.getTime()/1000L < time/1000L + cooldown) {
data.getTradingCooldowns().put(item, Cooldown.formatTime(date));
}
}
}
});
}
public void savePlayer(final UUID uuid, boolean async) {
if(this.database == null) return;
//Delete existing rows for player
final String uuidStr = uuid.toString();
if(async) this.database.execute(DELETE_ITEMS.replace("?", uuidStr), false, (result,args) -> save(uuid, true));
else {
this.database.execute(DELETE_ITEMS.replace("?", uuidStr), false);
save(uuid, false);
}
}
private void save(final UUID uuid, boolean async) {
//Insert new rows for player pages
final PlayerData playerData = instance.getPlayerData().get(uuid);
if(playerData == null) return;
String values = "";
for(String item : playerData.getTradingCooldowns().keySet()) {
final String time = playerData.getTradingCooldowns().get(item);
if(!values.isEmpty()) values += ",";
values += "('"+uuid+"','"+item+"','"+time+"')";
}
if(values.isEmpty()) return;
String sql = INSERT_ITEM.replace("?", values);
if(this.database.isMySQL()) sql = sql.replace(" OR ", " ");
if(async) this.database.execute(sql, false, (result,args) -> {});
else this.database.execute(sql, false);
}
}

View File

@ -0,0 +1,35 @@
package com.pretzel.dev.villagertradelimiter.database;
import com.mysql.cj.jdbc.MysqlConnectionPoolDataSource;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.plugin.java.JavaPlugin;
import javax.sql.DataSource;
import java.sql.SQLException;
public class MySQL extends Database {
private final MysqlConnectionPoolDataSource source;
public MySQL(final JavaPlugin instance, final ConfigurationSection cfg) {
super(instance);
this.source = new MysqlConnectionPoolDataSource();
this.load(cfg);
}
public void load(final ConfigurationSection cfg) {
this.source.setServerName(cfg.getString("host", "localhost"));
this.source.setPort(cfg.getInt("port", 3306));
this.source.setDatabaseName(cfg.getString("database", "sagas_holo"));
this.source.setUser(cfg.getString("username", "root"));
this.source.setPassword(cfg.getString("password", "root"));
try {
this.source.setCharacterEncoding(cfg.getString("encoding", "utf8"));
this.source.setUseSSL(cfg.getBoolean("useSSL", false));
} catch (SQLException ignored) {}
this.test();
}
public boolean isMySQL() { return true; }
public DataSource getSource() { return this.source; }
}

View File

@ -0,0 +1,25 @@
package com.pretzel.dev.villagertradelimiter.database;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.plugin.java.JavaPlugin;
import org.sqlite.javax.SQLiteConnectionPoolDataSource;
import javax.sql.DataSource;
public class SQLite extends Database {
private final SQLiteConnectionPoolDataSource source;
public SQLite(final JavaPlugin instance) {
super(instance);
this.source = new SQLiteConnectionPoolDataSource();
this.load(null);
}
public void load(final ConfigurationSection cfg) {
this.source.setUrl("jdbc:sqlite:"+instance.getDataFolder().getPath()+"/database.db");
this.test();
}
public boolean isMySQL() { return false; }
public DataSource getSource() { return this.source; }
}

View File

@ -1,5 +1,10 @@
package com.pretzel.dev.villagertradelimiter.lib;
public interface Callback<T> {
void call(T result);
/**
* Callback function
* @param result Any type of result to be passed into the callback function
* @param args Any extra arguments to be passed into the callback function
*/
void call(T result, String... args);
}

View File

@ -1,84 +0,0 @@
package com.pretzel.dev.villagertradelimiter.lib;
import java.io.File;
import java.io.FileReader;
import java.io.Reader;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
public class ConfigUpdater {
private final String[] cfgDefault;
private String[] cfgActive;
public ConfigUpdater(final Reader def, final File active) {
this.cfgDefault = Util.readFile(def);
this.cfgActive = this.cfgDefault;
try {
this.cfgActive = Util.readFile(new FileReader(active));
} catch (Exception e) {
Util.errorMsg(e);
}
}
private String getVersion(String[] cfg) {
for(String x : cfg)
if(x.startsWith("#") && x.endsWith("#") && x.contains("Version: "))
return x.split(": ")[1].replace("#", "").trim();
return "0";
}
public FileConfiguration updateConfig(File file, String prefix) {
final FileConfiguration cfg = (FileConfiguration)YamlConfiguration.loadConfiguration(file);
if(this.isUpdated()) return cfg;
Util.consoleMsg(prefix+"Updating config.yml...");
String out = "";
for(int i = 0; i < cfgDefault.length; i++) {
String line = cfgDefault[i];
if(line.startsWith("#") || line.replace(" ", "").isEmpty()) {
if(!line.startsWith(" ")) out += line+"\n";
} else if(!line.startsWith(" ")) {
if(line.contains(": ")) {
out += matchActive(line.split(": ")[0], line)+"\n";
} else if(line.contains(":")) {
String set = matchActive(line, "");
if(set.contains("none")) {
out += set+"\n";
continue;
}
out += line+"\n";
boolean found = false;
for(int j = 0; j < cfgActive.length; j++) {
String line2 = cfgActive[j];
if(line2.startsWith(" ") && !line2.replace(" ", "").isEmpty()) {
out += line2+"\n";
found = true;
}
}
if(found == false) {
while(i < cfgDefault.length-1) {
i++;
String line2 = cfgDefault[i];
out += line2+"\n";
if(!line2.startsWith(" ")) break;
}
}
}
}
}
Util.writeFile(file, out+"\n");
return (FileConfiguration)YamlConfiguration.loadConfiguration(file);
}
public boolean isUpdated() {
return getVersion(cfgActive).contains(getVersion(cfgDefault));
}
private String matchActive(String start, String def) {
for(String x : cfgActive)
if(x.startsWith(start))
return x;
return def;
}
}

View File

@ -2,47 +2,61 @@ package com.pretzel.dev.villagertradelimiter.lib;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Reader;
import org.bukkit.Bukkit;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.entity.Villager;
import org.bukkit.inventory.ItemStack;
import org.bukkit.util.io.BukkitObjectInputStream;
import org.bukkit.util.io.BukkitObjectOutputStream;
import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder;
import net.md_5.bungee.api.ChatColor;
public class Util {
//Sends a message to the sender of a command
public static void sendMsg(String msg, Player p) {
if(p == null) consoleMsg(msg);
else p.sendMessage(msg);
/**
* Sends a message to the sender of a command
* @param msg The message to send
* @param player The player (or console) to sent the message to
*/
public static void sendMsg(String msg, Player player) {
if(player == null) consoleMsg(msg);
else player.sendMessage(msg);
}
//Sends a message to a player if they have permission
/**
* Sends a message to a player if they have permission
* @param perm The name of the permission to check
* @param msg The message to send
* @param player The player (or console) to sent the message to
*/
public static void sendIfPermitted(String perm, String msg, Player player) {
if(player.hasPermission(perm)) player.sendMessage(msg);
}
//Sends a message to the console
/**
* Sends a message to the console
* @param msg The message to send
*/
public static void consoleMsg(String msg) {
if(msg != null) Bukkit.getServer().getConsoleSender().sendMessage(msg);
}
/**
* Replaces the color tags to bukkit color tags
* @param in The string to replace color tags for
* @return The string with replaced colors
*/
public static String replaceColors(String in) {
if(in == null) return null;
return in.replace("&", "\u00A7");
}
//Sends an error message to the console
/**
* Sends an error message to the console
* @param e The error to send a message for
*/
public static void errorMsg(Exception e) {
String error = e.toString();
for(StackTraceElement x : e.getStackTrace()) {
@ -51,26 +65,47 @@ public class Util {
consoleMsg(ChatColor.RED+"ERROR: "+error);
}
//Returns whether a player is a Citizens NPC or not
public static boolean isNPC(Player player) {
return player.hasMetadata("NPC");
/**
* Checks whether an entity is a Citizens NPC or not
* @param entity The entity to check
* @return True if the entity is an NPC, false otherwise
*/
public static boolean isNPC(Entity entity) {
return entity.hasMetadata("NPC");
}
//Returns whether a villager is a Citizens NPC or not
public static boolean isNPC(Villager villager) {
return villager.hasMetadata("NPC");
/**
* Returns whether an entity is a shopkeeper NPC or not
* @param entity The villager to check
* @return True if the villager is a shopkeeper, false otherwise
*/
public static boolean isShopkeeper(Entity entity) {
return entity.hasMetadata("shopkeeper");
}
//Converts an int array to a string
public static String intArrayToString(int[] arr) {
/**
* Combines the elements of an int[] into a string
* @param arr The int[] to combine
* @param separator (optional) The string to place between elements of the int[]
* @return The combined string of the int[]
*/
public static String intArrayToString(int[] arr, String separator) {
String res = "";
for(int a : arr) { res += a+""; }
for(int a : arr) { res += a+separator; }
return res;
}
public static String intArrayToString(int[] arr) {
return intArrayToString(arr, "");
}
/**
* Reads the lines of a file into a String[]
* @param reader The file reader
* @return The lines of the file, as a String[]
*/
public static String[] readFile(Reader reader) {
String out = "";
BufferedReader br = null;
BufferedReader br;
try {
br = new BufferedReader(reader);
String line;
@ -83,6 +118,12 @@ public class Util {
}
return null;
}
/**
* Reads the lines of a file into a String[]
* @param file The file
* @return The lines of the file, as a String[]
*/
public static String[] readFile(File file) {
try {
return readFile(new FileReader(file));
@ -92,8 +133,13 @@ public class Util {
}
}
/**
* Writes a String to a file
* @param file The file to write the String to
* @param out The String to write to the file
*/
public static void writeFile(File file, String out) {
BufferedWriter bw = null;
BufferedWriter bw;
try {
bw = new BufferedWriter(new FileWriter(file));
bw.write(out);
@ -102,50 +148,4 @@ public class Util {
errorMsg(e);
}
}
public static final String stacksToBase64(final ItemStack[] contents) {
try {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
BukkitObjectOutputStream dataOutput = new BukkitObjectOutputStream(outputStream);
dataOutput.writeInt(contents.length);
for (ItemStack stack : contents) dataOutput.writeObject(stack);
dataOutput.close();
return Base64Coder.encodeLines(outputStream.toByteArray()).replace("\n", "").replace("\r", "");
} catch (Exception e) {
throw new IllegalStateException("Unable to save item stacks.", e);
}
}
public static final ItemStack[] stacksFromBase64(final String data) {
if (data == null || Base64Coder.decodeLines(data).equals(null))
return new ItemStack[]{};
ByteArrayInputStream inputStream = new ByteArrayInputStream(Base64Coder.decodeLines(data));
BukkitObjectInputStream dataInput = null;
ItemStack[] stacks = null;
try {
dataInput = new BukkitObjectInputStream(inputStream);
stacks = new ItemStack[dataInput.readInt()];
} catch (IOException e) {
Util.errorMsg(e);
}
for (int i = 0; i < stacks.length; i++) {
try {
stacks[i] = (ItemStack) dataInput.readObject();
} catch (IOException | ClassNotFoundException e) {
try { dataInput.close(); }
catch (IOException ignored) {}
Util.errorMsg(e);
return null;
}
}
try { dataInput.close(); }
catch (IOException ignored) {}
return stacks;
}
}

View File

@ -0,0 +1,149 @@
package com.pretzel.dev.villagertradelimiter.listeners;
import com.pretzel.dev.villagertradelimiter.VillagerTradeLimiter;
import com.pretzel.dev.villagertradelimiter.data.Cooldown;
import com.pretzel.dev.villagertradelimiter.data.PlayerData;
import com.pretzel.dev.villagertradelimiter.settings.Settings;
import com.pretzel.dev.villagertradelimiter.wrappers.VillagerWrapper;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.entity.Player;
import org.bukkit.entity.Villager;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.MerchantRecipe;
import java.time.Instant;
import java.util.Date;
public class InventoryListener implements Listener {
private final VillagerTradeLimiter instance;
private final Settings settings;
/**
* @param instance The instance of VillagerTradeLimiter.java
* @param settings The settings instance
*/
public InventoryListener(final VillagerTradeLimiter instance, final Settings settings) {
this.instance = instance;
this.settings = settings;
}
/** Handles when a player clicks in an inventory window */
@EventHandler
public void onPlayerClickInventory(final InventoryClickEvent event) {
if(event.getInventory().getType() != InventoryType.CHEST) return;
if(event.getInventory().getSize() != 9) return;
if(!(event.getWhoClicked() instanceof Player)) return;
if(settings.shouldSkipNPC(event.getWhoClicked())) return; //Skips NPCs
ItemStack barrier = event.getInventory().getItem(8);
if(barrier == null || !barrier.isSimilar(instance.getBarrier())) return;
//If the inventory matches the invsee inventory, cancel click events
event.setCancelled(true);
if(event.getCurrentItem() != null && event.getCurrentItem().isSimilar(instance.getBarrier())) {
Bukkit.getScheduler().runTaskLater(instance, () -> event.getWhoClicked().closeInventory(), 0L);
}
}
/** Handles when a player stops trading with a villager */
@EventHandler
public void onPlayerStopTrading(final InventoryCloseEvent event) {
//Don't do anything unless the player is actually finished trading with a villager
if(event.getInventory().getType() != InventoryType.MERCHANT) return;
if(!(event.getPlayer() instanceof Player)) return;
if(!(event.getInventory().getHolder() instanceof Villager)) return;
final Player player = (Player)event.getPlayer();
final Villager villager = (Villager)event.getInventory().getHolder();
if(settings.shouldSkipNPC(player) || settings.shouldSkipNPC(villager)) return; //Skips NPCs
//Reset the villager's NBT data when a player is finished trading
final PlayerData playerData = instance.getPlayerData().get(player.getUniqueId());
if(playerData == null) return;
final VillagerWrapper villagerWrapper = playerData.getTradingVillager();
if(villagerWrapper == null) return;
playerData.setTradingVillager(null);
villagerWrapper.reset();
}
/** Handles when a player successfully trades with a villager */
@EventHandler
public void onPlayerMakeTrade(final InventoryClickEvent event) {
if(event.getInventory().getType() != InventoryType.MERCHANT) return;
if(!(event.getInventory().getHolder() instanceof Villager)) return;
if(!(event.getWhoClicked() instanceof Player)) return;
if(event.getRawSlot() != 2) return;
final Player player = (Player)event.getWhoClicked();
final Villager villager = (Villager)event.getInventory().getHolder();
if(settings.shouldSkipNPC(player) || settings.shouldSkipNPC(villager)) return; //Skips NPCs
//Get the items involved in the trade
final ItemStack result = event.getCurrentItem();
ItemStack ingredient1 = event.getInventory().getItem(0);
ItemStack ingredient2 = event.getInventory().getItem(1);
if(result == null || result.getType() == Material.AIR) return;
if(ingredient1 == null) ingredient1 = new ItemStack(Material.AIR, 1);
if(ingredient2 == null) ingredient2 = new ItemStack(Material.AIR, 1);
//Check if there is a cooldown set for the trade
final ConfigurationSection overrides = instance.getCfg().getConfigurationSection("Overrides");
if(overrides == null) return;
final String type = settings.getType(result, ingredient1, ingredient2);
String cooldownStr = instance.getCfg().getString("Cooldown", "0");
cooldownStr = overrides.getString(type+".Cooldown", cooldownStr);
String restockStr = instance.getCfg().getString("Restock", "0");
restockStr = overrides.getString(type+".Restock", restockStr);
if(cooldownStr.equals("0") && restockStr.equals("0")) return;
//Get the selected recipe by the items in the slots
final MerchantRecipe selectedRecipe = getSelectedRecipe(villager, ingredient1, ingredient2, result);
if(selectedRecipe == null) {
event.setCancelled(true);
return;
}
//Add a cooldown to the trade if the player has reached the max uses
final PlayerData playerData = instance.getPlayerData().get(player.getUniqueId());
final PlayerData villagerData = instance.getPlayerData().get(villager.getUniqueId());
if(playerData == null || playerData.getTradingVillager() == null) return;
Bukkit.getScheduler().runTaskLater(instance, () -> {
int uses = selectedRecipe.getUses();
final String time = Cooldown.formatTime(Date.from(Instant.now()));
if(uses >= selectedRecipe.getMaxUses()) {
if(!playerData.getTradingCooldowns().containsKey(type)) {
playerData.getTradingCooldowns().put(type, time);
}
if(villagerData != null && !villagerData.getTradingCooldowns().containsKey(type)) {
villagerData.getTradingCooldowns().put(type, time);
}
}
}, 1);
}
/**
* @param villager The villager to get the recipe from
* @param ingredient1 The item in the first ingredient slot of the trade interface
* @param ingredient2 The item in the second ingredient slot of the trade interface
* @param result The item in the result slot of the trade interface
* @return The villager's recipe that matches the items in the slots
*/
private MerchantRecipe getSelectedRecipe(final Villager villager, final ItemStack ingredient1, final ItemStack ingredient2, final ItemStack result) {
for(MerchantRecipe recipe : villager.getRecipes()) {
final ItemStack item1 = recipe.getIngredients().get(0);
final ItemStack item2 = recipe.getIngredients().get(1);
if(!recipe.getResult().isSimilar(result)) continue;
if((item1.isSimilar(ingredient1) && item2.isSimilar(ingredient2)) || (item1.isSimilar(ingredient2) && item2.isSimilar(ingredient1)))
return recipe;
}
return null;
}
}

View File

@ -1,196 +1,274 @@
package com.pretzel.dev.villagertradelimiter.listeners;
import com.pretzel.dev.villagertradelimiter.VillagerTradeLimiter;
import com.pretzel.dev.villagertradelimiter.data.Cooldown;
import com.pretzel.dev.villagertradelimiter.data.PlayerData;
import com.pretzel.dev.villagertradelimiter.lib.Util;
import com.pretzel.dev.villagertradelimiter.nms.*;
import org.bukkit.Bukkit;
import com.pretzel.dev.villagertradelimiter.settings.Settings;
import com.pretzel.dev.villagertradelimiter.wrappers.IngredientWrapper;
import com.pretzel.dev.villagertradelimiter.wrappers.PlayerWrapper;
import com.pretzel.dev.villagertradelimiter.wrappers.RecipeWrapper;
import com.pretzel.dev.villagertradelimiter.wrappers.VillagerWrapper;
import java.time.Instant;
import java.util.Date;
import java.util.List;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.OfflinePlayer;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.enchantments.EnchantmentWrapper;
import org.bukkit.entity.Player;
import org.bukkit.entity.Villager;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerInteractEntityEvent;
import org.bukkit.inventory.MerchantRecipe;
import org.bukkit.inventory.meta.EnchantmentStorageMeta;
import org.bukkit.inventory.ItemStack;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
import java.util.List;
public class PlayerListener implements Listener {
private static final Material[] MATERIALS = new Material[] { Material.IRON_HELMET, Material.IRON_CHESTPLATE, Material.IRON_LEGGINGS, Material.IRON_BOOTS, Material.BELL, Material.CHAINMAIL_HELMET, Material.CHAINMAIL_CHESTPLATE, Material.CHAINMAIL_LEGGINGS, Material.CHAINMAIL_BOOTS, Material.SHIELD, Material.DIAMOND_HELMET, Material.DIAMOND_CHESTPLATE, Material.DIAMOND_LEGGINGS, Material.DIAMOND_BOOTS, Material.FILLED_MAP, Material.FISHING_ROD, Material.LEATHER_HELMET, Material.LEATHER_CHESTPLATE, Material.LEATHER_LEGGINGS, Material.LEATHER_BOOTS, Material.LEATHER_HORSE_ARMOR, Material.SADDLE, Material.ENCHANTED_BOOK, Material.STONE_AXE, Material.STONE_SHOVEL, Material.STONE_PICKAXE, Material.STONE_HOE, Material.IRON_AXE, Material.IRON_SHOVEL, Material.IRON_PICKAXE, Material.DIAMOND_AXE, Material.DIAMOND_SHOVEL, Material.DIAMOND_PICKAXE, Material.DIAMOND_HOE, Material.IRON_SWORD, Material.DIAMOND_SWORD };
private final VillagerTradeLimiter instance;
private final NMS nms;
private final Settings settings;
public PlayerListener(VillagerTradeLimiter instance) {
/**
* @param instance The instance of VillagerTradeLimiter.java
* @param settings The settings instance
*/
public PlayerListener(final VillagerTradeLimiter instance, final Settings settings) {
this.instance = instance;
this.nms = new NMS(Bukkit.getServer().getClass().getPackage().getName().split("\\.")[3]);
this.settings = settings;
}
/** Handles when a player begins trading with a villager */
@EventHandler
public void onPlayerInteract(PlayerInteractEntityEvent event) {
if(!(event.getRightClicked() instanceof Villager)) return;
public void onPlayerBeginTrading(final PlayerInteractEntityEvent event) {
if(event.isCancelled()) return; //Skips when event is already cancelled
if(!(event.getRightClicked() instanceof Villager)) return; //Skips non-villager entities
final Player player = event.getPlayer();
final Villager villager = (Villager)event.getRightClicked();
if(Util.isNPC(villager)) return; //Skips NPCs
if(villager.getProfession() == Villager.Profession.NONE || villager.getProfession() == Villager.Profession.NITWIT) return; //Skips non-trading villagers
if(villager.getRecipeCount() == 0) return; //Skips non-trading villagers
if(instance.getCfg().getBoolean("DisableTrading", false)) {
event.setCancelled(true);
//Skips when the villager is in a disabled world
if(instance.getCfg().getStringList("DisableWorlds").contains(villager.getWorld().getName())) return;
//Skips when player is holding an ignored item
ItemStack heldItem = player.getInventory().getItem(event.getHand());
if(heldItem != null) {
Material heldItemType = heldItem.getType();
for(String ignoredType : instance.getCfg().getStringList("IgnoreHeldItems")) {
if(heldItemType.equals(Material.matchMaterial(ignoredType))) return;
}
}
if(settings.shouldSkipNPC(event.getPlayer()) || settings.shouldSkipNPC(villager)) return; //Skips NPCs
if(villager.getProfession() == Villager.Profession.NONE || villager.getProfession() == Villager.Profession.NITWIT || villager.getRecipeCount() == 0) return; //Skips non-trading villagers
//DisableTrading feature
if(instance.getCfg().isBoolean("DisableTrading")) {
//If all trading is disabled
if(instance.getCfg().getBoolean("DisableTrading", false)) {
event.setCancelled(true);
return;
}
} else {
//If trading in the world the player is in is disabled
final List<String> disabledWorlds = instance.getCfg().getStringList("DisableTrading");
final String world = event.getPlayer().getWorld().getName();
for(String disabledWorld : disabledWorlds) {
if(world.equals(disabledWorld)) {
event.setCancelled(true);
return;
}
}
}
//Cancel the original event, and open the adjusted trade view
//event.setCancelled(true);
if(!instance.getPlayerData().containsKey(player.getUniqueId())) {
instance.getPlayerData().put(player.getUniqueId(), new PlayerData());
}
if(!instance.getPlayerData().containsKey(villager.getUniqueId())) {
instance.getPlayerData().put(villager.getUniqueId(), new PlayerData());
}
this.see(villager, player, player);
}
/**
* Opens the villager's trading menu, with the adjusted trades of another player (or the same player)
* @param villager The villager whose trades you want to see
* @param player The player who calls the command, or the player that has begun trading
* @param other The other player to view trades for, or the player that has just begun trading
*/
public void see(final Villager villager, final Player player, final OfflinePlayer other) {
//Skips when the villager is in a disabled world
if(instance.getCfg().getStringList("DisableWorlds").contains(villager.getWorld().getName())) {
Util.sendMsg(instance.getLang("see.noworld"), player);
return;
}
final Player player = event.getPlayer();
if(Util.isNPC(player)) return; //Skips NPCs
this.hotv(player);
this.maxDiscount(villager, player);
this.maxDemand(villager);
}
//Wraps the villager and player into wrapper classes
final VillagerWrapper villagerWrapper = new VillagerWrapper(villager);
final PlayerWrapper otherWrapper = new PlayerWrapper(other);
final Player otherPlayer = otherWrapper.getPlayer();
if(settings.shouldSkipNPC(player) || settings.shouldSkipNPC(villager) || otherPlayer == null || settings.shouldSkipNPC(otherPlayer)) return; //Skips NPCs
private void hotv(final Player player) {
final PotionEffectType effect = PotionEffectType.HERO_OF_THE_VILLAGE;
if(!player.hasPotionEffect(effect)) return; //Skips when player doesn't have HotV
final PlayerData playerData = instance.getPlayerData().get(other.getUniqueId());
if(playerData != null) playerData.setTradingVillager(villagerWrapper);
final int maxHeroLevel = instance.getCfg().getInt("MaxHeroLevel", 1);
if(maxHeroLevel == 0) player.removePotionEffect(effect);
if(maxHeroLevel <= 0) return; //Skips when disabled in config.yml
//Checks if the version is old, before the 1.16 UUID changes
String version = instance.getServer().getClass().getPackage().getName();
boolean isOld = version.contains("1_13_") || version.contains("1_14_") || version.contains("1_15_");
final PotionEffect pot = player.getPotionEffect(effect);
if(pot.getAmplifier() > maxHeroLevel-1) {
player.removePotionEffect(effect);
player.addPotionEffect(new PotionEffect(effect, pot.getDuration(), maxHeroLevel-1));
}
}
//Calculates the player's total reputation and Hero of the Village discount
int totalReputation = villagerWrapper.getTotalReputation(villagerWrapper, otherWrapper, isOld);
double hotvDiscount = getHotvDiscount(otherWrapper);
private void maxDiscount(final Villager villager, final Player player) {
final List<MerchantRecipe> recipes = villager.getRecipes();
int a = 0, b = 0, c = 0, d = 0, e = 0;
//Adjusts the recipe prices, MaxUses, and ingredients
final List<RecipeWrapper> recipes = villagerWrapper.getRecipes();
for(RecipeWrapper recipe : recipes) {
//Set the special price (discount)
recipe.setSpecialPrice(getDiscount(recipe, totalReputation, hotvDiscount));
final NBTContainer vnbt = new NBTContainer(this.nms, villager);
final NBTTagList gossips = new NBTTagList(this.nms, vnbt.getTag().get("Gossips"));
final NBTContainer pnbt = new NBTContainer(this.nms, player);
final String puuid = Util.intArrayToString(pnbt.getTag().getIntArray("UUID"));
for (int i = 0; i < gossips.size(); ++i) {
final NBTTagCompound gossip = gossips.getCompound(i);
final String type = gossip.getString("Type");
final String tuuid = Util.intArrayToString(gossip.getIntArray("Target"));
final int value = gossip.getInt("Value");
if (tuuid.equals(puuid)) {
switch(type) {
case "trading": c = value; break;
case "minor_positive": b = value; break;
case "minor_negative": d = value; break;
case "major_positive": a = value; break;
case "major_negative": e = value; break;
default: break;
}
//Set ingredient materials and amounts
final ConfigurationSection override = settings.getOverride(recipe.getItemStack("buy"), recipe.getItemStack("sell"));
if(override != null) {
setIngredient(override.getConfigurationSection("Item1"), recipe.getIngredient1());
setIngredient(override.getConfigurationSection("Item2"), recipe.getIngredient2());
setIngredient(override.getConfigurationSection("Result"), recipe.getResult());
}
//Set the maximum number of uses (trades/restock)
recipe.setMaxUses(getMaxUses(recipe, other));
}
final ConfigurationSection overrides = instance.getCfg().getConfigurationSection("Overrides");
for (final MerchantRecipe recipe : recipes) {
final int x = recipe.getIngredients().get(0).getAmount();
final float p0 = this.getPriceMultiplier(recipe);
final int w = 5 * a + b + c - d - 5 * e;
final float y = x - p0 * w;
double maxDiscount = instance.getCfg().getDouble("MaxDiscount", 0.3);
//Open the villager's trading menu
player.openMerchant(villager, false);
}
/**
* @param recipe The recipe to get the base price for
* @return The initial price of a recipe/trade, before any discounts are applied
*/
private int getBasePrice(final RecipeWrapper recipe) {
int basePrice = recipe.getIngredient1().getItemStack().getAmount();
basePrice = settings.fetchInt(recipe, "Item1.Amount", basePrice);
return Math.min(Math.max(basePrice, 1), 64);
}
/**
* @param recipe The recipe to get the demand for
* @return The current value of the demand for the given recipe
*/
private int getDemand(final RecipeWrapper recipe) {
int demand = recipe.getDemand();
int maxDemand = settings.fetchInt(recipe, "MaxDemand", -1);
if(maxDemand < 0) maxDemand = 1000000;
if(demand > maxDemand) {
recipe.setDemand(maxDemand);
return maxDemand;
}
return demand;
}
/**
* @param recipe The recipe to get the discount for
* @param totalReputation The player's total reputation from a villager's gossips
* @param hotvDiscount The total discount from the Hero of the Village effect
* @return The total discount for the recipe, which is added to the base price to get the final price
*/
private int getDiscount(final RecipeWrapper recipe, int totalReputation, double hotvDiscount) {
//Calculates the total discount
int basePrice = getBasePrice(recipe);
int demand = getDemand(recipe);
float priceMultiplier = recipe.getPriceMultiplier();
int discount = -(int)(totalReputation * priceMultiplier) - (int)(hotvDiscount * basePrice) + Math.max(0, (int)(demand * priceMultiplier * basePrice));
double maxDiscount = settings.fetchDouble(recipe, "MaxDiscount", 0.3); //0.1
if(maxDiscount >= 0.0 && maxDiscount <= 1.0) {
//Change the discount to the smaller MaxDiscount
if(basePrice + discount < basePrice * (1.0 - maxDiscount)) {
discount = -(int)(basePrice * maxDiscount);
}
} else if(maxDiscount > 1.0) {
//Change the discount to the larger MaxDiscount
//TODO: Allow for better fine-tuning
discount = (int)(discount * maxDiscount);
}
return discount;
}
/**
* @param recipe The recipe to get the MaxUses for
* @return The current maximum number of times a player can make a trade before the villager restocks
*/
private int getMaxUses(final RecipeWrapper recipe, final OfflinePlayer player) {
int uses = recipe.getMaxUses();
int maxUses = settings.fetchInt(recipe, "MaxUses", -1);
boolean disabled = settings.fetchBoolean(recipe, "Disabled", false);
//Disables the trade if the player has an active cooldown for the trade
final PlayerData playerData = instance.getPlayerData().get(player.getUniqueId());
if(playerData != null && playerData.getTradingVillager() != null) {
final ConfigurationSection overrides = instance.getCfg().getConfigurationSection("Overrides");
if(overrides != null) {
for (final String k : overrides.getKeys(false)) {
final ConfigurationSection item = this.getItem(recipe, k);
if (item != null) {
maxDiscount = item.getDouble("MaxDiscount", maxDiscount);
break;
final String type = settings.getType(recipe.getItemStack("sell"), recipe.getItemStack("buy"), recipe.getItemStack("buyB"));
final String global = instance.getCfg().getString("Cooldown", "0");
final String local = overrides.getString(type+".Cooldown", global);
if(type != null && !local.equals("0")) {
if(playerData.getTradingCooldowns().containsKey(type)) {
final Date now = Date.from(Instant.now());
final Date lastTrade = Cooldown.parseTime(playerData.getTradingCooldowns().get(type));
long cooldown = Cooldown.parseCooldown(local);
if(lastTrade != null && (now.getTime()/1000L >= lastTrade.getTime()/1000L + cooldown)) {
playerData.getTradingCooldowns().remove(type);
} else {
maxUses = 0;
}
}
}
}
if(maxDiscount >= 0.0 && maxDiscount <= 1.0) {
if(y < x * (1.0 - maxDiscount) && y != x) {
recipe.setPriceMultiplier(x * (float)maxDiscount / w);
} else {
recipe.setPriceMultiplier(p0);
}
} else {
recipe.setPriceMultiplier(p0);
}
}
if(maxUses < 0) maxUses = uses;
if(disabled) maxUses = 0;
return maxUses;
}
private void maxDemand(final Villager villager) {
List<MerchantRecipe> recipes = villager.getRecipes();
final NBTContainer vnbt = new NBTContainer(this.nms, villager);
final NBTTagCompound vtag = vnbt.getTag();
final NBTTagList recipes2 = new NBTTagList(this.nms, vtag.getCompound("Offers").get("Recipes"));
/**
* @param playerWrapper The wrapped player to check the hotv effect for
* @return The Hero of the Village discount factor, adjusted by config
*/
private double getHotvDiscount(final PlayerWrapper playerWrapper) {
final Player player = playerWrapper.getPlayer();
if(player == null) return 0.0;
final ConfigurationSection overrides = instance.getCfg().getConfigurationSection("Overrides");
for(int i = 0; i < recipes2.size(); ++i) {
final NBTTagCompound recipe2 = recipes2.getCompound(i);
final int demand = recipe2.getInt("demand");
int maxDemand = instance.getCfg().getInt("MaxDemand", -1);
if(overrides != null) {
for(final String k : overrides.getKeys(false)) {
final ConfigurationSection item = this.getItem(recipes.get(i), k);
if(item != null) {
maxDemand = item.getInt("MaxDemand", maxDemand);
break;
}
}
}
if(maxDemand >= 0 && demand > maxDemand) {
recipe2.setInt("demand", maxDemand);
}
final PotionEffectType effectType = PotionEffectType.HERO_OF_THE_VILLAGE;
if(!player.hasPotionEffect(effectType)) return 0.0;
final PotionEffect effect = player.getPotionEffect(effectType);
if(effect == null) return 0.0;
//Calculates the discount factor from the player's current effect level or the defined maximum
int heroLevel = effect.getAmplifier()+1;
final int maxHeroLevel = instance.getCfg().getInt("MaxHeroLevel", -1);
if(maxHeroLevel == 0 || heroLevel == 0) return 0.0;
if(maxHeroLevel > 0 && heroLevel > maxHeroLevel) {
heroLevel = maxHeroLevel;
}
villager.getInventory().clear();
vnbt.saveTag(villager, vtag);
return 0.0625*(heroLevel-1) + 0.3;
}
private float getPriceMultiplier(final MerchantRecipe recipe) {
float p = 0.05f;
final Material type = recipe.getResult().getType();
for(int length = MATERIALS.length, i = 0; i < length; ++i) {
if(type == MATERIALS[i]) {
p = 0.2f;
break;
}
/**
* @param item The config section that contains the settings for Item1, Item2, or Result items in the trade
* @param ingredient The respective ingredient to change, based on config.yml
*/
private void setIngredient(final ConfigurationSection item, final IngredientWrapper ingredient) {
if(item == null) return;
ItemStack previous = ingredient.getItemStack();
Material material = Material.matchMaterial(item.getString("Material", previous.getType().getKey().getKey()));
if (material != null) {
ingredient.setItemStack(new ItemStack(
material,
item.getInt("Amount", previous.getAmount())
));
}
return p;
}
private ConfigurationSection getItem(final MerchantRecipe recipe, final String k) {
final ConfigurationSection item = instance.getCfg().getConfigurationSection("Overrides."+k);
if(item == null) return null;
if(!k.contains("_")) {
//Return the item if the item name is valid
if(this.verify(recipe, Material.matchMaterial(k))) return item;
return null;
}
final String[] words = k.split("_");
try {
//Return the enchanted book item if there's a number in the item name
final int level = Integer.parseInt(words[words.length-1]);
if(recipe.getResult().getType() != Material.ENCHANTED_BOOK) return null;
final EnchantmentStorageMeta meta = (EnchantmentStorageMeta)recipe.getResult().getItemMeta();
final Enchantment enchantment = EnchantmentWrapper.getByKey(NamespacedKey.minecraft(k.substring(0, k.lastIndexOf("_"))));
if(meta.hasStoredEnchant(enchantment) && meta.getStoredEnchantLevel(enchantment) == level) return item;
return null;
} catch(NumberFormatException e) {
//Return the item if the item name is valid
if(this.verify(recipe, Material.matchMaterial(k))) return item;
return null;
} catch(Exception e2) {
//Send an error message
Util.errorMsg(e2);
return null;
}
}
//Verifies that an item exists in the villager's trade
private boolean verify(final MerchantRecipe recipe, final Material material) {
return ((recipe.getResult().getType() == material) || (recipe.getIngredients().get(0).getType() == material));
}
}

View File

@ -0,0 +1,114 @@
package com.pretzel.dev.villagertradelimiter.listeners;
import com.google.common.collect.Iterables;
import com.pretzel.dev.villagertradelimiter.VillagerTradeLimiter;
import com.pretzel.dev.villagertradelimiter.data.Cooldown;
import com.pretzel.dev.villagertradelimiter.data.PlayerData;
import com.pretzel.dev.villagertradelimiter.settings.Settings;
import org.bukkit.Material;
import org.bukkit.entity.Villager;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.VillagerAcquireTradeEvent;
import org.bukkit.event.entity.VillagerCareerChangeEvent;
import org.bukkit.event.entity.VillagerReplenishTradeEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.MerchantRecipe;
import java.time.Instant;
import java.util.Date;
import java.util.List;
import java.util.UUID;
public class VillagerListener implements Listener {
private final VillagerTradeLimiter instance;
private final Settings settings;
/**
* @param instance The instance of VillagerTradeLimiter.java
* @param settings The settings instance
*/
public VillagerListener(final VillagerTradeLimiter instance, final Settings settings) {
this.instance = instance;
this.settings = settings;
}
/** Handles villager promotions */
@EventHandler
public void onVillagerPromotion(final VillagerAcquireTradeEvent event) {
//Gets the items in the trade
final MerchantRecipe recipe = event.getRecipe();
List<ItemStack> items = recipe.getIngredients();
items.add(recipe.getResult());
//Gets the disabled item list from config
List<String> disabledItems = instance.getCfg().getStringList("DisableItems");
//Checks each item if it should be removed from the trade list
for(ItemStack item : items) {
for(String disabledItem : disabledItems) {
if(disabledItem.equalsIgnoreCase(item.getType().name())) {
event.setCancelled(true);
return;
}
}
}
}
/** Handles villager profession change **/
@EventHandler
public void onVillagerChangeProfession(final VillagerCareerChangeEvent event) {
//Gets the new profession
final Villager.Profession profession = event.getProfession();
//Gets the disabled profession list from config
List<String> disabledProfessions = instance.getCfg().getStringList("DisableProfessions");
//Changes the new profession to none if disabled in config
for(String disabledProfession : disabledProfessions) {
if(disabledProfession.equalsIgnoreCase(profession.name())) {
event.setProfession(Villager.Profession.NONE);
return;
}
}
}
/** Handles villager restocks */
@EventHandler
public void onVillagerRestock(final VillagerReplenishTradeEvent event) {
if(!(event.getEntity() instanceof Villager)) return;
final Villager villager = (Villager)event.getEntity();
if(settings.shouldSkipNPC(villager)) return; //Skips NPCs
//Get the items involved in the restock
final MerchantRecipe recipe = event.getRecipe();
final ItemStack result = recipe.getResult();
ItemStack ingredient1 = Iterables.get(recipe.getIngredients(), 0, new ItemStack(Material.AIR));
ItemStack ingredient2 = Iterables.get(recipe.getIngredients(), 1, new ItemStack(Material.AIR));
final String type = settings.getType(result, ingredient1, ingredient2);
//Get the villager's data container
final UUID uuid = villager.getUniqueId();
final PlayerData villagerData = instance.getPlayerData().get(uuid);
if(villagerData == null) return;
//Get the time of the last trade, restock cooldown setting, and now
final String lastTradeStr = villagerData.getTradingCooldowns().get(type);
if(lastTradeStr == null) return;
String cooldownStr = instance.getCfg().getString("Restock", "0");
cooldownStr = instance.getCfg().getString("Overrides."+type+".Restock", cooldownStr);
final Date now = Date.from(Instant.now());
final Date lastTrade = Cooldown.parseTime(lastTradeStr);
if(lastTrade == null) return;
final long cooldown = Cooldown.parseCooldown(cooldownStr);
//Cancel the event if there is an active restock cooldown, otherwise remove the restock cooldown
if(now.getTime()/1000L >= lastTrade.getTime()/1000L + cooldown) {
villagerData.getTradingCooldowns().remove(type);
} else {
event.setCancelled(true);
}
}
}

View File

@ -1,23 +0,0 @@
package com.pretzel.dev.villagertradelimiter.nms;
import com.pretzel.dev.villagertradelimiter.lib.Util;
import org.bukkit.entity.Entity;
public class CraftEntity {
private final NMS nms;
private final Class<?> c;
public CraftEntity(final NMS nms) {
this.nms = nms;
this.c = nms.getCraftBukkitClass("entity.CraftEntity");
}
public Object getHandle(final Entity entity) {
try {
return nms.getMethod(this.c, "getHandle").invoke(this.c.cast(entity));
} catch (Exception e) {
Util.errorMsg(e);
return null;
}
}
}

View File

@ -1,44 +0,0 @@
package com.pretzel.dev.villagertradelimiter.nms;
import com.pretzel.dev.villagertradelimiter.lib.Util;
import org.bukkit.entity.Entity;
public class NBTContainer {
private final NMS nms;
private final Entity entity;
private final NBTTagCompound tag;
public NBTContainer(final NMS nms, final Entity entity) {
this.nms = nms;
this.entity = entity;
this.tag = this.loadTag();
}
public NBTTagCompound loadTag() {
final CraftEntity craftEntity = new CraftEntity(nms);
final NMSEntity nmsEntity = new NMSEntity(nms);
final Class<?> tgc;
if(nms.getVersion().compareTo("v1_17_R1") < 0)
tgc = nms.getNMSClass("server."+nms.getVersion()+".NBTTagCompound");
else
tgc = nms.getNMSClass("nbt.NBTTagCompound");
try {
final NBTTagCompound tag = new NBTTagCompound(nms, tgc.getDeclaredConstructor().newInstance());
nmsEntity.save(craftEntity.getHandle(this.entity), tag);
return tag;
} catch (Exception e) {
Util.errorMsg(e);
return null;
}
}
public void saveTag(final Entity entity, final NBTTagCompound tag) {
final CraftEntity craftEntity = new CraftEntity(nms);
final NMSEntity nmsEntity = new NMSEntity(nms);
nmsEntity.load(craftEntity.getHandle(entity), tag);
}
public NBTTagCompound getTag() {
return this.tag;
}
}

View File

@ -1,71 +0,0 @@
package com.pretzel.dev.villagertradelimiter.nms;
import com.pretzel.dev.villagertradelimiter.lib.Util;
public class NBTTagCompound {
private final NMS nms;
private final Class<?> c;
private final Object self;
public NBTTagCompound(final NMS nms, final Object self) {
this.nms = nms;
this.self = self;
this.c = self.getClass();
}
public Object get(final String key) {
try {
return this.nms.getMethod(this.c, "get", String.class).invoke(this.self, key);
} catch (Exception e) {
Util.errorMsg(e);
return null;
}
}
public String getString(final String key) {
try {
return (String)this.nms.getMethod(this.c, "getString", String.class).invoke(this.self, key);
} catch (Exception e) {
Util.errorMsg(e);
return null;
}
}
public int getInt(final String key) {
try {
return (int)this.nms.getMethod(this.c, "getInt", String.class).invoke(this.self, key);
} catch (Exception e) {
Util.errorMsg(e);
return Integer.MIN_VALUE;
}
}
public int[] getIntArray(final String key) {
try {
return (int[])this.nms.getMethod(this.c, "getIntArray", String.class).invoke(this.self, key);
} catch (Exception e) {
Util.errorMsg(e);
return null;
}
}
public NBTTagCompound getCompound(final String key) {
try {
return new NBTTagCompound(this.nms, this.nms.getMethod(this.self.getClass(), "getCompound", String.class).invoke(this.self, key));
} catch (Exception e) {
Util.errorMsg(e);
return null;
}
}
public void setInt(final String key, final int value) {
try {
this.nms.getMethod(this.c, "setInt", String.class, Integer.TYPE).invoke(this.self, key, value);
} catch (Exception e) {
Util.errorMsg(e);
}
}
public Object getSelf() { return this.self; }
public Class<?> getC() { return this.c; }
}

View File

@ -1,41 +0,0 @@
package com.pretzel.dev.villagertradelimiter.nms;
import com.pretzel.dev.villagertradelimiter.lib.Util;
public class NBTTagList
{
private final NMS nms;
private final Object self;
private final Class<?> c;
public NBTTagList(final NMS nms, final Object self) {
this.nms = nms;
this.self = self;
if(nms.getVersion().compareTo("v1_17_R1") < 0)
this.c = nms.getNMSClass("server."+nms.getVersion()+".NBTTagList");
else
this.c = nms.getNMSClass("nbt.NBTTagList");
}
public NBTTagCompound getCompound(final int index) {
try {
return new NBTTagCompound(this.nms, this.nms.getMethod(this.c, "getCompound", Integer.TYPE).invoke(this.self, index));
} catch (Exception e) {
Util.errorMsg(e);
return null;
}
}
public int size() {
try {
return (int)this.nms.getMethod(this.c, "size").invoke(this.self, new Object[0]);
} catch (Exception e) {
Util.errorMsg(e);
return -1;
}
}
public Object getSelf() {
return this.self;
}
}

View File

@ -1,82 +0,0 @@
package com.pretzel.dev.villagertradelimiter.nms;
import com.pretzel.dev.villagertradelimiter.lib.Util;
import java.lang.reflect.Method;
import java.util.HashMap;
public class NMS
{
private final HashMap<String, Class<?>> nmsClasses;
private final HashMap<String, Class<?>> bukkitClasses;
private final HashMap<String, Method> methods;
private final String version;
public NMS(final String version) {
this.nmsClasses = new HashMap<>();
this.bukkitClasses = new HashMap<>();
this.methods = new HashMap<>();
this.version = version;
}
public Class<?> getNMSClass(final String name) {
if(this.nmsClasses.containsKey(name)) {
return this.nmsClasses.get(name);
}
try {
Class<?> c = Class.forName("net.minecraft."+name);
this.nmsClasses.put(name, c);
return c;
} catch (Exception e) {
Util.errorMsg(e);
return this.nmsClasses.put(name, null);
}
}
public Class<?> getCraftBukkitClass(final String name) {
if(this.bukkitClasses.containsKey(name)) {
return this.bukkitClasses.get(name);
}
try {
Class<?> c = Class.forName("org.bukkit.craftbukkit." + this.version + "." + name);
this.bukkitClasses.put(name, c);
return c;
} catch (Exception e) {
Util.errorMsg(e);
return this.bukkitClasses.put(name, null);
}
}
public Method getMethod(final Class<?> invoker, final String name) throws NoSuchMethodException {
return this.getMethod(invoker, name, null, null);
}
public Method getMethod(final Class<?> invoker, final String name, final Class<?> type) throws NoSuchMethodException {
return this.getMethod(invoker, name, type, null);
}
public Method getMethod(final Class<?> invoker, final String name, final Class<?> type, final Class<?> type2) throws NoSuchMethodException {
if(this.methods.containsKey(name) && this.methods.get(name).getDeclaringClass().equals(invoker)) {
return this.methods.get(name);
}
Method method;
try {
if(type2 != null) {
method = invoker.getMethod(name, type, type2);
} else if(type != null) {
method = invoker.getMethod(name, type);
} else {
method = invoker.getMethod(name);
}
} catch (Exception e) {
Util.errorMsg(e);
return this.methods.put(name, null);
}
this.methods.put(name, method);
return method;
}
public String getVersion() { return this.version; }
}

View File

@ -1,38 +0,0 @@
package com.pretzel.dev.villagertradelimiter.nms;
import com.pretzel.dev.villagertradelimiter.lib.Util;
public class NMSEntity {
private final NMS nms;
private final Class<?> c;
public NMSEntity(final NMS nms) {
this.nms = nms;
if(nms.getVersion().compareTo("v1_17_R1") < 0)
this.c = nms.getNMSClass("server."+nms.getVersion()+".Entity");
else
this.c = nms.getNMSClass("world.entity.Entity");
}
public void save(final Object nmsEntity, final NBTTagCompound tag) {
try {
nms.getMethod(this.c, "save", tag.getC()).invoke(nmsEntity, tag.getSelf());
} catch (Exception e) {
Util.errorMsg(e);
}
}
public void load(final Object nmsEntity, final NBTTagCompound tag) {
try {
nms.getMethod(this.c, "load", tag.getC()).invoke(nmsEntity, tag.getSelf());
} catch (NoSuchMethodException e) {
try {
nms.getMethod(this.c, "f", tag.getC()).invoke(nmsEntity, tag.getSelf());
} catch (Exception e2) {
Util.errorMsg(e2);
}
} catch (Exception e3) {
Util.errorMsg(e3);
}
}
}

View File

@ -0,0 +1,246 @@
package com.pretzel.dev.villagertradelimiter.settings;
import com.google.common.base.Preconditions;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.plugin.Plugin;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
/*
* Code by tchristofferson
* https://github.com/tchristofferson/Config-Updater
*/
public class ConfigUpdater {
//Used for separating keys in the keyBuilder inside parseComments method
private static final char SEPARATOR = '.';
public static void update(Plugin plugin, String resourceName, File toUpdate, String... ignoredSections) throws IOException {
update(plugin, resourceName, toUpdate, Arrays.asList(ignoredSections));
}
public static void update(Plugin plugin, String resourceName, File toUpdate, List<String> ignoredSections) throws IOException {
Preconditions.checkArgument(toUpdate.exists(), "The toUpdate file doesn't exist!");
FileConfiguration defaultConfig = YamlConfiguration.loadConfiguration(new InputStreamReader(plugin.getResource(resourceName), StandardCharsets.UTF_8));
FileConfiguration currentConfig = YamlConfiguration.loadConfiguration(toUpdate);
Map<String, String> comments = parseComments(plugin, resourceName, defaultConfig);
Map<String, String> ignoredSectionsValues = parseIgnoredSections(toUpdate, currentConfig, comments, ignoredSections == null ? Collections.emptyList() : ignoredSections);
// will write updated config file "contents" to a string
StringWriter writer = new StringWriter();
write(defaultConfig, currentConfig, new BufferedWriter(writer), comments, ignoredSectionsValues);
String value = writer.toString(); // config contents
Path toUpdatePath = toUpdate.toPath();
if (!value.equals(new String(Files.readAllBytes(toUpdatePath), StandardCharsets.UTF_8))) { // if updated contents are not the same as current file contents, update
Files.write(toUpdatePath, value.getBytes(StandardCharsets.UTF_8));
}
}
private static void write(FileConfiguration defaultConfig, FileConfiguration currentConfig, BufferedWriter writer, Map<String, String> comments, Map<String, String> ignoredSectionsValues) throws IOException {
//Used for converting objects to yaml, then cleared
FileConfiguration parserConfig = new YamlConfiguration();
keyLoop: for (String fullKey : defaultConfig.getKeys(true)) {
String indents = KeyBuilder.getIndents(fullKey, SEPARATOR);
if (ignoredSectionsValues.isEmpty()) {
writeCommentIfExists(comments, writer, fullKey, indents);
} else {
for (Map.Entry<String, String> entry : ignoredSectionsValues.entrySet()) {
if (entry.getKey().equals(fullKey)) {
writer.write(entry.getValue() + "\n");
continue keyLoop;
} else if (KeyBuilder.isSubKeyOf(entry.getKey(), fullKey, SEPARATOR)) {
continue keyLoop;
} else {
writeCommentIfExists(comments, writer, fullKey, indents);
}
}
}
Object currentValue = currentConfig.get(fullKey);
if (currentValue == null)
currentValue = defaultConfig.get(fullKey);
String[] splitFullKey = fullKey.split("[" + SEPARATOR + "]");
String trailingKey = splitFullKey[splitFullKey.length - 1];
if (currentValue instanceof ConfigurationSection) {
writer.write(indents + trailingKey + ":");
if (!((ConfigurationSection) currentValue).getKeys(false).isEmpty())
writer.write("\n");
else
writer.write(" {}\n");
continue;
}
parserConfig.set(trailingKey, currentValue);
String yaml = parserConfig.saveToString();
yaml = yaml.substring(0, yaml.length() - 1).replace("\n", "\n" + indents);
String toWrite = indents + yaml + "\n";
parserConfig.set(trailingKey, null);
writer.write(toWrite);
}
String danglingComments = comments.get(null);
if (danglingComments != null)
writer.write(danglingComments);
writer.close();
}
//Returns a map of key comment pairs. If a key doesn't have any comments it won't be included in the map.
private static Map<String, String> parseComments(Plugin plugin, String resourceName, FileConfiguration defaultConfig) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(plugin.getResource(resourceName)));
Map<String, String> comments = new LinkedHashMap<>();
StringBuilder commentBuilder = new StringBuilder();
KeyBuilder keyBuilder = new KeyBuilder(defaultConfig, SEPARATOR);
String line;
while ((line = reader.readLine()) != null) {
String trimmedLine = line.trim();
//Only getting comments for keys. A list/array element comment(s) not supported
if (trimmedLine.startsWith("-")) {
continue;
}
if (trimmedLine.isEmpty() || trimmedLine.startsWith("#")) {//Is blank line or is comment
commentBuilder.append(trimmedLine).append("\n");
} else {//is a valid yaml key
keyBuilder.parseLine(trimmedLine);
String key = keyBuilder.toString();
//If there is a comment associated with the key it is added to comments map and the commentBuilder is reset
if (commentBuilder.length() > 0) {
comments.put(key, commentBuilder.toString());
commentBuilder.setLength(0);
}
//Remove the last key from keyBuilder if current path isn't a config section or if it is empty to prepare for the next key
if (!keyBuilder.isConfigSectionWithKeys()) {
keyBuilder.removeLastKey();
}
}
}
reader.close();
if (commentBuilder.length() > 0)
comments.put(null, commentBuilder.toString());
return comments;
}
private static Map<String, String> parseIgnoredSections(File toUpdate, FileConfiguration currentConfig, Map<String, String> comments, List<String> ignoredSections) throws IOException {
BufferedReader reader = new BufferedReader(new FileReader(toUpdate));
Map<String, String> ignoredSectionsValues = new LinkedHashMap<>(ignoredSections.size());
KeyBuilder keyBuilder = new KeyBuilder(currentConfig, SEPARATOR);
StringBuilder valueBuilder = new StringBuilder();
String currentIgnoredSection = null;
String line;
lineLoop : while ((line = reader.readLine()) != null) {
String trimmedLine = line.trim();
if (trimmedLine.isEmpty() || trimmedLine.startsWith("#"))
continue;
if (trimmedLine.startsWith("-")) {
for (String ignoredSection : ignoredSections) {
boolean isIgnoredParent = ignoredSection.equals(keyBuilder.toString());
if (isIgnoredParent || keyBuilder.isSubKeyOf(ignoredSection)) {
valueBuilder.append("\n").append(line);
continue lineLoop;
}
}
}
keyBuilder.parseLine(trimmedLine);
String fullKey = keyBuilder.toString();
//If building the value for an ignored section and this line is no longer a part of the ignored section,
// write the valueBuilder, reset it, and set the current ignored section to null
if (currentIgnoredSection != null && !KeyBuilder.isSubKeyOf(currentIgnoredSection, fullKey, SEPARATOR)) {
ignoredSectionsValues.put(currentIgnoredSection, valueBuilder.toString());
valueBuilder.setLength(0);
currentIgnoredSection = null;
}
for (String ignoredSection : ignoredSections) {
boolean isIgnoredParent = ignoredSection.equals(fullKey);
if (isIgnoredParent || keyBuilder.isSubKeyOf(ignoredSection)) {
if (valueBuilder.length() > 0)
valueBuilder.append("\n");
String comment = comments.get(fullKey);
if (comment != null) {
String indents = KeyBuilder.getIndents(fullKey, SEPARATOR);
valueBuilder.append(indents).append(comment.replace("\n", "\n" + indents));//Should end with new line (\n)
valueBuilder.setLength(valueBuilder.length() - indents.length());//Get rid of trailing \n and spaces
}
valueBuilder.append(line);
//Set the current ignored section for future iterations of while loop
//Don't set currentIgnoredSection to any ignoredSection sub-keys
if (isIgnoredParent)
currentIgnoredSection = fullKey;
break;
}
}
}
reader.close();
if (valueBuilder.length() > 0)
ignoredSectionsValues.put(currentIgnoredSection, valueBuilder.toString());
return ignoredSectionsValues;
}
private static void writeCommentIfExists(Map<String, String> comments, BufferedWriter writer, String fullKey, String indents) throws IOException {
String comment = comments.get(fullKey);
//Comments always end with new line (\n)
if (comment != null)
//Replaces all '\n' with '\n' + indents except for the last one
writer.write(indents + comment.substring(0, comment.length() - 1).replace("\n", "\n" + indents) + "\n");
}
//Input: 'key1.key2' Result: 'key1'
private static void removeLastKey(StringBuilder keyBuilder) {
if (keyBuilder.length() == 0)
return;
String keyString = keyBuilder.toString();
//Must be enclosed in brackets in case a regex special character is the separator
String[] split = keyString.split("[" + SEPARATOR + "]");
//Makes sure begin index isn't < 0 (error). Occurs when there is only one key in the path
int minIndex = Math.max(0, keyBuilder.length() - split[split.length - 1].length() - 1);
keyBuilder.replace(minIndex, keyBuilder.length(), "");
}
private static void appendNewLine(StringBuilder builder) {
if (builder.length() > 0)
builder.append("\n");
}
}

View File

@ -0,0 +1,120 @@
package com.pretzel.dev.villagertradelimiter.settings;
import org.bukkit.configuration.file.FileConfiguration;
/*
* Code by tchristofferson
* https://github.com/tchristofferson/Config-Updater
*/
public class KeyBuilder implements Cloneable {
private final FileConfiguration config;
private final char separator;
private final StringBuilder builder;
public KeyBuilder(FileConfiguration config, char separator) {
this.config = config;
this.separator = separator;
this.builder = new StringBuilder();
}
private KeyBuilder(KeyBuilder keyBuilder) {
this.config = keyBuilder.config;
this.separator = keyBuilder.separator;
this.builder = new StringBuilder(keyBuilder.toString());
}
public void parseLine(String line) {
line = line.trim();
String[] currentSplitLine = line.split(":");
//Checks keyBuilder path against config to see if the path is valid.
//If the path doesn't exist in the config it keeps removing last key in keyBuilder.
while (builder.length() > 0 && !config.contains(builder.toString() + separator + currentSplitLine[0])) {
removeLastKey();
}
//Add the separator if there is already a key inside keyBuilder
//If currentSplitLine[0] is 'key2' and keyBuilder contains 'key1' the result will be 'key1.' if '.' is the separator
if (builder.length() > 0)
builder.append(separator);
//Appends the current key to keyBuilder
//If keyBuilder is 'key1.' and currentSplitLine[0] is 'key2' the resulting keyBuilder will be 'key1.key2' if separator is '.'
builder.append(currentSplitLine[0]);
}
public String getLastKey() {
if (builder.length() == 0)
return "";
return builder.toString().split("[" + separator + "]")[0];
}
public boolean isEmpty() {
return builder.length() == 0;
}
//Checks to see if the full key path represented by this instance is a sub-key of the key parameter
public boolean isSubKeyOf(String parentKey) {
return isSubKeyOf(parentKey, builder.toString(), separator);
}
//Checks to see if subKey is a sub-key of the key path this instance represents
public boolean isSubKey(String subKey) {
return isSubKeyOf(builder.toString(), subKey, separator);
}
public static boolean isSubKeyOf(String parentKey, String subKey, char separator) {
if (parentKey.isEmpty())
return false;
return subKey.startsWith(parentKey)
&& subKey.substring(parentKey.length()).startsWith(String.valueOf(separator));
}
public static String getIndents(String key, char separator) {
String[] splitKey = key.split("[" + separator + "]");
StringBuilder builder = new StringBuilder();
for (int i = 1; i < splitKey.length; i++) {
builder.append(" ");
}
return builder.toString();
}
public boolean isConfigSection() {
String key = builder.toString();
return config.isConfigurationSection(key);
}
public boolean isConfigSectionWithKeys() {
String key = builder.toString();
return config.isConfigurationSection(key) && !config.getConfigurationSection(key).getKeys(false).isEmpty();
}
//Input: 'key1.key2' Result: 'key1'
public void removeLastKey() {
if (builder.length() == 0)
return;
String keyString = builder.toString();
//Must be enclosed in brackets in case a regex special character is the separator
String[] split = keyString.split("[" + separator + "]");
//Makes sure begin index isn't < 0 (error). Occurs when there is only one key in the path
int minIndex = Math.max(0, builder.length() - split[split.length - 1].length() - 1);
builder.replace(minIndex, builder.length(), "");
}
@Override
public String toString() {
return builder.toString();
}
@Override
protected KeyBuilder clone() {
return new KeyBuilder(this);
}
}

View File

@ -0,0 +1,65 @@
package com.pretzel.dev.villagertradelimiter.settings;
import com.pretzel.dev.villagertradelimiter.lib.Util;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.plugin.Plugin;
import java.io.File;
import java.io.IOException;
import java.io.Reader;
public class Lang {
private final FileConfiguration def;
private FileConfiguration cfg;
/**
* @param plugin The Bukkit/Spigot/Paper plugin instance
* @param reader The file reader for the default messages.yml file (located in the src/main/resources)
* @param path The file path for the active messages.yml file (located on the server in plugins/[plugin name])
*/
public Lang(final Plugin plugin, final Reader reader, final String path) {
//Gets the default values, puts them in a temp file, and loads them as a FileConfiguration
String[] defLines = Util.readFile(reader);
String def = "";
if(defLines == null) defLines = new String[0];
for(String line : defLines) def += line+"\n";
final File defFile = new File(path, "temp.yml");
Util.writeFile(defFile, def);
this.def = YamlConfiguration.loadConfiguration(defFile);
//Gets the active values and loads them as a FileConfiguration
File file = new File(path,"messages.yml");
try {
if(file.createNewFile()) Util.writeFile(file, def);
} catch (Exception e) {
Util.errorMsg(e);
}
this.cfg = null;
try {
ConfigUpdater.update(plugin, "messages.yml", file);
} catch (IOException e) {
Util.errorMsg(e);
}
this.cfg = YamlConfiguration.loadConfiguration(file);
defFile.delete();
}
/**
* @param key The key (or path) of the section in messages.yml (e.g, common.reloaded)
* @return The String value in messages.yml that is mapped to the given key
*/
public String get(final String key) {
return get(key, def.getString("help", ""));
}
/**
* @param key The key (or path) of the section in messages.yml (e.g, common.reloaded)
* @param def The default value to return if the key is not found
* @return The String value in messages.yml that is mapped to the given key, or the given default value if the key was not found
*/
public String get(final String key, final String def) {
return Util.replaceColors(this.cfg.getString(key, def));
}
}

View File

@ -0,0 +1,194 @@
package com.pretzel.dev.villagertradelimiter.settings;
import com.pretzel.dev.villagertradelimiter.VillagerTradeLimiter;
import com.pretzel.dev.villagertradelimiter.lib.Util;
import com.pretzel.dev.villagertradelimiter.wrappers.RecipeWrapper;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.enchantments.EnchantmentWrapper;
import org.bukkit.entity.Entity;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.EnchantmentStorageMeta;
import java.util.Arrays;
public class Settings {
private final VillagerTradeLimiter instance;
/** @param instance The instance of VillagerTradeLimiter.java */
public Settings(final VillagerTradeLimiter instance) { this.instance = instance; }
/**
* @param entity The entity to check the NPC status of
* @return True if the entity is an NPC and config is set to ignore NPCs
*/
public boolean shouldSkipNPC(final Entity entity) {
if(entity == null) return true;
if(instance.getCfg().getBoolean("IgnoreCitizens", true) && Util.isNPC(entity)) return true;
return instance.getCfg().getBoolean("IgnoreShopkeepers", true) && Util.isShopkeeper(entity);
}
/**
* @param recipe The wrapped recipe to fetch any overrides for
* @param key The key where the fetched value is stored in config.yml (e.g, DisableTrading)
* @param defaultValue The default boolean value to use if the key does not exist
* @return A boolean value that has the most specific value possible between the global setting and the overrides settings
*/
public boolean fetchBoolean(final RecipeWrapper recipe, String key, boolean defaultValue) {
boolean global = instance.getCfg().getBoolean(key, defaultValue);
final ConfigurationSection override = getOverride(recipe.getItemStack("buy"), recipe.getItemStack("sell"));
if(override != null) return override.getBoolean(key, global);
return global;
}
/**
* @param recipe The wrapped recipe to fetch any overrides for
* @param key The key where the fetched value is stored in config.yml (e.g, MaxDemand)
* @param defaultValue The default integer value to use if the key does not exist
* @return An integer value that has the most specific value possible between the global setting and the overrides settings
*/
public int fetchInt(final RecipeWrapper recipe, String key, int defaultValue) {
int global = instance.getCfg().getInt(key, defaultValue);
final ConfigurationSection override = getOverride(recipe.getItemStack("buy"), recipe.getItemStack("sell"));
if(override != null) return override.getInt(key, global);
return global;
}
/**
* @param recipe The wrapped recipe to fetch any overrides for
* @param key The key where the fetched value is stored in config.yml (e.g, MaxDiscount)
* @param defaultValue The default double value to use if the key does not exist
* @return A double value that has the most specific value possible between the global setting and the overrides settings
*/
public double fetchDouble(final RecipeWrapper recipe, String key, double defaultValue) {
double global = instance.getCfg().getDouble(key, defaultValue);
final ConfigurationSection override = getOverride(recipe.getItemStack("buy"), recipe.getItemStack("sell"));
if(override != null) return override.getDouble(key, global);
return global;
}
/**
* @param result The itemstack for the recipe's result
* @param ingredient1 The itemstack for the recipe's first ingredient
* @param ingredient2 The itemstack for the recipe's second ingredient
* @return The matched type of the item, if any
*/
public String getType(final ItemStack result, final ItemStack ingredient1, final ItemStack ingredient2) {
final String resultType = result.getType().name().toLowerCase();
final String ingredient1Type = ingredient1.getType().name().toLowerCase();
final String ingredient2Type = ingredient2.getType().name().toLowerCase();
final String defaultType;
if(result.getType() == Material.EMERALD) {
if(ingredient1.getType() == Material.BOOK || ingredient1.getType() == Material.AIR) {
defaultType = ingredient2Type;
} else {
defaultType = ingredient1Type;
}
} else {
defaultType = resultType;
}
if(result.getType() == Material.ENCHANTED_BOOK) {
final EnchantmentStorageMeta meta = (EnchantmentStorageMeta) result.getItemMeta();
if(meta == null) return defaultType;
for(Enchantment key : meta.getStoredEnchants().keySet()) {
if (key != null) {
final String itemType = key.getKey().getKey() +"_"+meta.getStoredEnchantLevel(key);
if(getItem(ingredient1, result, itemType) != null) return itemType;
}
}
return defaultType;
}
final ItemStack ingredient = (ingredient1.getType() == Material.AIR ? ingredient2 : ingredient1);
if(getItem(ingredient, result, resultType) != null) return resultType;
if(getItem(ingredient, result, ingredient1Type) != null) return ingredient1Type;
if(getItem(ingredient, result, ingredient2Type) != null) return ingredient2Type;
return defaultType;
}
/**
* @param buy The first ingredient of the recipe
* @param sell The result of the recipe
* @return The corresponding override config section for the recipe, if it exists, or null
*/
public ConfigurationSection getOverride(final ItemStack buy, ItemStack sell) {
final ConfigurationSection overrides = instance.getCfg().getConfigurationSection("Overrides");
if(overrides == null) return null;
for(final String override : overrides.getKeys(false)) {
final ConfigurationSection item = this.getItem(buy, sell, override);
if(item != null) return item;
}
return null;
}
/**
* @param buy The first ingredient of the recipe
* @param sell The result of the recipe
* @param key The key where the override settings are stored in config.yml
* @return The corresponding override config section for the recipe, if it exists, or null
*/
public ConfigurationSection getItem(final ItemStack buy, final ItemStack sell, String key) {
final ConfigurationSection item = instance.getCfg().getConfigurationSection("Overrides."+key);
if(item == null) return null;
String match = "both";
if(!key.contains("_")) {
//Return the item if the item name is valid
if(verify(buy, sell, match, Material.matchMaterial(key))) return item;
return null;
}
String[] words = key.split("_");
if(words[words.length-1].equalsIgnoreCase("left")) {
match = "left";
key = key.replace("_left", "");
words = Arrays.copyOf(words, words.length-1);
} else if(words[words.length-1].equalsIgnoreCase("right")) {
match = "right";
key = key.replace("_right", "");
words = Arrays.copyOf(words, words.length-1);
}
try {
//Return the enchanted book item if there's a number in the item name
final int level = Integer.parseInt(words[words.length-1]);
if(sell.getType() == Material.ENCHANTED_BOOK) {
final EnchantmentStorageMeta meta = (EnchantmentStorageMeta) sell.getItemMeta();
final Enchantment enchantment = EnchantmentWrapper.getByKey(NamespacedKey.minecraft(key.substring(0, key.lastIndexOf("_"))));
if(meta == null || enchantment == null) return null;
if(meta.hasStoredEnchant(enchantment) && meta.getStoredEnchantLevel(enchantment) == level) return item;
}
} catch(NumberFormatException e) {
//Return the item if the item name is valid
if(verify(buy, sell, match, Material.matchMaterial(key))) return item;
return null;
} catch(Exception e2) {
//Send an error message
Util.errorMsg(e2);
}
return null;
}
/**
* @param buy The first ingredient of the recipe
* @param sell The result of the recipe
* @param material The material to compare the recipe against
* @return True if a recipe matches an override section, false otherwise
*/
private boolean verify(final ItemStack buy, final ItemStack sell, final String match, final Material material) {
if(match.equals("left")) {
if(buy == null) return false;
return buy.getType().equals(material);
} else if(match.equals("right")) {
if(sell == null) return false;
return sell.getType().equals(material);
}
if(buy == null && sell == null) return false;
if(buy == null) return sell.getType() == material;
if(sell == null) return buy.getType() == material;
return ((buy.getType() == material) || (sell.getType() == material));
}
}

View File

@ -0,0 +1,48 @@
package com.pretzel.dev.villagertradelimiter.wrappers;
import com.pretzel.dev.villagertradelimiter.lib.Util;
import de.tr7zw.changeme.nbtapi.NBTCompound;
public class GossipWrapper {
private final NBTCompound gossip;
public enum GossipType {
MAJOR_NEGATIVE(-5),
MINOR_NEGATIVE(-1),
TRADING(1),
MINOR_POSITIVE(1),
MAJOR_POSITIVE(5),
OTHER(0);
private final int weight;
GossipType(int weight) { this.weight = weight; }
int getWeight() { return this.weight; }
}
/** @param gossip The NBTCompound that contains the villager's NBT data of the gossip */
public GossipWrapper(final NBTCompound gossip) { this.gossip = gossip; }
/** @return The GossipType of this gossip: MAJOR_NEGATIVE, MINOR_NEGATIVE, TRADING, MINOR_POSITIVE, MAJOR_POSITIVE, or OTHER if not found */
public GossipType getType() {
try {
return GossipType.valueOf(gossip.getString("Type").toUpperCase());
} catch (IllegalArgumentException e) {
return GossipType.OTHER;
}
}
/**
* @param isOld Whether the server is older than 1.16 or not. Minecraft changed how UUID's are represented in 1.16
* @return A string representation of the target UUID, for use when matching the target UUID to a player's UUID
*/
public String getTargetUUID(final boolean isOld) {
//BEFORE 1.16 (< 1.16)
if(isOld) return gossip.getLong("TargetMost")+";"+gossip.getLong("TargetLeast");
//AFTER 1.16 (>= 1.16)
return Util.intArrayToString(gossip.getIntArray("Target"));
}
/** @return The strength of this gossip, which is a value between 0 and: 25, 100, or 200, depending on the gossip type */
public int getValue() { return gossip.getInteger("Value"); }
}

View File

@ -0,0 +1,35 @@
package com.pretzel.dev.villagertradelimiter.wrappers;
import de.tr7zw.changeme.nbtapi.NBTCompound;
import org.bukkit.inventory.ItemStack;
public class IngredientWrapper {
private final NBTCompound recipe;
private final String key;
private final ItemStack itemStack;
/**
* @param recipe The NBTCompound that contains the recipe's NBT data of the ingredient
* @param key The key under which the recipe is located
*/
public IngredientWrapper(final NBTCompound recipe, final String key) {
this.recipe = recipe;
this.key = key;
this.itemStack = getItemStack();
}
/** @return The {@link ItemStack} representing the data in the recipe */
public ItemStack getItemStack() {
return recipe.getItemStack(key);
}
/** @param itemStack The {@link ItemStack} which will replace the item in the recipe */
public void setItemStack(final ItemStack itemStack) {
recipe.setItemStack(key, itemStack);
}
/** Resets the material ID and the amount of this ingredient to default values */
public void reset() {
setItemStack(itemStack);
}
}

View File

@ -0,0 +1,36 @@
package com.pretzel.dev.villagertradelimiter.wrappers;
import com.pretzel.dev.villagertradelimiter.lib.Util;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import java.util.UUID;
public class PlayerWrapper {
private final OfflinePlayer player;
/** @param player The offline player that this wrapper wraps */
public PlayerWrapper(final OfflinePlayer player) { this.player = player; }
/**
* @param isOld Whether the server is older than 1.16 or not. Minecraft changed how UUID's are represented in 1.16
* @return A string representation of the player's UUID, for use when matching the player's UUID to a gossip's target UUID
*/
public String getUUID(final boolean isOld) {
final UUID uuid = player.getUniqueId();
//BEFORE 1.16 (< 1.16)
if(isOld) return uuid.getMostSignificantBits()+";"+uuid.getLeastSignificantBits();
//AFTER 1.16 (>= 1.16)
final String uuidString = uuid.toString().replace("-", "");
int[] intArray = new int[4];
for(int i = 0; i < 4; i++) {
intArray[i] = (int)Long.parseLong(uuidString.substring(8*i, 8*(i+1)), 16);
}
return Util.intArrayToString(intArray);
}
/** @return The regular, online player of this wrapper's offline player, or null if the player is not online */
public Player getPlayer() { return player.getPlayer(); }
}

View File

@ -0,0 +1,79 @@
package com.pretzel.dev.villagertradelimiter.wrappers;
import de.tr7zw.changeme.nbtapi.NBTCompound;
import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;
import java.util.Arrays;
public class RecipeWrapper {
//A list of all the items with a default MaxUses of 12 and 3, respectively
private static final Material[] MAX_USES_12 = new Material[]{Material.IRON_HELMET, Material.IRON_CHESTPLATE, Material.IRON_LEGGINGS, Material.IRON_BOOTS, Material.IRON_INGOT, Material.BELL, Material.CHAINMAIL_HELMET, Material.CHAINMAIL_CHESTPLATE, Material.CHAINMAIL_LEGGINGS, Material.CHAINMAIL_BOOTS, Material.LAVA_BUCKET, Material.DIAMOND, Material.SHIELD, Material.RABBIT_STEW, Material.DRIED_KELP_BLOCK, Material.SWEET_BERRIES, Material.MAP, Material.FILLED_MAP, Material.COMPASS, Material.ITEM_FRAME, Material.GLOBE_BANNER_PATTERN, Material.WHITE_BANNER, Material.LIGHT_GRAY_BANNER, Material.GRAY_BANNER, Material.BLACK_BANNER, Material.BROWN_BANNER, Material.ORANGE_BANNER, Material.YELLOW_BANNER, Material.LIME_BANNER, Material.GREEN_BANNER, Material.CYAN_BANNER, Material.BLUE_BANNER, Material.LIGHT_BLUE_BANNER, Material.PURPLE_BANNER, Material.MAGENTA_BANNER, Material.PINK_BANNER, Material.RED_BANNER, Material.WHITE_BED, Material.LIGHT_GRAY_BED, Material.GRAY_BED, Material.BLACK_BED, Material.BROWN_BED, Material.ORANGE_BED, Material.YELLOW_BED, Material.LIME_BED, Material.GREEN_BED, Material.CYAN_BED, Material.BLUE_BED, Material.LIGHT_BLUE_BED, Material.PURPLE_BED, Material.MAGENTA_BED, Material.PINK_BED, Material.RED_BED, Material.REDSTONE, Material.GOLD_INGOT, Material.LAPIS_LAZULI, Material.RABBIT_FOOT, Material.GLOWSTONE, Material.SCUTE, Material.GLASS_BOTTLE, Material.ENDER_PEARL, Material.NETHER_WART, Material.EXPERIENCE_BOTTLE, Material.PUMPKIN, Material.PUMPKIN_PIE, Material.MELON, Material.COOKIE, Material.CAKE, Material.SUSPICIOUS_STEW, Material.GOLDEN_CARROT, Material.GLISTERING_MELON_SLICE, Material.CAMPFIRE, Material.TROPICAL_FISH, Material.PUFFERFISH, Material.BIRCH_BOAT, Material.ACACIA_BOAT, Material.OAK_BOAT, Material.DARK_OAK_BOAT, Material.SPRUCE_BOAT, Material.JUNGLE_BOAT, Material.ARROW, Material.FLINT, Material.STRING, Material.TRIPWIRE_HOOK, Material.TIPPED_ARROW, Material.LEATHER_HELMET, Material.LEATHER_CHESTPLATE, Material.LEATHER_LEGGINGS, Material.LEATHER_BOOTS, Material.LEATHER, Material.RABBIT_HIDE, Material.LEATHER_HORSE_ARMOR, Material.SADDLE, Material.BOOK, Material.ENCHANTED_BOOK, Material.BOOKSHELF, Material.INK_SAC, Material.GLASS, Material.WRITABLE_BOOK, Material.CLOCK, Material.NAME_TAG, Material.QUARTZ, Material.QUARTZ_PILLAR, Material.QUARTZ_BLOCK, Material.TERRACOTTA, Material.WHITE_TERRACOTTA, Material.LIGHT_GRAY_TERRACOTTA, Material.GRAY_TERRACOTTA, Material.BLACK_TERRACOTTA, Material.BROWN_TERRACOTTA, Material.ORANGE_TERRACOTTA, Material.YELLOW_TERRACOTTA, Material.LIME_TERRACOTTA, Material.GREEN_TERRACOTTA, Material.CYAN_TERRACOTTA, Material.BLUE_TERRACOTTA, Material.LIGHT_BLUE_TERRACOTTA, Material.PURPLE_TERRACOTTA, Material.MAGENTA_TERRACOTTA, Material.PINK_TERRACOTTA, Material.RED_TERRACOTTA, Material.WHITE_GLAZED_TERRACOTTA, Material.LIGHT_GRAY_GLAZED_TERRACOTTA, Material.GRAY_GLAZED_TERRACOTTA, Material.BLACK_GLAZED_TERRACOTTA, Material.BROWN_GLAZED_TERRACOTTA, Material.ORANGE_GLAZED_TERRACOTTA, Material.YELLOW_GLAZED_TERRACOTTA, Material.LIME_GLAZED_TERRACOTTA, Material.GREEN_GLAZED_TERRACOTTA, Material.CYAN_GLAZED_TERRACOTTA, Material.BLUE_GLAZED_TERRACOTTA, Material.LIGHT_BLUE_GLAZED_TERRACOTTA, Material.PURPLE_GLAZED_TERRACOTTA, Material.MAGENTA_GLAZED_TERRACOTTA, Material.PINK_GLAZED_TERRACOTTA, Material.RED_GLAZED_TERRACOTTA, Material.SHEARS, Material.PAINTING, Material.STONE_AXE, Material.STONE_SHOVEL, Material.STONE_PICKAXE, Material.STONE_HOE};
private static final Material[] MAX_USES_3 = new Material[]{Material.DIAMOND_HELMET, Material.DIAMOND_CHESTPLATE, Material.DIAMOND_LEGGINGS, Material.DIAMOND_BOOTS, Material.DIAMOND_SWORD, Material.DIAMOND_AXE, Material.DIAMOND_SHOVEL, Material.DIAMOND_PICKAXE, Material.DIAMOND_HOE, Material.IRON_SWORD, Material.IRON_AXE, Material.IRON_SHOVEL, Material.IRON_PICKAXE, Material.FISHING_ROD, Material.BOW, Material.CROSSBOW};
private final NBTCompound recipe;
private final IngredientWrapper ingredient1;
private final IngredientWrapper ingredient2;
private final IngredientWrapper result;
private final int specialPrice;
/** @param recipe The NBTCompound that contains the villager's NBT data of the recipe */
public RecipeWrapper(final NBTCompound recipe) {
this.recipe = recipe;
this.ingredient1 = new IngredientWrapper(recipe, "buy");
this.ingredient2 = new IngredientWrapper(recipe, "buyB");
this.result = new IngredientWrapper(recipe, "sell");
this.specialPrice = getSpecialPrice();
}
/** @param demand The demand, which increases prices if you buy too often. Negative values are ignored. */
public void setDemand(int demand) { recipe.setInteger("demand", demand); }
/** @param specialPrice The discount, which is added to the base price. A negative value will decrease the price, and a positive value will increase the price. */
public void setSpecialPrice(int specialPrice) { recipe.setInteger("specialPrice", specialPrice); }
/** @param maxUses The maximum number of times a player can make a trade before the villager restocks */
public void setMaxUses(int maxUses) { recipe.setInteger("maxUses", maxUses); }
/** Resets the recipe back to its default state */
public void reset() {
this.setSpecialPrice(this.specialPrice);
this.ingredient1.reset();
this.ingredient2.reset();
this.result.reset();
int maxUses = 16;
Material buyMaterial = recipe.getItemStack("buy").getType();
Material sellMaterial = recipe.getItemStack("sell").getType();
if(Arrays.asList(MAX_USES_12).contains(buyMaterial) || Arrays.asList(MAX_USES_12).contains(sellMaterial)) {
maxUses = 12;
} else if(Arrays.asList(MAX_USES_3).contains(buyMaterial) || Arrays.asList(MAX_USES_3).contains(sellMaterial)) {
maxUses = 3;
}
setMaxUses(maxUses);
}
/** @return The wrapper for the first ingredient */
public IngredientWrapper getIngredient1() { return ingredient1; }
/** @return The wrapper for the second ingredient */
public IngredientWrapper getIngredient2() { return ingredient2; }
/** @return The wrapper for the result */
public IngredientWrapper getResult() { return result; }
/** @return The demand for this recipe (increases the price when above 0) */
public int getDemand() { return recipe.getInteger("demand"); }
/** @return The price multiplier for this recipe (controls how strongly gossips, demand, etc. affect the price) */
public float getPriceMultiplier() { return recipe.getFloat("priceMultiplier"); }
/** @return The discount, which is added to the base price. A negative value will decrease the price, and a positive value will increase the price. */
public int getSpecialPrice() { return recipe.getInteger("specialPrice"); }
/** @return The maximum number of times a player can make a trade before the villager restocks */
public int getMaxUses() { return recipe.getInteger("maxUses"); }
/** @return The ItemStack representation of an ingredient or the result */
public ItemStack getItemStack(final String key) { return recipe.getItemStack(key); }
}

View File

@ -0,0 +1,90 @@
package com.pretzel.dev.villagertradelimiter.wrappers;
import de.tr7zw.changeme.nbtapi.NBTCompound;
import de.tr7zw.changeme.nbtapi.NBTCompoundList;
import de.tr7zw.changeme.nbtapi.NBTEntity;
import de.tr7zw.changeme.nbtapi.iface.ReadWriteNBT;
import org.bukkit.entity.Villager;
import org.bukkit.inventory.ItemStack;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.util.ArrayList;
import java.util.List;
public class VillagerWrapper {
private final Villager villager;
private final NBTEntity entity;
private final ItemStack[] contents;
/** @param villager The Villager to store in this wrapper */
public VillagerWrapper(final Villager villager) {
this.villager = villager;
this.entity = new NBTEntity(villager);
this.contents = new ItemStack[villager.getInventory().getContents().length];
for(int i = 0; i < this.contents.length; i++) {
ItemStack item = villager.getInventory().getItem(i);
this.contents[i] = (item == null ? null : item.clone());
}
}
/** @return a list of wrapped recipes for the villager */
public List<RecipeWrapper> getRecipes() {
final List<RecipeWrapper> recipes = new ArrayList<>();
//Add the recipes from the villager's NBT data into a list of wrapped recipes
final NBTCompound offers = entity.getCompound("Offers");
if(offers == null) return recipes;
final NBTCompoundList nbtRecipes = offers.getCompoundList("Recipes");
for(ReadWriteNBT nbtRecipe : nbtRecipes) {
recipes.add(new RecipeWrapper((NBTCompound)nbtRecipe));
}
return recipes;
}
/** @return A list of wrapped gossips for the villager */
private List<GossipWrapper> getGossips() {
final List<GossipWrapper> gossips = new ArrayList<>();
if(!entity.hasTag("Gossips")) return gossips;
//Add the gossips from the villager's NBT data into a list of wrapped gossips
final NBTCompoundList nbtGossips = entity.getCompoundList("Gossips");
for(ReadWriteNBT nbtGossip : nbtGossips) {
gossips.add(new GossipWrapper((NBTCompound) nbtGossip));
}
return gossips;
}
/**
* @param villager The wrapped villager that contains the gossips
* @param player The wrapped player that the gossips are about
* @param isOld Whether the server is older than 1.16 or not. Minecraft changed how UUID's are represented in 1.16
* @return the total reputation (from gossips) for a player
*/
public int getTotalReputation(@NonNull final VillagerWrapper villager, @NonNull final PlayerWrapper player, final boolean isOld) {
int totalReputation = 0;
final String playerUUID = player.getUUID(isOld);
final List<GossipWrapper> gossips = villager.getGossips();
for(GossipWrapper gossip : gossips) {
final GossipWrapper.GossipType type = gossip.getType();
if(type == null || type == GossipWrapper.GossipType.OTHER) continue;
final String targetUUID = gossip.getTargetUUID(isOld);
if(targetUUID.equals(playerUUID)) {
totalReputation += gossip.getValue() * type.getWeight();
}
}
return totalReputation;
}
/** Resets the villager's NBT data to default */
public void reset() {
//Reset the recipes back to their default ingredients, MaxUses, and discounts
for(RecipeWrapper recipe : this.getRecipes()) {
recipe.reset();
}
this.villager.getInventory().clear();
this.villager.getInventory().setContents(this.contents);
}
}

View File

@ -1,6 +1,5 @@
#---------------------------------------------------------------------------------#
# VTL ~ VillagerTradeLimiter #
# Version: 1.2.1 #
# By: PretzelJohn #
#---------------------------------------------------------------------------------#
@ -9,32 +8,96 @@
# This helps me keep track of what server versions are being used. Please leave this set to true.
bStats: true
# Set this to true if you want to completely disable ALL villager trading.
DisableTrading: false
# Database connection settings
database:
mysql: false
host: 127.0.0.1
port: 3306
database: villagertradelimiter
username: root
password: root
encoding: utf8
useSSL: false
# Add world names for worlds that you want to have unaltered, vanilla villager trading in. Set to [] to disable this feature.
DisableWorlds:
- world_nether
- world_the_end
# Ignore Citizens NPCs, and/or Shopkeepers NPCs if true
IgnoreCitizens: true
IgnoreShopkeepers: true
# Ignore interactions when the player is holding one of these item types (e.g. spawn_egg, name_tag)
# Without disabling nametag, you cannot rename a villager with a profession. Do not remove name_tag if you want to retain vanilla behavior.
# Ghast spawn egg is added to add compatibility with Safarinet plugin. If your server doesn't give ghast egg to noromal players you can ignore it.
# Set to [] to disable this feature.
IgnoreHeldItems:
- "name_tag"
- "ghast_spawn_egg"
# Add world names for worlds that you want to completely disable ALL villager trading. Set to [] to disable this feature.
DisableTrading:
- world_nether
- world_the_end
# Add profession names that you want to prevent villagers from acquiring
DisableProfessions: []
# Add item names that you want to prevent villagers from offering as trades.
# This is a permanent change. The items can't be re-added to the villager's trades.
DisableItems: []
# The maximum level of the "Hero of the Village" (HotV) effect that a player can have. This limits HotV price decreases.
# * Set to -1 to disable this feature and keep vanilla behavior.
# * Set to a number between 0 and 5 to set the maximum HotV effect level players can have
MaxHeroLevel: 1
# For more information, see https://minecraft.fandom.com/wiki/Hero_of_the_Village#Price_decrement
MaxHeroLevel: -1
# The maximum discount (%) you can get from trading/healing zombie villagers. This limits reputation-based price decreases.
# * Set to -1.0 to disable this feature and keep vanilla behavior
# * Set to a number between 0.0 and 1.0 to set the maximum discount a player can get. (NOTE: 30% = 0.3)
# * Set to a number between 0.0 and 1.0 to limit the maximum discount a player can get. (NOTE: 30% = 0.3)
# * Set to a number above 1.0 to increase the maximum discount a player can get. (NOTE: 250% = 2.5)
MaxDiscount: 0.3
# The maximum demand for all items. This limits demand-based price increases.
# * Set to -1 to disable this feature and keep vanilla behavior
# * Set to 0 or higher to set the maximum demand for all items
# WARNING: The previous demand cannot be recovered if it was higher than the MaxDemand.
# For more information, see https://minecraft.fandom.com/wiki/Trading#Economics
MaxDemand: -1
# The maximum number of times a player can make any trade before a villager is out of stock.
# * Set to -1 to disable this feature and keep vanilla behavior
# * Set to 0 or higher to change the maximum number of uses for all items
# For more information, see https://minecraft.fandom.com/el/wiki/Trading#Java_Edition
MaxUses: -1
# The per-player, per-trade cooldown in real-world time.
# After a player makes a trade <MaxUses> times, the trade will be disabled for the player until the cooldown is over.
# * Set to 0 to disable this feature and keep vanilla behavior
# * Set to a number and interval to add a per-player, per-trade cooldown for all trades (see below)
# A valid cooldown follows the <Number><Interval> format, such as 7d or 30s. The valid intervals are:
# * s = seconds (e.g. 30s)
# * m = minutes (e.g. 10m)
# * h = hours (e.g. 1h)
# * d = days (e.g. 3d)
# * w = weeks (e.g. 2w)
Cooldown: 0
# The per-villager, per-trade cooldown in real-world time.
# This is the same as Cooldown, but applies to a villager's restocking function
# * Set to 0 to disable this feature and keep vanilla behavior
# * Set to a number and interval to add a per-villager, per-trade cooldown for all trades (see below)
Restock: 0
#-------------------------------- PER-ITEM SETTINGS --------------------------------#
# Override the global settings for individual items. To disable, set like this --> Overrides: none
# To enable, add items below!
# * Enchanted books must follow the format: enchantment_name_level (ex: mending_1)
# * All other items must follow the format: item_name (ex: stone_bricks)
# For each item you add, you can override MaxDiscount and/or MaxDemand.
# For each item you add, you can disable the trade (set Disabled: true), or override MaxDiscount and/or MaxDemand.
Overrides:
mending_1:
MaxDiscount: 0.1
@ -44,7 +107,23 @@ Overrides:
name_tag:
MaxDiscount: -1.0
MaxDemand: 60
MaxUses: 2
Cooldown: 7d
Item1:
Material: "book"
Amount: 64
Item2:
Material: "ink_sac"
Amount: 48
Result:
Material: "name_tag"
Amount: 2
flint_left:
MaxUses: 8
flint_right:
MaxUses: 1
clock:
MaxDemand: 12
paper:
MaxDiscount: 0.1
MaxUses: 1
Restock: 1h

View File

@ -0,0 +1,20 @@
# Command help messages (description on hover). Format: [Permission]Usage;Description
help: |-
[villagertradelimiter.use]&a----- VTL Commands -----
[villagertradelimiter.use]&b/vtl;&fshows this help message
[villagertradelimiter.reload]&b/vtl reload;&freloads config.yml and messages.yml
[villagertradelimiter.see]&b/vtl see <player>;&fshows the adjusted trades for another player
# Common messages:
common:
reloaded: "&eVillagerTradeLimiter (VTL) &ahas been reloaded!"
noconsole: "&cYou cannot use this command from the console."
noargs: "&cNot enough arguments! For command usage, see &b/vtl"
# Messages for the /vtl see <player> command:
see:
success: "&aShowing the adjusted trades for &b{player}&a..."
noplayer: "&cInvalid player &b{player}&c! Please use a valid player's name."
novillager: "&cInvalid entity! Please look at the villager you want to check."
noworld: "&cVTL is disabled in this world!"

View File

@ -1,7 +1,7 @@
name: VillagerTradeLimiter
author: PretzelJohn
main: com.pretzel.dev.villagertradelimiter.VillagerTradeLimiter
version: 1.2.1
version: 1.6.4
api-version: 1.14
commands:
@ -16,10 +16,18 @@ permissions:
children:
villagertradelimiter.use: true
villagertradelimiter.reload: true
villagertradelimiter.see: true
villagertradelimiter.invsee: true
default: op
villagertradelimiter.use:
description: Allows players to use VillagerTradeLimiter.
default: op
villagertradelimiter.reload:
description: Allows players to reload config.yml.
default: op
villagertradelimiter.see:
description: Allows players to see the trades for another player
default: op
villagertradelimiter.invsee:
description: Allows players to see inventory of a villager
default: op