Changed how items are getting defined in the config.

Internally this new format uses Bukkit's item serialization for parsing
the item data, which allows it to support the specification of arbitrary
item data and hopefully not require any major updating/maintenance for
future minecraft versions. At the same time it tries to stay (slightly)
more user-friendly than Bukkit's item serialization by omitting any data
that can be restored by the plugin, by avoiding one level of nesting
between the item type and item data, by translating ampersand ('&')
color codes in display name and lore, and by offering a compact
representation for specifying an item only by its type.

This change also allows a more detailed specification of some of the
editor button items. However, many editor buttons still miss
corresponding config settings. Also keep in mind that the display name
and lore for these button items get specified via corresponding message
settings, so any specified item display name and lore will get replaced
by that.

When checking if an in-game item matches the item data specified in the
config, only the specified data gets compared. So this does not check
for item data equality, but instead the checked item is able to contain
additional data but still get matched (like before, but previously this
was limited to checking display name and lore).

The previous item data gets automatically migrated to the new format
(config version 2).

Other changes:
* Internal: Moved config migrations into a separate package.
* Internal: Moved some function(s) into ConfigUtils.
* Internal: Slightly changed how the plugin checks whether the high
currency is enabled.
* Internal: Avoiding ItemStack#hasItemMeta calls before getting an
item's ItemMeta, since this might be heavier than simply getting the
ItemMeta directly and performing only the relevant checks on that.
Internally ItemStack#hasItemMeta checks emptiness for all item
attributes and might (for CraftItemStacks) even first copy all the
item's data into a new ItemMeta object. And even if the item actually
has no data (Bukkit ItemStack with null ItemMeta), ItemStack#getItemMeta
will simply create a new empty ItemMeta object without having to copy
any data, so this is still a similarly lightweight operation anyways.
* Internal: Added ItemData tests. Unfortunately this requires
CraftBukkit as test dependency.
master
blablubbabc 2019-08-18 04:51:49 +02:00
parent 490ceb42fb
commit 37b68085f8
28 changed files with 1625 additions and 309 deletions

5
.gitignore vendored
View File

@ -39,3 +39,8 @@ Desktop.ini
# Mac crap
.DS_Store
############
**/logs

View File

@ -8,6 +8,10 @@ Date format: (YYYY-MM-DD)
### Supported MC versions: 1.14.4
* Bumped Bukkit dependency from 1.14.3 to 1.14.4.
* Added: Changed how items are getting defined in the config. Internally this new format uses Bukkit's item serialization for parsing the item data, which allows it to support the specification of arbitrary item data and hopefully not require any major maintenance for future minecraft versions. At the same time it tries to stay (slightly) more user-friendly than Bukkit's item serialization by omitting any data that can be restored by the plugin, by avoiding one level of nesting between the item type and item data, by translating ampersand ('&') color codes in display name and lore, and by offering a compact representation for specifying an item only by its type.
* This change also allows a more detailed specification of some of the editor button items. However, many editor buttons still miss corresponding config settings. Also keep in mind that the display name and lore for these button items get specified via corresponding message settings, so any specified item display name and lore will get replaced by that.
* When checking if an in-game item matches the item data specified in the config, only the specified data gets compared. So this does not check for item data equality, but instead the checked item is able to contain additional data but still get matched (like before, but previously this was limited to checking display name and lore).
* The previous item data gets automatically migrated to the new format (config version 2).
* Changed: All priorities and ignoring of cancelled events were reconsidered.
* Event handlers potentially modifying or canceling the event and which don't depend on other plugins' event handling are called early (LOW or LOWEST), so that other plugins can react to / ignore those modified or canceled events.
* All event handlers which simply cancel some event use the LOW priority now and more consistently ignore the event if already cancelled. Previously they mostly used NORMAL priority.
@ -24,7 +28,7 @@ Date format: (YYYY-MM-DD)
* Interactions with regular villagers (blocking, hiring) are handled at HIGH priority (like before), so that hiring can be skipped if some other plugin has cancelled the interaction.
* Changed: Replaced the 'bypass-shop-interaction-blocking' setting (default: false) with the new setting 'check-shop-interaction-result' (default: false).
* Changed: The new 'check-shop-interaction-result' setting also applies to sign shops now.
* Changed: Added a new 'loadbefore' entry for GriefPrevention as workaround to fix some compatibility issue caused by our changed event priorities and GriefPrevention reacting to entity interactions at LOWEST priority.
* Fixed: Also cancelling the PlayerInteractAtEntityEvent for shopkeeper entity interactions.
* Changed: When forcing an entity to spawn, the pitch and yaw of the expected and actual spawn location are ignored now. This avoids a warning message for some entity types (such as shulkers), which always spawn with fixed pitch and yaw.
* Changed: Some entity attributes are setup prior to entity spawning now (such as metadata, non-persist flag and name (if it has/uses one)). This should help other plugins to identify Shopkeeper entities during spawning.
@ -36,12 +40,17 @@ Date format: (YYYY-MM-DD)
* API: Minor javadoc changes.
* API/Fixed: ShopkeepersAPI was missing getDefaultUITypes.
* Internal: Avoiding ItemStack#hasItemMeta calls before getting an item's ItemMeta, since this might be heavier than simply getting the ItemMeta directly and performing only the relevant checks on that. Internally ItemStack#hasItemMeta checks emptiness for all item attributes and might (for CraftItemStacks) even first copy all the item's data into a new ItemMeta object. And even if the item actually has no data (Bukkit ItemStack with null ItemMeta), ItemStack#getItemMeta will simply create a new empty ItemMeta object without having to copy any data, so this is still a similarly lightweight operation anyways.
* Internal: Made all priorities and ignoring of cancelled events explicit.
* Internal: Moved code for checking chest access into util package.
* Internal: Moved config migrations into a separate package.
* Internal: Moved some functions into ConfigUtils.
* Internal: Slightly changed how the plugin checks whether the high currency is enabled.
* Internal: Metrics will also report now whether the settings 'check-shop-interaction-result', 'bypass-spawn-blocking', 'enable-spawn-verifier' and 'increment-villager-statistics' are used.
* Internal: Skipping shopkeeper spawning requests for unloaded worlds (should usually not be the case, but we guard against this anyways now).
* Internal: Spigot is stopping the conversion of zombie villagers on its own now if the corresponding transform event gets cancelled.
* Internal: Added a test to ensure consistency between ShopkeepersPlugin and ShopkeepersAPI.
* Internal: Added ItemData tests. This requires CraftBukkit as new test dependency due to relying on item serialization.
* Debugging: Small changes and additions to some debug messages, especially related to shopkeeper interactions and shopkeeper spawning.
* Debugging: Added setting 'debug-options', which can be used to enable additional debugging tools.

View File

@ -44,6 +44,16 @@
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<workingDirectory>target/test-server</workingDirectory>
<excludes>
<exclude>com/nisovin/shopkeepers/PerformanceTests.java</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>

View File

@ -104,6 +104,19 @@
<artifactId>maven-dependency-plugin</artifactId>
<version>2.8</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.12.4</version>
<configuration>
<includes>
<include>**/Test*.java</include>
<include>**/*Test.java</include>
<include>**/*Tests.java</include>
<include>**/*TestCase.java</include>
</includes>
</configuration>
</plugin>
</plugins>
</pluginManagement>
@ -147,6 +160,22 @@
<version>1.14.4-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<!-- Corresponding CraftBukkit version. Only required for testing.
Individual NMS modules can use a more specific/later CraftBukkit version.
-->
<dependency>
<groupId>org.bukkit</groupId>
<artifactId>craftbukkit</artifactId>
<version>1.14.4-R0.1-SNAPSHOT</version>
<type>jar</type>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.bukkit</groupId>
<artifactId>bukkit</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>

View File

@ -96,13 +96,20 @@
<groupId>org.bukkit</groupId>
<artifactId>bukkit</artifactId>
</dependency>
<dependency>
<groupId>org.bukkit</groupId>
<artifactId>craftbukkit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-library</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.sk89q.worldguard</groupId>

View File

@ -160,8 +160,8 @@ public class SKShopkeepersPlugin extends JavaPlugin implements ShopkeepersPlugin
}
// load config:
File file = new File(this.getDataFolder(), "config.yml");
if (!file.exists()) {
File configFile = new File(this.getDataFolder(), "config.yml");
if (!configFile.exists()) {
this.saveDefaultConfig();
}
this.reloadConfig();

View File

@ -10,12 +10,14 @@ import java.util.Locale;
import org.bukkit.Material;
import org.bukkit.configuration.Configuration;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import com.nisovin.shopkeepers.config.migration.ConfigMigrations;
import com.nisovin.shopkeepers.util.ConfigUtils;
import com.nisovin.shopkeepers.util.ConversionUtils;
import com.nisovin.shopkeepers.util.ItemData;
import com.nisovin.shopkeepers.util.ItemUtils;
import com.nisovin.shopkeepers.util.Log;
import com.nisovin.shopkeepers.util.StringUtils;
@ -56,9 +58,7 @@ public class Settings {
/*
* Shop Creation (and removal)
*/
public static Material shopCreationItem = Material.VILLAGER_SPAWN_EGG;
public static String shopCreationItemName = "";
public static List<String> shopCreationItemLore = new ArrayList<>(0);
public static ItemData shopCreationItem = new ItemData(Material.VILLAGER_SPAWN_EGG);
public static boolean preventShopCreationItemRegularUsage = false;
public static boolean deletingPlayerShopReturnsCreationItem = false;
@ -165,18 +165,17 @@ public class Settings {
*/
public static String editorTitle = "Shopkeeper Editor";
public static Material previousPageItem = Material.WRITABLE_BOOK;
public static Material nextPageItem = Material.WRITABLE_BOOK;
public static Material currentPageItem = Material.WRITABLE_BOOK;
public static Material tradeSetupItem = Material.PAPER;
public static ItemData previousPageItem = new ItemData(Material.WRITABLE_BOOK);
public static ItemData nextPageItem = new ItemData(Material.WRITABLE_BOOK);
public static ItemData currentPageItem = new ItemData(Material.WRITABLE_BOOK);
public static ItemData tradeSetupItem = new ItemData(Material.PAPER);
public static Material nameItem = Material.NAME_TAG;
public static List<String> nameItemLore = new ArrayList<>(0);
public static ItemData nameItem = new ItemData(Material.NAME_TAG);
public static boolean enableChestOptionOnPlayerShop = true;
public static Material chestItem = Material.CHEST;
public static ItemData chestItem = new ItemData(Material.CHEST);
public static Material deleteItem = Material.BONE;
public static ItemData deleteItem = new ItemData(Material.BONE);
/*
* Non-shopkeeper villagers
@ -193,9 +192,7 @@ public class Settings {
/*
* Hiring
*/
public static Material hireItem = Material.EMERALD;
public static String hireItemName = "";
public static List<String> hireItemLore = new ArrayList<>(0);
public static ItemData hireItem = new ItemData(Material.EMERALD);
public static int hireOtherVillagersCosts = 1;
public static String forHireTitle = "For Hire";
public static boolean hireRequireCreationPermission = true;
@ -215,26 +212,15 @@ public class Settings {
/*
* Currencies
*/
public static Material currencyItem = Material.EMERALD;
public static String currencyItemName = "";
public static List<String> currencyItemLore = new ArrayList<>(0);
public static Material zeroCurrencyItem = Material.BARRIER;
public static String zeroCurrencyItemName = "";
public static List<String> zeroCurrencyItemLore = new ArrayList<>(0);
public static Material highCurrencyItem = Material.EMERALD_BLOCK;
public static String highCurrencyItemName = "";
public static List<String> highCurrencyItemLore = new ArrayList<>(0);
public static ItemData currencyItem = new ItemData(Material.EMERALD);
public static ItemData zeroCurrencyItem = new ItemData(Material.BARRIER);
public static ItemData highCurrencyItem = new ItemData(Material.EMERALD_BLOCK);
public static ItemData highZeroCurrencyItem = new ItemData(Material.BARRIER);
// note: this can in general be larger than 64!
public static int highCurrencyValue = 9;
public static int highCurrencyMinCost = 20;
public static Material highZeroCurrencyItem = Material.BARRIER;
public static String highZeroCurrencyItemName = "";
public static List<String> highZeroCurrencyItemLore = new ArrayList<>(0);
/*
* Messages
*/
@ -466,13 +452,10 @@ public class Settings {
public static boolean loadConfiguration(Configuration config) {
boolean configChanged = false;
// perform config migrations (if the config is not empty):
if (!config.getKeys(false).isEmpty()) {
int configVersion = config.getInt("config-version", 0); // default value is important here
if (configVersion <= 0) {
migrateConfig_0_to_1(config);
configChanged = true;
}
// perform config migrations:
boolean migrated = ConfigMigrations.applyMigrations(config);
if (migrated) {
configChanged = true;
}
// exempt a few string / string list settings from color conversion:
@ -544,27 +527,23 @@ public class Settings {
Log.warning("Config: 'gravity-chunk-range' cannot be negative.");
gravityChunkRange = 0;
}
if (highCurrencyValue <= 0 && highCurrencyItem != Material.AIR) {
Log.debug("Config: 'high-currency-item' disabled because of 'high-currency-value' being less than 1.");
highCurrencyItem = Material.AIR;
}
// certain items cannot be of type AIR:
if (shopCreationItem == Material.AIR) {
if (shopCreationItem.getType() == Material.AIR) {
Log.warning("Config: 'shop-creation-item' can not be AIR.");
shopCreationItem = Material.VILLAGER_SPAWN_EGG;
shopCreationItem = shopCreationItem.withType(Material.VILLAGER_SPAWN_EGG);
}
if (hireItem == Material.AIR) {
if (hireItem.getType() == Material.AIR) {
Log.warning("Config: 'hire-item' can not be AIR.");
hireItem = Material.EMERALD;
hireItem = hireItem.withType(Material.EMERALD);
}
if (currencyItem == Material.AIR) {
if (currencyItem.getType() == Material.AIR) {
Log.warning("Config: 'currency-item' can not be AIR.");
currencyItem = Material.EMERALD;
currencyItem = currencyItem.withType(Material.EMERALD);
}
if (namingOfPlayerShopsViaItem) {
if (nameItem == Material.AIR) {
if (nameItem.getType() == Material.AIR) {
Log.warning("Config: 'name-item' can not be AIR if naming-of-player-shops-via-item is enabled!");
nameItem = Material.NAME_TAG;
nameItem = nameItem.withType(Material.NAME_TAG);
}
}
if (taxRate < 0) {
@ -575,6 +554,9 @@ public class Settings {
taxRate = 100;
}
// prepare derived settings:
DerivedSettings.setup();
return configChanged;
}
@ -594,12 +576,24 @@ public class Settings {
return config.getBoolean(configKey);
} else if (typeClass == Material.class) {
// this assumes that legacy item conversion has already been performed
Material material = loadMaterial(config, configKey, false);
Material material = ConfigUtils.loadMaterial(config, configKey);
if (material == null) {
Log.warning("Config: Unknown material for config entry '" + configKey + "': " + config.get(configKey));
Log.warning("Config: All valid material names can be found here: https://hub.spigotmc.org/javadocs/bukkit/org/bukkit/Material.html");
}
return material;
} else if (typeClass == ItemData.class) {
ItemData itemData = ItemData.deserialize(config.get(configKey), (warning) -> {
Log.warning("Config: Couldn't load item data for config entry '" + configKey + "': " + warning);
if (warning.contains("Unknown item type")) { // TODO this is ugly
Log.warning("Config: All valid material names can be found here: https://hub.spigotmc.org/javadocs/bukkit/org/bukkit/Material.html");
}
});
// normalize to not null:
if (itemData == null) {
itemData = new ItemData(Material.AIR);
}
return itemData;
} else if (typeClass == List.class) {
if (genericType == String.class) {
List<String> stringList = config.getStringList(configKey);
@ -616,6 +610,11 @@ public class Settings {
}
private static void setConfigValue(Configuration config, String configKey, List<String> noColorConversionKeys, Class<?> typeClass, Class<?> genericType, Object value) {
if (value == null) {
config.set(configKey, value); // removes value
return;
}
if (typeClass == Material.class) {
config.set(configKey, ((Material) value).name());
} else if (typeClass == String.class) {
@ -630,137 +629,13 @@ public class Settings {
value = Utils.decolorize(ConversionUtils.toStringList((List<?>) value));
}
config.set(configKey, value);
} else if (typeClass == ItemData.class) {
config.set(configKey, ((ItemData) value).serialize());
} else {
config.set(configKey, value);
}
}
private static Material loadMaterial(ConfigurationSection config, String key, boolean checkLegacy) {
String materialName = config.getString(key); // note: takes defaults into account
if (materialName == null) return null;
Material material = Material.matchMaterial(materialName);
if (material == null && checkLegacy) {
// check for legacy material:
String legacyMaterialName = Material.LEGACY_PREFIX + materialName;
material = Material.matchMaterial(legacyMaterialName);
}
return material;
}
private static void migrateConfig_0_to_1(Configuration config) {
// pre 1.13 to 1.13:
Log.info("Migrating config to version 1 ..");
// migrate shop creation item, if present:
String shopCreationItemTypeName = config.getString("shop-creation-item", null);
if (shopCreationItemTypeName != null) {
// note: this takes defaults into account:
Material shopCreationItem = loadMaterial(config, "shop-creation-item", true);
String shopCreationItemSpawnEggEntityType = config.getString("shop-creation-item-spawn-egg-entity-type");
if (shopCreationItem == Material.LEGACY_MONSTER_EGG && !StringUtils.isEmpty(shopCreationItemSpawnEggEntityType)) {
// migrate spawn egg (ignores the data value): spawn eggs are different materials now
EntityType spawnEggEntityType = null;
try {
spawnEggEntityType = EntityType.valueOf(shopCreationItemSpawnEggEntityType);
} catch (IllegalArgumentException e) {
// unknown entity type
}
Material newShopCreationItem = LegacyConversion.fromLegacySpawnEgg(spawnEggEntityType);
boolean usingDefault = false;
if (newShopCreationItem == null || newShopCreationItem == Material.AIR) {
// fallback to default:
newShopCreationItem = Material.VILLAGER_SPAWN_EGG;
usingDefault = true;
}
assert newShopCreationItem != null;
Log.info(" Migrating 'shop-creation-item' from '" + shopCreationItemTypeName + "' and spawn egg entity type '"
+ shopCreationItemSpawnEggEntityType + "' to '" + newShopCreationItem + "'" + (usingDefault ? " (default)" : "") + ".");
config.set("shop-creation-item", newShopCreationItem.name());
} else {
// regular material + data value migration:
migrateLegacyItemData(config, "shop-creation-item", "shop-creation-item", "shop-creation-item-data", Material.VILLAGER_SPAWN_EGG);
}
}
// remove shop-creation-item-spawn-egg-entity-type from config:
if (config.isSet("shop-creation-item-spawn-egg-entity-type")) {
Log.info(" Removing 'shop-creation-item-spawn-egg-entity-type' (previously '" + config.get("shop-creation-item-spawn-egg-entity-type", null) + "').");
config.set("shop-creation-item-spawn-egg-entity-type", null);
}
// remove shop-creation-item-data-value from config:
if (config.isSet("shop-creation-item-data")) {
Log.info(" Removing 'shop-creation-item-data' (previously '" + config.get("shop-creation-item-data", null) + "').");
config.set("shop-creation-item-data", null);
}
// name item:
migrateLegacyItemData(config, "name-item", "name-item", "name-item-data", Material.NAME_TAG);
// chest item:
migrateLegacyItemData(config, "chest-item", "chest-item", "chest-item-data", Material.CHEST);
// delete item:
migrateLegacyItemData(config, "delete-item", "delete-item", "delete-item-data", Material.BONE);
// hire item:
migrateLegacyItemData(config, "hire-item", "hire-item", "hire-item-data", Material.EMERALD);
// currency item:
migrateLegacyItemData(config, "currency-item", "currency-item", "currency-item-data", Material.EMERALD);
// zero currency item:
migrateLegacyItemData(config, "zero-currency-item", "zero-currency-item", "zero-currency-item-data", Material.BARRIER);
// high currency item:
migrateLegacyItemData(config, "high-currency-item", "high-currency-item", "high-currency-item-data", Material.EMERALD_BLOCK);
// high zero currency item:
migrateLegacyItemData(config, "high-zero-currency-item", "high-zero-currency-item", "high-zero-currency-item-data", Material.BARRIER);
// update config version:
config.set("config-version", 1);
Log.info("Config migration to version 1 done.");
}
// convert legacy material + data value to new material, returns true if migrations took place
private static boolean migrateLegacyItemData(ConfigurationSection config, String migratedItemId, String itemTypeKey, String itemDataKey, Material defaultType) {
boolean migrated = false;
// migrate material, if present:
String itemTypeName = config.getString(itemTypeKey, null);
if (itemTypeName != null) {
Material newItemType = null;
int itemData = config.getInt(itemDataKey, 0);
Material itemType = loadMaterial(config, itemTypeKey, true);
if (itemType != null) {
newItemType = LegacyConversion.fromLegacy(itemType, (byte) itemData);
}
boolean usingDefault = false;
if (newItemType == null || newItemType == Material.AIR) {
// fallback to default:
newItemType = defaultType;
usingDefault = true;
}
if (itemType != newItemType) {
Log.info(" Migrating '" + migratedItemId + "' from type '" + itemTypeName + "' and data value '" + itemData + "' to type '"
+ (newItemType == null ? "" : newItemType.name()) + "'" + (usingDefault ? " (default)" : "") + ".");
config.set(itemTypeKey, (newItemType != null ? newItemType.name() : null));
migrated = true;
}
}
// remove data value from config:
if (config.isSet(itemDataKey)) {
Log.info(" Removing '" + itemDataKey + "' (previously '" + config.get(itemDataKey, null) + "').");
config.set(itemDataKey, null);
migrated = true;
}
return migrated;
}
public static void loadLanguageConfiguration(Configuration config) {
try {
Field[] fields = Settings.class.getDeclaredFields();
@ -789,91 +664,117 @@ public class Settings {
// item utilities:
// stores derived settings that get setup after loading the config
public static class DerivedSettings {
public static ItemData namingItemData = new ItemData(Material.AIR);
// button items:
public static ItemData nameButtonItem = new ItemData(Material.AIR);
public static ItemData chestButtonItem = new ItemData(Material.AIR);
public static ItemData deleteButtonItem = new ItemData(Material.AIR);
public static ItemData hireButtonItem = new ItemData(Material.AIR);
// gets called after the config has been loaded:
private static void setup() {
// ignore display name (which is used for specifying the new shopkeeper name):
namingItemData = new ItemData(ItemUtils.setItemStackName(nameItem.createItemStack(), null));
// button items:
nameButtonItem = new ItemData(ItemUtils.setItemStackNameAndLore(nameItem.createItemStack(), msgButtonName, msgButtonNameLore));
chestButtonItem = new ItemData(ItemUtils.setItemStackNameAndLore(chestItem.createItemStack(), msgButtonChest, msgButtonChestLore));
deleteButtonItem = new ItemData(ItemUtils.setItemStackNameAndLore(deleteItem.createItemStack(), msgButtonDelete, msgButtonDeleteLore));
hireButtonItem = new ItemData(ItemUtils.setItemStackNameAndLore(hireItem.createItemStack(), msgButtonHire, msgButtonHireLore));
}
}
// creation item:
public static ItemStack createShopCreationItem() {
return ItemUtils.createItemStack(shopCreationItem, 1, shopCreationItemName, shopCreationItemLore);
return shopCreationItem.createItemStack();
}
public static boolean isShopCreationItem(ItemStack item) {
return ItemUtils.isSimilar(item, Settings.shopCreationItem, Settings.shopCreationItemName, Settings.shopCreationItemLore);
return shopCreationItem.matches(item);
}
// naming item:
public static ItemStack createNameButtonItem() {
return ItemUtils.createItemStack(nameItem, 1, msgButtonName, msgButtonNameLore);
public static boolean isNamingItem(ItemStack item) {
return DerivedSettings.namingItemData.matches(item);
}
public static boolean isNamingItem(ItemStack item) {
return ItemUtils.isSimilar(item, nameItem, null, Settings.nameItemLore);
public static ItemStack createNameButtonItem() {
return DerivedSettings.nameButtonItem.createItemStack();
}
// chest button:
public static ItemStack createChestButtonItem() {
return ItemUtils.createItemStack(chestItem, 1, msgButtonChest, msgButtonChestLore);
return DerivedSettings.chestButtonItem.createItemStack();
}
// delete button:
public static ItemStack createDeleteButtonItem() {
return ItemUtils.createItemStack(deleteItem, 1, msgButtonDelete, msgButtonDeleteLore);
return DerivedSettings.deleteButtonItem.createItemStack();
}
// hire item:
public static ItemStack createHireButtonItem() {
return ItemUtils.createItemStack(hireItem, 1, msgButtonHire, msgButtonHireLore);
return DerivedSettings.hireButtonItem.createItemStack();
}
public static boolean isHireItem(ItemStack item) {
return ItemUtils.isSimilar(item, hireItem, hireItemName, hireItemLore);
return hireItem.matches(item);
}
// CURRENCY
// currency item:
public static ItemStack createCurrencyItem(int amount) {
return ItemUtils.createItemStack(Settings.currencyItem, amount, Settings.currencyItemName, Settings.currencyItemLore);
return currencyItem.createItemStack(amount);
}
public static boolean isCurrencyItem(ItemStack item) {
return ItemUtils.isSimilar(item, Settings.currencyItem, Settings.currencyItemName, Settings.currencyItemLore);
return currencyItem.matches(item);
}
// high currency item:
public static boolean isHighCurrencyEnabled() {
return (Settings.highCurrencyItem != Material.AIR);
return (highCurrencyValue > 0 && highCurrencyItem.getType() != Material.AIR);
}
public static ItemStack createHighCurrencyItem(int amount) {
if (!isHighCurrencyEnabled()) return null;
return ItemUtils.createItemStack(Settings.highCurrencyItem, amount, Settings.highCurrencyItemName, Settings.highCurrencyItemLore);
return highCurrencyItem.createItemStack(amount);
}
public static boolean isHighCurrencyItem(ItemStack item) {
if (!isHighCurrencyEnabled()) return false;
return ItemUtils.isSimilar(item, Settings.highCurrencyItem, Settings.highCurrencyItemName, Settings.highCurrencyItemLore);
return highCurrencyItem.matches(item);
}
// zero currency item:
public static ItemStack createZeroCurrencyItem() {
if (Settings.zeroCurrencyItem == Material.AIR) return null;
return ItemUtils.createItemStack(Settings.zeroCurrencyItem, 1, Settings.zeroCurrencyItemName, Settings.zeroCurrencyItemLore);
if (zeroCurrencyItem.getType() == Material.AIR) return null;
return zeroCurrencyItem.createItemStack();
}
public static boolean isZeroCurrencyItem(ItemStack item) {
if (Settings.zeroCurrencyItem == Material.AIR) {
if (zeroCurrencyItem.getType() == Material.AIR) {
return ItemUtils.isEmpty(item);
}
return ItemUtils.isSimilar(item, Settings.zeroCurrencyItem, Settings.zeroCurrencyItemName, Settings.zeroCurrencyItemLore);
return zeroCurrencyItem.matches(item);
}
// high zero currency item:
public static ItemStack createHighZeroCurrencyItem() {
if (Settings.highZeroCurrencyItem == Material.AIR) return null;
return ItemUtils.createItemStack(Settings.highZeroCurrencyItem, 1, Settings.highZeroCurrencyItemName, Settings.highZeroCurrencyItemLore);
if (highZeroCurrencyItem.getType() == Material.AIR) return null;
return highZeroCurrencyItem.createItemStack();
}
public static boolean isHighZeroCurrencyItem(ItemStack item) {
if (Settings.highZeroCurrencyItem == Material.AIR) {
if (highZeroCurrencyItem.getType() == Material.AIR) {
return ItemUtils.isEmpty(item);
}
return ItemUtils.isSimilar(item, Settings.highZeroCurrencyItem, Settings.highZeroCurrencyItemName, Settings.highZeroCurrencyItemLore);
return highZeroCurrencyItem.matches(item);
}
//

View File

@ -0,0 +1,8 @@
package com.nisovin.shopkeepers.config.migration;
import org.bukkit.configuration.Configuration;
public interface ConfigMigration {
public void apply(Configuration config);
}

View File

@ -0,0 +1,125 @@
package com.nisovin.shopkeepers.config.migration;
import org.bukkit.Material;
import org.bukkit.configuration.Configuration;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.entity.EntityType;
import com.nisovin.shopkeepers.util.ConfigUtils;
import com.nisovin.shopkeepers.util.Log;
import com.nisovin.shopkeepers.util.StringUtils;
/**
* Migrate the config from version &lt;= 0 to version 1.
*/
public class ConfigMigration1 implements ConfigMigration {
@Override
public void apply(Configuration config) {
// pre 1.13 to 1.13:
// migrate shop creation item, if present:
String shopCreationItemTypeName = config.getString("shop-creation-item", null);
if (shopCreationItemTypeName != null) {
// note: this takes defaults into account:
Material shopCreationItem = ConfigUtils.loadMaterial(config, "shop-creation-item", true);
String shopCreationItemSpawnEggEntityType = config.getString("shop-creation-item-spawn-egg-entity-type");
if (shopCreationItem == Material.LEGACY_MONSTER_EGG && !StringUtils.isEmpty(shopCreationItemSpawnEggEntityType)) {
// migrate spawn egg (ignores the data value): spawn eggs are different materials now
EntityType spawnEggEntityType = null;
try {
spawnEggEntityType = EntityType.valueOf(shopCreationItemSpawnEggEntityType);
} catch (IllegalArgumentException e) {
// unknown entity type
}
Material newShopCreationItem = LegacyConversion.fromLegacySpawnEgg(spawnEggEntityType);
boolean usingDefault = false;
if (newShopCreationItem == null || newShopCreationItem == Material.AIR) {
// fallback to default:
newShopCreationItem = Material.VILLAGER_SPAWN_EGG;
usingDefault = true;
}
assert newShopCreationItem != null;
Log.info(" Migrating 'shop-creation-item' from '" + shopCreationItemTypeName + "' and spawn egg entity type '"
+ shopCreationItemSpawnEggEntityType + "' to '" + newShopCreationItem + "'" + (usingDefault ? " (default)" : "") + ".");
config.set("shop-creation-item", newShopCreationItem.name());
} else {
// regular material + data value migration:
migrateLegacyItemData(config, "shop-creation-item", "shop-creation-item", "shop-creation-item-data", Material.VILLAGER_SPAWN_EGG);
}
}
// remove shop-creation-item-spawn-egg-entity-type from config:
if (config.isSet("shop-creation-item-spawn-egg-entity-type")) {
Log.info(" Removing 'shop-creation-item-spawn-egg-entity-type' (previously '" + config.get("shop-creation-item-spawn-egg-entity-type", null) + "').");
config.set("shop-creation-item-spawn-egg-entity-type", null);
}
// remove shop-creation-item-data-value from config:
if (config.isSet("shop-creation-item-data")) {
Log.info(" Removing 'shop-creation-item-data' (previously '" + config.get("shop-creation-item-data", null) + "').");
config.set("shop-creation-item-data", null);
}
// name item:
migrateLegacyItemData(config, "name-item", "name-item", "name-item-data", Material.NAME_TAG);
// chest item:
migrateLegacyItemData(config, "chest-item", "chest-item", "chest-item-data", Material.CHEST);
// delete item:
migrateLegacyItemData(config, "delete-item", "delete-item", "delete-item-data", Material.BONE);
// hire item:
migrateLegacyItemData(config, "hire-item", "hire-item", "hire-item-data", Material.EMERALD);
// currency item:
migrateLegacyItemData(config, "currency-item", "currency-item", "currency-item-data", Material.EMERALD);
// zero currency item:
migrateLegacyItemData(config, "zero-currency-item", "zero-currency-item", "zero-currency-item-data", Material.BARRIER);
// high currency item:
migrateLegacyItemData(config, "high-currency-item", "high-currency-item", "high-currency-item-data", Material.EMERALD_BLOCK);
// high zero currency item:
migrateLegacyItemData(config, "high-zero-currency-item", "high-zero-currency-item", "high-zero-currency-item-data", Material.BARRIER);
}
// convert legacy material + data value to new material, returns true if migrations took place
private static boolean migrateLegacyItemData(ConfigurationSection config, String migratedItemId, String itemTypeKey, String itemDataKey, Material defaultType) {
boolean migrated = false;
// migrate material, if present:
String itemTypeName = config.getString(itemTypeKey, null);
if (itemTypeName != null) {
Material newItemType = null;
int itemData = config.getInt(itemDataKey, 0);
Material itemType = ConfigUtils.loadMaterial(config, itemTypeKey, true);
if (itemType != null) {
newItemType = LegacyConversion.fromLegacy(itemType, (byte) itemData);
}
boolean usingDefault = false;
if (newItemType == null || newItemType == Material.AIR) {
// fallback to default:
newItemType = defaultType;
usingDefault = true;
}
if (itemType != newItemType) {
Log.info(" Migrating '" + migratedItemId + "' from type '" + itemTypeName + "' and data value '" + itemData + "' to type '"
+ (newItemType == null ? "" : newItemType.name()) + "'" + (usingDefault ? " (default)" : "") + ".");
config.set(itemTypeKey, (newItemType != null ? newItemType.name() : null));
migrated = true;
}
}
// remove data value from config:
if (config.isSet(itemDataKey)) {
Log.info(" Removing '" + itemDataKey + "' (previously '" + config.get(itemDataKey, null) + "').");
config.set(itemDataKey, null);
migrated = true;
}
return migrated;
}
}

View File

@ -0,0 +1,106 @@
package com.nisovin.shopkeepers.config.migration;
import java.util.List;
import org.bukkit.Material;
import org.bukkit.configuration.Configuration;
import com.nisovin.shopkeepers.util.ConfigUtils;
import com.nisovin.shopkeepers.util.ItemData;
import com.nisovin.shopkeepers.util.Log;
import com.nisovin.shopkeepers.util.StringUtils;
import com.nisovin.shopkeepers.util.Utils;
/**
* Migrate the config from version 1 to version 2.
*/
public class ConfigMigration2 implements ConfigMigration {
@Override
public void apply(Configuration config) {
// Convert item data settings to ItemData:
// Due to the compact representation of ItemData, this is only required for items which previously supported
// further data (such as custom display name and/or lore).
// shop-creation-item:
migrateItem(config, "shop-creation-item", "shop-creation-item-name", "shop-creation-item-lore");
// name-item:
migrateItem(config, "name-item", null, "name-item-lore");
// hire-item:
migrateItem(config, "hire-item", "hire-item-name", "hire-item-lore");
// currency-item:
migrateItem(config, "currency-item", "currency-item-name", "currency-item-lore");
// zero-currency-item:
migrateItem(config, "zero-currency-item", "zero-currency-item-name", "zero-currency-item-lore");
// high-currency-item
migrateItem(config, "high-currency-item", "high-currency-item-name", "high-currency-item-lore");
// zero-high-currency-item:
migrateItem(config, "zero-high-currency-item", "zero-high-currency-item-name", "zero-high-currency-item-lore");
}
// displayNameKey and loreKey can be null if they don't exist
private static void migrateItem(Configuration config, String itemTypeKey, String displayNameKey, String loreKey) {
assert config != null && itemTypeKey != null;
StringBuilder msgBuilder = new StringBuilder();
msgBuilder.append(" Migrating item data for '")
.append(itemTypeKey)
.append("' (")
.append(config.get(itemTypeKey))
.append(")");
if (displayNameKey != null) {
msgBuilder.append(" and '")
.append(displayNameKey)
.append("' (")
.append(config.get(displayNameKey))
.append(")");
}
if (loreKey != null) {
msgBuilder.append(" and '")
.append(loreKey)
.append("' (")
.append(config.get(loreKey))
.append(")");
}
msgBuilder.append(" to new format.");
Log.info(msgBuilder.toString());
// item type:
Material itemType = ConfigUtils.loadMaterial(config, itemTypeKey);
if (itemType == null) {
Log.warning(" Skipping migration for item '" + itemTypeKey + "'! Unknown material: " + config.get(itemTypeKey));
return;
}
// display name:
String displayName = null;
if (displayNameKey != null) {
displayName = Utils.colorize(config.getString(displayNameKey));
if (StringUtils.isEmpty(displayName)) {
displayName = null; // normalize empty display name to null
}
}
// lore:
List<String> lore = null;
if (loreKey != null) {
lore = Utils.colorize(config.getStringList(loreKey));
if (lore == null || lore.isEmpty()) {
lore = null; // normalize empty lore to null
}
}
// create ItemData:
ItemData itemData = new ItemData(itemType, displayName, lore);
// remove old data:
config.set(itemTypeKey, null);
if (displayNameKey != null) {
config.set(displayNameKey, null);
}
if (loreKey != null) {
config.set(loreKey, null);
}
// save new data (under previous itemTypeKey):
config.set(itemTypeKey, itemData.serialize());
}
}

View File

@ -0,0 +1,43 @@
package com.nisovin.shopkeepers.config.migration;
import java.util.Arrays;
import java.util.List;
import org.bukkit.configuration.Configuration;
import com.nisovin.shopkeepers.util.Log;
public class ConfigMigrations {
private static final String CONFIG_VERSION_KEY = "config-version";
// each index corresponds to a source config version and its migration to the next version
private static final List<ConfigMigration> migrations = Arrays.asList(new ConfigMigration1(), new ConfigMigration2());
// returns true if any migrations got applied (if the config has potentially been modified)
public static boolean applyMigrations(Configuration config) {
// no migrations are required if the config is empty (missing entries will get generated from defaults):
if (config.getKeys(false).isEmpty()) return false;
boolean migrated = false;
int configVersion = config.getInt(CONFIG_VERSION_KEY, 0); // default value is important here
if (configVersion < 0) {
configVersion = 0; // first version is 0
}
for (int version = configVersion; version < migrations.size(); ++version) {
int nextVersion = (version + 1);
ConfigMigration migration = migrations.get(version);
Log.info("Migrating config from version " + version + " to version " + nextVersion + " ..");
if (migration != null) {
migration.apply(config);
}
// update config version:
config.set(CONFIG_VERSION_KEY, nextVersion);
migrated = true;
Log.info("Config migrated to version " + nextVersion + "." + (migration == null ? " (skipped)" : ""));
}
return migrated;
}
private ConfigMigrations() {
}
}

View File

@ -1,4 +1,4 @@
package com.nisovin.shopkeepers;
package com.nisovin.shopkeepers.config.migration;
import java.util.LinkedHashMap;
import java.util.Map;

View File

@ -147,13 +147,9 @@ public abstract class AbstractPlayerShopkeeper extends AbstractShopkeeper implem
PlayerShopEditorHandler editorHandler = (PlayerShopEditorHandler) this.getUIHandler(DefaultUITypes.EDITOR());
if (editorHandler.canOpen(player)) {
// rename with the player's item in hand:
String newName;
ItemMeta itemMeta;
if (!itemInMainHand.hasItemMeta() || (itemMeta = itemInMainHand.getItemMeta()) == null || !itemMeta.hasDisplayName()) {
newName = "";
} else {
newName = itemMeta.getDisplayName();
}
ItemMeta itemMeta = itemInMainHand.getItemMeta(); // can be null
String newName = (itemMeta != null && itemMeta.hasDisplayName()) ? itemMeta.getDisplayName() : "";
assert newName != null; // ItemMeta#getDisplayName returns non-null in all cases
// handled name changing:
if (SKShopkeepersPlugin.getInstance().getShopkeeperNaming().requestNameChange(player, this, newName)) {
@ -281,7 +277,7 @@ public abstract class AbstractPlayerShopkeeper extends AbstractShopkeeper implem
ItemStack item2 = null;
if (Settings.isHighCurrencyEnabled() && price > Settings.highCurrencyMinCost) {
int highCurrencyAmount = Math.min(price / Settings.highCurrencyValue, Settings.highCurrencyItem.getMaxStackSize());
int highCurrencyAmount = Math.min(price / Settings.highCurrencyValue, Settings.highCurrencyItem.getType().getMaxStackSize());
if (highCurrencyAmount > 0) {
remainingPrice -= (highCurrencyAmount * Settings.highCurrencyValue);
ItemStack highCurrencyItem = Settings.createHighCurrencyItem(highCurrencyAmount);
@ -290,7 +286,7 @@ public abstract class AbstractPlayerShopkeeper extends AbstractShopkeeper implem
}
if (remainingPrice > 0) {
if (remainingPrice > Settings.currencyItem.getMaxStackSize()) {
if (remainingPrice > Settings.currencyItem.getType().getMaxStackSize()) {
// cannot represent this price with the used currency items:
Log.warning("Shopkeeper at " + this.getPositionString() + " owned by " + ownerName + " has an invalid cost!");
return null;
@ -309,7 +305,7 @@ public abstract class AbstractPlayerShopkeeper extends AbstractShopkeeper implem
// returns null (and logs a warning) if the price cannot be represented correctly by currency items
protected TradingRecipe createBuyingRecipe(ItemStack itemBeingBought, int price, boolean outOfStock) {
if (price > Settings.currencyItem.getMaxStackSize()) {
if (price > Settings.currencyItem.getType().getMaxStackSize()) {
// cannot represent this price with the used currency items:
Log.warning("Shopkeeper at " + this.getPositionString() + " owned by " + ownerName + " has an invalid cost!");
return null;

View File

@ -134,7 +134,7 @@ public abstract class PlayerShopEditorHandler extends EditorHandler {
if (Settings.isHighCurrencyEnabled()) {
int highCost = 0;
if (remainingCost > Settings.highCurrencyMinCost) {
highCost = Math.min((remainingCost / Settings.highCurrencyValue), Settings.highCurrencyItem.getMaxStackSize());
highCost = Math.min((remainingCost / Settings.highCurrencyValue), Settings.highCurrencyItem.getType().getMaxStackSize());
}
if (highCost > 0) {
remainingCost -= (highCost * Settings.highCurrencyValue);
@ -144,7 +144,7 @@ public abstract class PlayerShopEditorHandler extends EditorHandler {
}
}
if (remainingCost > 0) {
if (remainingCost <= Settings.currencyItem.getMaxStackSize()) {
if (remainingCost <= Settings.currencyItem.getType().getMaxStackSize()) {
lowCostItem = Settings.createCurrencyItem(remainingCost);
} else {
// cost is to large to represent: reset cost to zero:
@ -165,10 +165,10 @@ public abstract class PlayerShopEditorHandler extends EditorHandler {
ItemStack lowCostItem = recipe.getItem1();
ItemStack highCostItem = recipe.getItem2();
int price = 0;
if (lowCostItem != null && lowCostItem.getType() == Settings.currencyItem && lowCostItem.getAmount() > 0) {
if (lowCostItem != null && lowCostItem.getType() == Settings.currencyItem.getType() && lowCostItem.getAmount() > 0) {
price += lowCostItem.getAmount();
}
if (Settings.isHighCurrencyEnabled() && highCostItem != null && highCostItem.getType() == Settings.highCurrencyItem && highCostItem.getAmount() > 0) {
if (Settings.isHighCurrencyEnabled() && highCostItem != null && highCostItem.getType() == Settings.highCurrencyItem.getType() && highCostItem.getAmount() > 0) {
price += (highCostItem.getAmount() * Settings.highCurrencyValue);
}
return price;

View File

@ -147,9 +147,7 @@ public class SKBookPlayerShopkeeper extends AbstractPlayerShopkeeper implements
protected static BookMeta getBookMeta(ItemStack item) {
if (ItemUtils.isEmpty(item)) return null;
if (item.getType() != Material.WRITTEN_BOOK) return null;
if (!item.hasItemMeta()) return null;
return (BookMeta) item.getItemMeta();
return (BookMeta) item.getItemMeta(); // can be null
}
protected static Generation getBookGeneration(ItemStack item) {

View File

@ -73,7 +73,7 @@ public class BuyingPlayerShopEditorHandler extends PlayerShopEditorHandler {
ItemStack priceItem = recipe.getResultItem();
assert priceItem != null;
if (priceItem.getType() != Settings.currencyItem) return; // checking this just in case
if (priceItem.getType() != Settings.currencyItem.getType()) return; // checking this just in case
assert priceItem.getAmount() > 0;
SKBuyingPlayerShopkeeper shopkeeper = this.getShopkeeper();

View File

@ -162,7 +162,7 @@ public class BuyingPlayerShopTradingHandler extends PlayerShopTradingHandler {
// add the remaining change into empty slots (all partial slots have already been cleared above):
// TODO this could probably be replaced with Utils.addItems
int maxStackSize = Settings.currencyItem.getMaxStackSize();
int maxStackSize = Settings.currencyItem.getType().getMaxStackSize();
for (int slot = 0; slot < contents.length; slot++) {
ItemStack itemStack = contents[slot];
if (!ItemUtils.isEmpty(itemStack)) continue;

View File

@ -344,7 +344,7 @@ public abstract class EditorHandler extends UIHandler {
"{prev_page}", prevPage,
"{page}", String.valueOf(page),
"{max_page}", String.valueOf(TRADES_MAX_PAGES));
return ItemUtils.createItemStack(Settings.previousPageItem, 1, itemName, Settings.msgButtonPreviousPageLore);
return ItemUtils.setItemStackNameAndLore(Settings.previousPageItem.createItemStack(), itemName, Settings.msgButtonPreviousPageLore);
}
protected ItemStack createNextPageIcon(int page) {
@ -356,21 +356,21 @@ public abstract class EditorHandler extends UIHandler {
"{next_page}", nextPage,
"{page}", String.valueOf(page),
"{max_page}", String.valueOf(TRADES_MAX_PAGES));
return ItemUtils.createItemStack(Settings.nextPageItem, 1, itemName, Settings.msgButtonNextPageLore);
return ItemUtils.setItemStackNameAndLore(Settings.nextPageItem.createItemStack(), itemName, Settings.msgButtonNextPageLore);
}
protected ItemStack createCurrentPageIcon(int page) {
String itemName = Utils.replaceArgs(Settings.msgButtonCurrentPage,
"{page}", String.valueOf(page),
"{max_page}", String.valueOf(TRADES_MAX_PAGES));
return ItemUtils.createItemStack(Settings.currentPageItem, page, itemName, Settings.msgButtonCurrentPageLore);
return ItemUtils.setItemStackNameAndLore(Settings.currentPageItem.createItemStack(), itemName, Settings.msgButtonCurrentPageLore);
}
protected ItemStack createTradeSetupIcon() {
ShopType<?> shopType = this.getShopkeeper().getType();
String itemName = Utils.replaceArgs(Settings.msgTradeSetupDescHeader,
"{shopType}", shopType.getDisplayName());
return ItemUtils.createItemStack(Settings.tradeSetupItem, 1, itemName, shopType.getTradeSetupDescription());
return ItemUtils.setItemStackNameAndLore(Settings.tradeSetupItem.createItemStack(), itemName, shopType.getTradeSetupDescription());
}
private final List<Button> buttons = new ArrayList<>();

View File

@ -0,0 +1,54 @@
package com.nisovin.shopkeepers.util;
import java.util.Map;
import java.util.Map.Entry;
import org.bukkit.Material;
import org.bukkit.configuration.ConfigurationSection;
public class ConfigUtils {
public static Material loadMaterial(ConfigurationSection config, String key) {
return loadMaterial(config, key, false);
}
public static Material loadMaterial(ConfigurationSection config, String key, boolean checkLegacy) {
String materialName = config.getString(key); // note: takes defaults into account
if (materialName == null) return null;
Material material = Material.matchMaterial(materialName);
if (material == null && checkLegacy) {
// check for legacy material:
String legacyMaterialName = Material.LEGACY_PREFIX + materialName;
material = Material.matchMaterial(legacyMaterialName);
}
return material;
}
// the given top section itself does not get converted
public static void convertSectionsToMaps(ConfigurationSection section) {
for (Entry<String, Object> entry : section.getValues(false).entrySet()) {
Object value = entry.getValue();
if (value instanceof ConfigurationSection) {
// recursively replace sections with maps:
Map<String, Object> innerSectionMap = ((ConfigurationSection) value).getValues(false);
section.set(entry.getKey(), innerSectionMap);
convertSectionsToMaps(innerSectionMap);
}
}
}
public static void convertSectionsToMaps(Map<String, Object> sectionMap) {
for (Entry<String, Object> entry : sectionMap.entrySet()) {
Object value = entry.getValue();
if (value instanceof ConfigurationSection) {
// recursively replace sections with maps:
Map<String, Object> innerSectionMap = ((ConfigurationSection) value).getValues(false);
entry.setValue(innerSectionMap);
convertSectionsToMaps(innerSectionMap);
}
}
}
private ConfigUtils() {
}
}

View File

@ -0,0 +1,307 @@
package com.nisovin.shopkeepers.util;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Consumer;
import org.apache.commons.lang.Validate;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.serialization.ConfigurationSerializable;
import org.bukkit.configuration.serialization.ConfigurationSerialization;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
/**
* An unmodifiable object which stores type and meta data information of an item.
*/
public class ItemData implements Cloneable {
private static final Consumer<String> SILENT_WARNING_HANDLER = (warning) -> {
};
private static final String ITEM_META_SERIALIZATION_KEY = "ItemMeta";
private static final String META_TYPE_KEY = "meta-type";
private static final String DISPLAY_NAME_KEY = "display-name";
private static final String LORE_KEY = "lore";
// special case: omitting 'blockMaterial' for empty TILE_ENTITY item meta
private static final String TILE_ENTITY_BLOCK_MATERIAL_KEY = "blockMaterial";
public static ItemData deserialize(Object dataObject) {
return deserialize(dataObject, null);
}
public static ItemData deserialize(Object dataObject, Consumer<String> warningHandler) {
if (warningHandler == null) {
warningHandler = SILENT_WARNING_HANDLER; // ignore all warnings
}
if (dataObject == null) return null;
String typeName = null;
Map<String, Object> dataMap = null;
if (dataObject instanceof String) {
// load from compact representation (no additional item data):
typeName = (String) dataObject;
assert typeName != null;
} else {
if (dataObject instanceof ConfigurationSection) {
dataMap = ((ConfigurationSection) dataObject).getValues(false);
} else if (dataObject instanceof Map) {
dataMap = (Map<String, Object>) dataObject; // assuming this is safe
} else {
warningHandler.accept("Unknown item data: " + dataObject);
return null;
}
assert dataMap != null;
Object typeData = dataMap.get("type");
if (typeData != null) {
typeName = typeData.toString();
}
if (typeName == null) {
// missing item type information:
warningHandler.accept("Missing item type");
return null;
}
assert typeName != null;
// skip meta data loading if no further data (besides item type) is given:
if (dataMap.size() <= 1) {
dataMap = null;
}
}
assert typeName != null;
// assuming up-to-date material name (performs no conversions besides basic formatting):
Material type = Material.matchMaterial(typeName);
if (type == null) {
// unknown item type:
warningHandler.accept("Unknown item type: " + typeName);
return null;
}
// create item stack (still misses meta data):
ItemStack dataItem = new ItemStack(type);
// load additional meta data:
if (dataMap != null) {
// prepare for meta data deserialization (assumes dataMap is modifiable):
// note: additional information does not need to be removed, but simply gets ignored (eg. item type)
// recursively replace all config sections with maps, since ItemMeta deserialization expects Maps:
ConfigUtils.convertSectionsToMaps(dataMap);
// determine meta type by creating the serialization of a dummy item meta:
ItemMeta dummyItemMeta = dataItem.getItemMeta();
dummyItemMeta.setDisplayName("dummy name"); // ensure item meta is not empty
Object metaType = dummyItemMeta.serialize().get(META_TYPE_KEY);
if (metaType == null) {
throw new IllegalStateException("Couldn't determine meta type with key '" + META_TYPE_KEY + "'!");
}
// insert meta type:
dataMap.put(META_TYPE_KEY, metaType);
// convert color codes for display name and lore:
Object displayNameData = dataMap.get(DISPLAY_NAME_KEY);
if (displayNameData instanceof String) { // also checks for null
dataMap.put(DISPLAY_NAME_KEY, Utils.colorize((String) displayNameData));
}
Object loreData = dataMap.get(LORE_KEY);
if (loreData instanceof List) { // also checks for null
dataMap.put(LORE_KEY, Utils.colorizeUnknown((List<?>) loreData));
}
// deserialize item meta:
// get the class CraftBukkit internally uses for the deserialization:
Class<? extends ConfigurationSerializable> serializableItemMetaClass = ConfigurationSerialization.getClassByAlias(ITEM_META_SERIALIZATION_KEY);
if (serializableItemMetaClass == null) {
throw new IllegalStateException("Missing ItemMeta ConfigurationSerializable class for key/alias '" + ITEM_META_SERIALIZATION_KEY + "'!");
}
// can be null:
ItemMeta itemMeta = (ItemMeta) ConfigurationSerialization.deserializeObject(dataMap, serializableItemMetaClass);
// apply item meta:
dataItem.setItemMeta(itemMeta);
}
// create ItemData:
ItemData itemData = new ItemData(dataItem);
return itemData;
}
/////
private final ItemStack dataItem;
// cache serialized item meta data, to avoid doing it again for every comparison:
private Map<String, Object> serializedData = null; // gets lazily initialized (only when actually needed)
public ItemData(Material type) {
this(new ItemStack(type));
}
public ItemData(Material type, String displayName, List<String> lore) {
this(ItemUtils.createItemStack(type, 1, displayName, lore));
}
public ItemData(ItemStack dataItem) {
Validate.notNull(dataItem, "The given data ItemStack is null!");
this.dataItem = dataItem.clone();
this.dataItem.setAmount(1);
}
public Material getType() {
return dataItem.getType();
}
// Creates a copy of this ItemData, but changes the item type.
// Any incompatible data gets removed.
public ItemData withType(Material type) {
ItemStack newDataItem = this.createItemStack();
newDataItem.setType(type);
return new ItemData(newDataItem);
}
// not null
private Map<String, Object> getSerializedData() {
// lazily cache the serialized data:
if (serializedData == null) {
ItemMeta itemMeta = dataItem.getItemMeta();
// check whether itemMeta is empty; equivalent to ItemStack#hasItemMeta
if (itemMeta != null && !Bukkit.getItemFactory().equals(itemMeta, null)) {
serializedData = itemMeta.serialize(); // assert: not null nor empty
} else {
serializedData = Collections.emptyMap(); // ensure field is not null after initialization
}
}
assert serializedData != null;
return serializedData;
}
public boolean hasItemMeta() {
return !this.getSerializedData().isEmpty(); // equivalent to dataItem.hasItemMeta()
}
public ItemMeta getItemMeta() {
// returns a copy, therefore cannot modify the original data:
return dataItem.getItemMeta();
}
// amount of 1
public ItemStack createItemStack() {
return dataItem.clone();
}
public ItemStack createItemStack(int amount) {
ItemStack item = this.createItemStack();
item.setAmount(amount);
return item;
}
public boolean isSimilar(ItemStack other) {
return dataItem.isSimilar(other);
}
public boolean matches(ItemStack item) {
return this.matches(item, false); // not matching partial lists
}
public boolean matches(ItemStack item, boolean matchPartialLists) {
// same type and matching data:
return ItemUtils.matchesData(item, this.getType(), this.getSerializedData(), matchPartialLists);
}
public boolean matches(ItemData itemData) {
return this.matches(itemData, false); // not matching partial lists
}
// given ItemData is of same type and has data matching this ItemData
public boolean matches(ItemData itemData, boolean matchPartialLists) {
if (itemData == null) return false;
if (itemData.getType() != this.getType()) return false;
return ItemUtils.matchesData(itemData.getSerializedData(), this.getSerializedData(), matchPartialLists);
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("ItemData [data=");
builder.append(dataItem);
builder.append("]");
return builder.toString();
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + dataItem.hashCode();
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (!(obj instanceof ItemData)) return false;
ItemData other = (ItemData) obj;
if (!dataItem.equals(other.dataItem)) return false;
return true;
}
@Override
public ItemData clone() {
return new ItemData(dataItem); // clones the item internally
}
public Object serialize() {
Map<String, Object> serializedData = this.getSerializedData();
if (serializedData.isEmpty()) {
// use a more compact representation if there is no additional item data:
return dataItem.getType().name();
}
Map<String, Object> dataMap = new LinkedHashMap<>();
dataMap.put("type", dataItem.getType().name());
for (Entry<String, Object> entry : serializedData.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
// omitting any data which can be easily restored during deserialization:
// omit meta type key:
if (META_TYPE_KEY.equals(key)) continue;
// omit 'blockMaterial' for empty TILE_ENTITY item meta:
if (TILE_ENTITY_BLOCK_MATERIAL_KEY.equals(key)) {
// check if specific meta type only contains unspecific meta data:
ItemMeta specificItemMeta = dataItem.getItemMeta();
// TODO relies on some material with unspecific item meta
ItemMeta unspecificItemMeta = Bukkit.getItemFactory().asMetaFor(specificItemMeta, Material.STONE);
if (Bukkit.getItemFactory().equals(unspecificItemMeta, specificItemMeta)) {
continue; // skip 'blockMaterial' entry
}
}
// use alternative color codes for display name and lore:
if (DISPLAY_NAME_KEY.equals(key)) {
if (value instanceof String) {
value = Utils.decolorize((String) value);
}
} else if (LORE_KEY.equals(key)) {
if (value instanceof List) {
value = Utils.decolorizeUnknown((List<?>) value);
}
}
// move into data map: avoiding a deep copy, since it is assumed to not be needed
dataMap.put(key, value);
}
return dataMap;
}
// TODO clone
}

View File

@ -2,6 +2,8 @@ package com.nisovin.shopkeepers.util;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.apache.commons.lang.Validate;
import org.bukkit.Bukkit;
@ -66,8 +68,8 @@ public final class ItemUtils {
public static ItemStack createItemStack(Material type, int amount, String displayName, List<String> lore) {
// TODO return null in case of type AIR?
ItemStack item = new ItemStack(type, amount);
return ItemUtils.setItemStackNameAndLore(item, displayName, lore);
ItemStack itemStack = new ItemStack(type, amount);
return ItemUtils.setItemStackNameAndLore(itemStack, displayName, lore);
}
public static ItemStack setItemStackNameAndLore(ItemStack item, String displayName, List<String> lore) {
@ -85,6 +87,19 @@ public final class ItemUtils {
return item;
}
// null to remove display name
public static ItemStack setItemStackName(ItemStack itemStack, String displayName) {
if (itemStack == null) return null;
ItemMeta itemMeta = itemStack.getItemMeta();
if (itemMeta == null) return itemStack;
if (displayName == null && !itemMeta.hasDisplayName()) {
return itemStack;
}
itemMeta.setDisplayName(displayName); // null will clear the display name
itemStack.setItemMeta(itemMeta);
return itemStack;
}
public static ItemStack setLocalizedName(ItemStack item, String locName) {
if (item == null) return null;
ItemMeta meta = item.getItemMeta();
@ -194,10 +209,9 @@ public final class ItemUtils {
public static int getDurability(ItemStack itemStack) {
assert itemStack != null;
if (!itemStack.hasItemMeta()) return 0;
ItemMeta meta = itemStack.getItemMeta();
if (!(meta instanceof Damageable)) return 0;
return ((Damageable) meta).getDamage();
ItemMeta itemMeta = itemStack.getItemMeta();
if (!(itemMeta instanceof Damageable)) return 0; // also checks for null ItemMeta
return ((Damageable) itemMeta).getDamage();
}
public static String getSimpleItemInfo(ItemStack item) {
@ -248,26 +262,22 @@ public final class ItemUtils {
if (item == null) return false;
if (item.getType() != type) return false;
ItemMeta itemMeta = null;
// compare display name:
if (displayName != null && !displayName.isEmpty()) {
if (!item.hasItemMeta()) return false;
itemMeta = item.getItemMeta();
if (itemMeta == null) return false;
boolean checkDisplayName = (displayName != null && !displayName.isEmpty());
boolean checkLore = (lore != null && !lore.isEmpty());
if (!checkDisplayName && !checkLore) return true;
ItemMeta itemMeta = item.getItemMeta();
if (itemMeta == null) return false;
// compare display name:
if (checkDisplayName) {
if (!itemMeta.hasDisplayName() || !displayName.equals(itemMeta.getDisplayName())) {
return false;
}
}
// compare lore:
if (lore != null && !lore.isEmpty()) {
if (itemMeta == null) {
if (!item.hasItemMeta()) return false;
itemMeta = item.getItemMeta();
if (itemMeta == null) return false;
}
if (checkLore) {
if (!itemMeta.hasLore() || !lore.equals(itemMeta.getLore())) {
return false;
}
@ -276,6 +286,116 @@ public final class ItemUtils {
return true;
}
// ITEM DATA MATCHING
public static boolean matchesData(ItemStack item, ItemStack data) {
return matchesData(item, data, false); // not matching partial lists
}
// same type and contains data
public static boolean matchesData(ItemStack item, ItemStack data, boolean matchPartialLists) {
if (item == data) return true;
if (data == null) return true;
if (item == null) return false;
// compare item types:
if (item.getType() != data.getType()) return false;
// check if meta data is contained in item:
return matchesData(item.getItemMeta(), data.getItemMeta(), matchPartialLists);
}
public static boolean matchesData(ItemStack item, Material dataType, Map<String, Object> data, boolean matchPartialLists) {
assert dataType != null;
if (item == null) return false;
if (item.getType() != dataType) return false;
if (data == null || data.isEmpty()) return true;
return matchesData(item.getItemMeta(), data, matchPartialLists);
}
public static boolean matchesData(ItemMeta itemMetaData, ItemMeta dataMetaData) {
return matchesData(itemMetaData, dataMetaData, false); // not matching partial lists
}
// Checks if the meta data contains the other given meta data.
// Similar to minecraft's nbt data matching (trading does not match partial lists, but data specified in commands
// does), but there are a few differences: Minecraft requires explicitly specified empty lists to perfectly match in
// all cases, and some data is treated as list in Minecraft but as map in Bukkit (eg. enchantments). But the
// behavior is the same if not matching partial lists.
public static boolean matchesData(ItemMeta itemMetaData, ItemMeta dataMetaData, boolean matchPartialLists) {
if (itemMetaData == dataMetaData) return true;
if (dataMetaData == null) return true;
if (itemMetaData == null) return false;
// TODO maybe there is a better way of doing this in the future..
Map<String, Object> itemMetaDataMap = itemMetaData.serialize();
Map<String, Object> dataMetaDataMap = dataMetaData.serialize();
return matchesData(itemMetaDataMap, dataMetaDataMap, matchPartialLists);
}
public static boolean matchesData(ItemMeta itemMetaData, Map<String, Object> data, boolean matchPartialLists) {
if (data == null || data.isEmpty()) return true;
if (itemMetaData == null) return false;
Map<String, Object> itemMetaDataMap = itemMetaData.serialize();
return matchesData(itemMetaDataMap, data, matchPartialLists);
}
public static boolean matchesData(Map<String, Object> itemData, Map<String, Object> data, boolean matchPartialLists) {
return _matchesData(itemData, data, matchPartialLists);
}
private static boolean _matchesData(Object target, Object data, boolean matchPartialLists) {
if (target == data) return true;
if (data == null) return true;
if (target == null) return false;
// check if map contains given data:
if (data instanceof Map) {
if (!(target instanceof Map)) return false;
Map<?, ?> targetMap = (Map<?, ?>) target;
Map<?, ?> dataMap = (Map<?, ?>) data;
for (Entry<?, ?> entry : dataMap.entrySet()) {
Object targetValue = targetMap.get(entry.getKey());
if (!_matchesData(targetValue, entry.getValue(), matchPartialLists)) {
return false;
}
}
return true;
}
// check if list contains given data:
if (matchPartialLists && data instanceof List) {
if (!(target instanceof List)) return false;
List<?> targetList = (List<?>) target;
List<?> dataList = (List<?>) data;
// if empty list is explicitly specified, then target list has to be empty as well:
/*if (dataList.isEmpty()) {
return targetList.isEmpty();
}*/
// Avoid loop (TODO: only works if dataList doesn't contain duplicate entries):
if (dataList.size() > targetList.size()) {
return false;
}
for (Object dataEntry : dataList) {
boolean dataContained = false;
for (Object targetEntry : targetList) {
if (_matchesData(targetEntry, dataEntry, matchPartialLists)) {
dataContained = true;
break;
}
}
if (!dataContained) {
return false;
}
}
return true;
}
// check if objects are equal:
return data.equals(target);
}
//
/**
* Increases the amount of the given {@link ItemStack}.
* <p>
@ -346,25 +466,23 @@ public final class ItemUtils {
}
/**
* Checks if the given contents contains at least the specified amount of items matching the specified attributes.
* Checks if the given contents contains at least the specified amount of items matching the specified
* {@link ItemData}.
*
* @param contents
* the contents to search through
* @param type
* the item type
* @param displayName
* the displayName, or <code>null</code> to ignore it
* @param lore
* the item lore, or <code>null</code> or empty to ignore it
* @param itemData
* the item data to match, <code>null</code> will not match any item
* @param amount
* the amount of items to look for
* @return <code>true</code> if the at least specified amount of matching items was found
*/
public static boolean containsAtLeast(ItemStack[] contents, Material type, String displayName, List<String> lore, int amount) {
public static boolean containsAtLeast(ItemStack[] contents, ItemData itemData, int amount) {
if (contents == null) return false;
if (itemData == null) return false; // consider null to match no item here
int remainingAmount = amount;
for (ItemStack itemStack : contents) {
if (!isSimilar(itemStack, type, displayName, lore)) continue;
if (!itemData.matches(itemStack)) continue;
int currentAmount = itemStack.getAmount() - remainingAmount;
if (currentAmount >= 0) {
return true;
@ -376,26 +494,23 @@ public final class ItemUtils {
}
/**
* Removes the specified amount of items which match the specified attributes from the given contents.
* Removes the specified amount of items which match the specified {@link ItemData} from the given contents.
*
* @param contents
* the contents
* @param type
* the item type
* @param displayName
* the display name, or <code>null</code> to ignore it
* @param lore
* the item lore, or <code>null</code> or empty to ignore it
* @param itemData
* the item data to match, <code>null</code> will not match any item
* @param amount
* the amount of matching items to remove
* @return the amount of items that couldn't be removed (<code>0</code> on full success)
*/
public static int removeItems(ItemStack[] contents, Material type, String displayName, List<String> lore, int amount) {
public static int removeItems(ItemStack[] contents, ItemData itemData, int amount) {
if (contents == null) return amount;
if (itemData == null) return amount;
int remainingAmount = amount;
for (int slotId = 0; slotId < contents.length; slotId++) {
ItemStack itemStack = contents[slotId];
if (!isSimilar(itemStack, type, displayName, lore)) continue;
if (!itemData.matches(itemStack)) continue;
int newAmount = itemStack.getAmount() - remainingAmount;
if (newAmount > 0) {
itemStack.setAmount(newAmount);

View File

@ -576,6 +576,20 @@ public final class Utils {
return decolored;
}
// decolorizes string entries, otherwise adopts them as they are
public static List<Object> decolorizeUnknown(List<?> colored) {
if (colored == null) return null;
List<Object> decolored = new ArrayList<>(colored.size());
for (Object entry : colored) {
Object decolorizedEntry = entry;
if (entry instanceof String) {
decolorizedEntry = Utils.translateColorCodesToAlternative(COLOR_CHAR_ALTERNATIVE, (String) entry);
}
decolored.add(decolorizedEntry);
}
return decolored;
}
public static String colorize(String message) {
if (message == null || message.isEmpty()) return message;
return ChatColor.translateAlternateColorCodes(COLOR_CHAR_ALTERNATIVE, message);
@ -590,6 +604,20 @@ public final class Utils {
return colored;
}
// colorizes string entries, otherwise adopts them as they are
public static List<Object> colorizeUnknown(List<?> uncolored) {
if (uncolored == null) return null;
List<Object> colored = new ArrayList<>(uncolored.size());
for (Object entry : uncolored) {
Object colorizedEntry = entry;
if (entry instanceof String) {
colorizedEntry = Utils.colorize((String) entry);
}
colored.add(colorizedEntry);
}
return colored;
}
public static String replaceArgs(String message, String... args) {
if (!StringUtils.isEmpty(message) && args != null && args.length >= 2) {
// replace arguments (key-value replacement):

View File

@ -51,7 +51,7 @@ public class VillagerInteractionListener implements Listener {
Log.debug(" ignoring (probably citizens2) NPC");
return;
}
if ((isVillager && Settings.disableOtherVillagers) || (isWanderingTrader && Settings.disableWanderingTraders)) {
// prevent trading with non-shopkeeper villagers:
event.setCancelled(true);
@ -92,14 +92,14 @@ public class VillagerInteractionListener implements Listener {
ItemStack itemInMainHand = playerInventory.getItemInMainHand();
if (!Settings.isHireItem(itemInMainHand)) {
Utils.sendMessage(player, Settings.msgVillagerForHire, "{costs}", String.valueOf(Settings.hireOtherVillagersCosts),
"{hire-item}", Settings.hireItem.name()); // TODO also print required hire item name and lore?
"{hire-item}", Settings.hireItem.getType().name()); // TODO also print required hire item name and lore?
return false;
} else {
// check if the player has enough of those hiring items
int costs = Settings.hireOtherVillagersCosts;
if (costs > 0) {
ItemStack[] storageContents = playerInventory.getStorageContents();
if (ItemUtils.containsAtLeast(storageContents, Settings.hireItem, Settings.hireItemName, Settings.hireItemLore, costs)) {
if (ItemUtils.containsAtLeast(storageContents, Settings.hireItem, costs)) {
Log.debug(" Villager hiring: the player has the needed amount of hiring items");
int inHandAmount = itemInMainHand.getAmount();
int remaining = inHandAmount - costs;
@ -110,7 +110,7 @@ public class VillagerInteractionListener implements Listener {
playerInventory.setItemInMainHand(null); // remove item in hand
if (remaining < 0) {
// remove remaining costs from inventory:
ItemUtils.removeItems(storageContents, Settings.hireItem, Settings.hireItemName, Settings.hireItemLore, -remaining);
ItemUtils.removeItems(storageContents, Settings.hireItem, -remaining);
// apply the change to the player's inventory:
playerInventory.setStorageContents(storageContents);
}

View File

@ -61,12 +61,9 @@ enable-towny-restrictions: false
# Shop Creation (and removal)
# *~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*
# The item type used to create player shops.
# The item used to create player shops.
shop-creation-item: VILLAGER_SPAWN_EGG
# The display name of the shop-creation item. Empty to ignore.
shop-creation-item-name: ""
# The item lore of the shop-creation item. Empty to ignore.
shop-creation-item-lore: []
# Whether to prevent normal usage of the shop-creation item. Players with the
# bypass permission (usually admins) can bypass this.
prevent-shop-creation-item-regular-usage: false
@ -238,24 +235,24 @@ allow-renaming-of-player-npc-shops: false
# The window title of the shopkeeper editor menu.
editor-title: "Shopkeeper Editor"
# The item types for the buttons and icons in the trades page row.
# The items used for the buttons and icons in the trades page row.
# The display name and lore of these items get set via the corresponding
# messages and can therefore not be defined here.
previous-page-item: WRITABLE_BOOK
next-page-item: WRITABLE_BOOK
current-page-item: WRITABLE_BOOK
trade-setup-item: PAPER
# The item type of the set-name button, and of the naming item (if enabled).
# The item used for the set-name button, and the naming item (if enabled).
name-item: NAME_TAG
# The required item lore of the naming item. Empty to ignore.
name-item-lore: []
# Whether the editor menu of player shops contains an options to open the
# shop's chest.
enable-chest-option-on-player-shop: true
# The item type of the open-chest button.
# The item used for the open-chest button.
chest-item: CHEST
# The item type of the delete button.
# The item used for the delete button.
delete-item: BONE
# *~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*
@ -286,13 +283,9 @@ hire-wandering-traders: false
# Hiring
# *~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*
# The item type to use for the hire button in player shopkeepers that are for
# sale, and for the hire-cost item when hiring non-shopkeeper villagers.
# The item to use for the hire button in player shopkeepers that are for sale,
# and for the hire-cost when hiring non-shopkeeper villagers.
hire-item: EMERALD
# The required item name of the hire-cost item. Empty to ignore.
hire-item-name: ""
# The required item lore of the hire-cost item. Empty to ignore.
hire-item-lore: []
# The amount of hire-cost items it costs to hire a non-shopkeeper villager.
hire-other-villagers-costs: 1
# The title of the hiring inventory window when hiring a player shopkeeper.
@ -337,28 +330,19 @@ tax-round-up: false
# Currencies
# *~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*
# The item type of the currency items used in player shops.
# The item for the currency used in player shops.
currency-item: EMERALD
# The item name of the currency item. Empty to ignore.
currency-item-name: ""
# The item lore of the currency item. Empty to ignore.
currency-item-lore: []
# The item type of the placeholder when a player has not set the cost for an
# item.
# The item for the placeholder when a player has not set the cost for an item.
zero-currency-item: BARRIER
# The item name of the zero-currency item.
zero-currency-item-name: ""
# The item lore of the zero-currency item.
zero-currency-item-lore: []
# The item type of a second, higher-value currency used in the second trading
# slot of player shops. Set to 'AIR' to disable the second currency.
# The item for a second, higher-value currency used in the second trading slot
# of player shops. Set to 'AIR' to disable the second currency.
high-currency-item: EMERALD_BLOCK
# The item name of the second currency item. Empty to ignore.
high-currency-item-name: ""
# The item lore of the second currency item. Empty to ignore.
high-currency-item-lore: []
# The item for the placeholder when a player has not set the second currency
# cost for an item.
high-zero-currency-item: BARRIER
# The value of the second currency, based on the first currency.
high-currency-value: 9
@ -366,14 +350,6 @@ high-currency-value: 9
# value.
high-currency-min-cost: 20
# The item type of the placeholder when a player has not set the second
# currency cost for an item.
high-zero-currency-item: BARRIER
# The item name of the zero-second-currency item. Empty to ignore.
high-zero-currency-item-name: ""
# The item lore of the zero-second-currency item. Empty to ignore.
high-zero-currency-item-lore: []
# *~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*
# Messages
# *~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*

View File

@ -0,0 +1,140 @@
package com.nisovin.shopkeepers;
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadMXBean;
import java.util.List;
import org.bukkit.Material;
import org.bukkit.craftbukkit.v1_14_R1.inventory.CraftItemStack;
import org.bukkit.inventory.ItemStack;
import org.junit.Test;
import com.nisovin.shopkeepers.testutil.AbstractTestBase;
import com.nisovin.shopkeepers.util.ItemData;
import com.nisovin.shopkeepers.util.ItemDataTest;
import com.nisovin.shopkeepers.util.ItemUtils;
import net.minecraft.server.v1_14_R1.GameProfileSerializer;
import net.minecraft.server.v1_14_R1.NBTTagCompound;
public class PerformanceTests extends AbstractTestBase {
public static void testPerformance(String outputPrefix, String testName, int warmupCount, int testCount, Runnable function) {
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
boolean cpuTimeSupported = threadMXBean.isCurrentThreadCpuTimeSupported();
if (!cpuTimeSupported) {
System.out.println(outputPrefix + "Note: Thread cpu time not supported!");
}
// warm up:
for (int i = 0; i < warmupCount; ++i) {
function.run();
}
long start = System.nanoTime();
long cpuTimestart = (cpuTimeSupported ? threadMXBean.getCurrentThreadCpuTime() : 0);
for (int i = 0; i < testCount; ++i) {
function.run();
}
long cpuTimeDuration = (cpuTimeSupported ? threadMXBean.getCurrentThreadCpuTime() : 0) - cpuTimestart;
long duration = System.nanoTime() - start;
System.out.println(outputPrefix + "Duration of '" + testName + "' (" + testCount + " runs): "
+ (duration / 1000000.0D) + " ms (CPU time: " + (cpuTimeDuration / 1000000.0D) + " ms)");
}
@Test
public void testCreateItemStackPerformance() {
System.out.println("Testing ItemStack creation performance:");
int warmupCount = 10000;
int testCount = 1000000;
ItemStack itemStack = ItemDataTest.createItemStackFull();
ItemData itemData = new ItemData(itemStack);
testPerformance(" ", "ItemData#createItemStack", warmupCount, testCount, () -> {
itemData.createItemStack();
});
testPerformance(" ", "ItemStack#clone()", warmupCount, testCount, () -> {
itemStack.clone();
});
CraftItemStack craftItemStack = CraftItemStack.asCraftMirror(CraftItemStack.asNMSCopy(itemStack));
testPerformance(" ", "CraftItemStack#clone()", warmupCount, testCount, () -> {
craftItemStack.clone();
});
}
@Test
public void testIsSimilarPerformance() {
System.out.println("Testing ItemStack isSimilar performance:");
int warmupCount = 10000;
int testCount = 1000000;
ItemStack itemStack = ItemDataTest.createItemStackFull();
ItemStack itemStackCopy = itemStack.clone();
CraftItemStack craftItemStack = CraftItemStack.asCraftMirror(CraftItemStack.asNMSCopy(itemStack));
CraftItemStack craftItemStackCopy = craftItemStack.clone();
testPerformance(" ", "ItemStack#isSimilar(ItemStack)", warmupCount, testCount, () -> {
itemStack.isSimilar(itemStackCopy);
});
testPerformance(" ", "ItemStack#isSimilar(CraftItemStack)", warmupCount, testCount, () -> {
itemStack.isSimilar(craftItemStack);
});
testPerformance(" ", "CraftItemStack#isSimilar(ItemStack)", warmupCount, testCount, () -> {
craftItemStack.isSimilar(itemStack);
});
testPerformance(" ", "CraftItemStack#isSimilar(CraftItemStack)", warmupCount, testCount, () -> {
craftItemStack.isSimilar(craftItemStackCopy);
});
}
@Test
public void testMatchesPerformance() {
System.out.println("Testing ItemStack matching performance:");
int warmupCount = 10000;
int testCount = 1000000;
ItemStack itemStack = ItemDataTest.createItemStackFull();
ItemData itemData = new ItemData(itemStack);
Material type = itemStack.getType();
String displayName = itemStack.getItemMeta().getDisplayName();
List<String> lore = itemStack.getItemMeta().getLore();
CraftItemStack craftItemStack = CraftItemStack.asCraftMirror(CraftItemStack.asNMSCopy(itemStack));
NBTTagCompound tag = CraftItemStack.asNMSCopy(itemStack).getTag();
NBTTagCompound tagCopy = tag.clone();
testPerformance(" ", "comparing name and lore", warmupCount, testCount, () -> {
ItemUtils.isSimilar(itemStack, type, displayName, lore);
});
testPerformance(" ", "ItemData#matches(ItemStack)", warmupCount, testCount, () -> {
itemData.matches(itemStack);
});
testPerformance(" ", "ItemData#matches(CraftItemStack)", warmupCount, testCount, () -> {
itemData.matches(itemStack);
});
testPerformance(" ", "matching NBT tags", warmupCount, testCount, () -> {
GameProfileSerializer.a(tag, tagCopy, false);
});
testPerformance(" ", "matching CraftItemStack with NBT tag", warmupCount, testCount, () -> {
GameProfileSerializer.a(tag, CraftItemStack.asNMSCopy(craftItemStack).getTag(), false);
});
testPerformance(" ", "matching ItemStack with NBT tag", warmupCount, testCount, () -> {
GameProfileSerializer.a(tag, CraftItemStack.asNMSCopy(itemStack).getTag(), false);
});
testPerformance(" ", "matching ItemStack tags", warmupCount, testCount, () -> {
GameProfileSerializer.a(CraftItemStack.asNMSCopy(itemStack).getTag(), CraftItemStack.asNMSCopy(itemStack).getTag(), false);
});
testPerformance(" ", "matching CraftItemStack tags", warmupCount, testCount, () -> {
GameProfileSerializer.a(CraftItemStack.asNMSCopy(craftItemStack).getTag(), CraftItemStack.asNMSCopy(craftItemStack).getTag(), false);
});
}
}

View File

@ -0,0 +1,9 @@
package com.nisovin.shopkeepers.testutil;
public abstract class AbstractTestBase {
static {
// Setup dummy server prior to running tests:
DummyServer.setup();
}
}

View File

@ -0,0 +1,98 @@
package com.nisovin.shopkeepers.testutil;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.Server;
import org.bukkit.craftbukkit.v1_14_R1.block.data.CraftBlockData;
import org.bukkit.craftbukkit.v1_14_R1.inventory.CraftItemFactory;
import org.bukkit.craftbukkit.v1_14_R1.util.CraftMagicNumbers;
import org.bukkit.craftbukkit.v1_14_R1.util.Versioning;
/**
* Mocks the Server (at least the functions required for our tests).
* <p>
* Adopted from CraftBukkit.
*/
public class DummyServer implements InvocationHandler {
private static interface MethodHandler {
Object handle(DummyServer server, Object[] args);
}
private static final Map<Method, MethodHandler> methods = new HashMap<>();
static {
try {
methods.put(Server.class.getMethod("getItemFactory"), new MethodHandler() {
@Override
public Object handle(DummyServer server, Object[] args) {
return CraftItemFactory.instance();
}
});
methods.put(Server.class.getMethod("getName"), new MethodHandler() {
@Override
public Object handle(DummyServer server, Object[] args) {
return DummyServer.class.getName();
}
});
methods.put(Server.class.getMethod("getVersion"), new MethodHandler() {
@Override
public Object handle(DummyServer server, Object[] args) {
return DummyServer.class.getPackage().getImplementationVersion();
}
});
methods.put(Server.class.getMethod("getBukkitVersion"), new MethodHandler() {
@Override
public Object handle(DummyServer server, Object[] args) {
return Versioning.getBukkitVersion();
}
});
methods.put(Server.class.getMethod("getLogger"), new MethodHandler() {
final Logger logger = Logger.getLogger(DummyServer.class.getCanonicalName());
@Override
public Object handle(DummyServer server, Object[] args) {
return logger;
}
});
methods.put(Server.class.getMethod("getUnsafe"), new MethodHandler() {
@Override
public Object handle(DummyServer server, Object[] args) {
return CraftMagicNumbers.INSTANCE;
}
});
methods.put(Server.class.getMethod("createBlockData", Material.class), new MethodHandler() {
@Override
public Object handle(DummyServer server, Object[] args) {
return CraftBlockData.newData((Material) args[0], null);
}
});
// set dummy server:
Bukkit.setServer((Server) Proxy.newProxyInstance(Server.class.getClassLoader(), new Class<?>[] { Server.class }, new DummyServer()));
} catch (Throwable t) {
throw new Error(t);
}
}
public static void setup() {
}
private DummyServer() {
};
@Override
public Object invoke(Object proxy, Method method, Object[] args) {
MethodHandler handler = methods.get(method);
if (handler != null) {
return handler.handle(this, args);
}
throw new UnsupportedOperationException(String.valueOf(method));
}
}

View File

@ -0,0 +1,352 @@
package com.nisovin.shopkeepers.util;
import java.util.Arrays;
import java.util.UUID;
import org.bukkit.ChatColor;
import org.bukkit.Color;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.attribute.Attribute;
import org.bukkit.attribute.AttributeModifier;
import org.bukkit.attribute.AttributeModifier.Operation;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemFlag;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.Damageable;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.inventory.meta.LeatherArmorMeta;
import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.persistence.PersistentDataType;
import org.junit.Assert;
import org.junit.Test;
import com.nisovin.shopkeepers.testutil.AbstractTestBase;
public class ItemDataTest extends AbstractTestBase {
private static void testDeserialization(ItemData originalItemData) {
YamlConfiguration config = new YamlConfiguration();
Object serialized = originalItemData.serialize();
config.set("key", serialized);
String configString = config.saveToString();
YamlConfiguration newConfig = new YamlConfiguration();
try {
newConfig.loadFromString(configString);
} catch (InvalidConfigurationException e) {
}
Object data = newConfig.get("key");
ItemData deserialized = ItemData.deserialize(data);
Assert.assertEquals(originalItemData, deserialized);
}
private static String yamlLineBreak() {
return "\n"; // YAML used unix line breaks by default
}
// COMPACT
private static ItemStack createItemStackSimple() {
ItemStack itemStack = new ItemStack(Material.DIAMOND_SWORD);
return itemStack;
}
@Test
public void testSerializationCompact() {
ItemStack itemStack = createItemStackSimple();
ItemData itemData = new ItemData(itemStack);
Object serialized = itemData.serialize();
Assert.assertEquals("DIAMOND_SWORD", serialized.toString());
}
@Test
public void testYAMLSerializationCompact() {
ItemStack itemStack = createItemStackSimple();
ItemData itemData = new ItemData(itemStack);
Object serialized = itemData.serialize();
YamlConfiguration config = new YamlConfiguration();
config.set("item", serialized);
String yamlString = config.saveToString();
Assert.assertEquals("item: DIAMOND_SWORD" + yamlLineBreak(), yamlString);
}
@Test
public void testDeserializationSimple() {
ItemStack itemStack = createItemStackSimple();
ItemData itemData = new ItemData(itemStack);
testDeserialization(itemData);
}
// MINIMAL
private static ItemStack createItemStackMinimal() {
ItemStack itemStack = new ItemStack(Material.DIAMOND_SWORD);
ItemMeta itemMeta = itemStack.getItemMeta();
itemMeta.setDisplayName(ChatColor.RED + "Custom Name");
itemStack.setItemMeta(itemMeta);
return itemStack;
}
@Test
public void testSerializationMinimal() {
ItemStack itemStack = createItemStackMinimal();
ItemData itemData = new ItemData(itemStack);
Object serialized = itemData.serialize();
Assert.assertEquals("{type=DIAMOND_SWORD, display-name=&cCustom Name}", serialized.toString());
}
@Test
public void testYAMLSerializationMinimal() {
ItemStack itemStack = createItemStackMinimal();
ItemData itemData = new ItemData(itemStack);
Object serialized = itemData.serialize();
YamlConfiguration config = new YamlConfiguration();
config.set("item", serialized);
String yamlString = config.saveToString();
Assert.assertEquals("item:" + yamlLineBreak() +
" type: DIAMOND_SWORD" + yamlLineBreak() +
" display-name: '&cCustom Name'" + yamlLineBreak(), yamlString);
}
@Test
public void testDeserializationMinimal() {
ItemStack itemStack = createItemStackMinimal();
ItemData itemData = new ItemData(itemStack);
testDeserialization(itemData);
}
// FULL
public static ItemStack createItemStackFull() {
ItemStack itemStack = new ItemStack(Material.DIAMOND_SWORD);
ItemMeta itemMeta = itemStack.getItemMeta();
itemMeta.setDisplayName(ChatColor.RED + "Custom Name");
itemMeta.setLore(Arrays.asList(ChatColor.GREEN + "lore1", "lore2"));
itemMeta.addEnchant(Enchantment.DURABILITY, 1, true);
itemMeta.addEnchant(Enchantment.DAMAGE_ALL, 2, true);
itemMeta.setCustomModelData(1);
itemMeta.setLocalizedName("loc name");
itemMeta.setUnbreakable(true);
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
itemMeta.addAttributeModifier(Attribute.GENERIC_ATTACK_SPEED, new AttributeModifier(new UUID(1L, 1L), "attack speed bonus", 2, Operation.ADD_NUMBER, EquipmentSlot.HAND));
itemMeta.addAttributeModifier(Attribute.GENERIC_ATTACK_SPEED, new AttributeModifier(new UUID(2L, 2L), "attack speed bonus 2", 0.5, Operation.MULTIPLY_SCALAR_1, EquipmentSlot.OFF_HAND));
itemMeta.addAttributeModifier(Attribute.GENERIC_MAX_HEALTH, new AttributeModifier(new UUID(3L, 3L), "attack speed bonus", 2, Operation.ADD_NUMBER, EquipmentSlot.HAND));
((Damageable) itemMeta).setDamage(2);
// note: this data ends up getting stored in an arbitrary order internally
PersistentDataContainer customTags = itemMeta.getPersistentDataContainer();
customTags.set(new NamespacedKey("some_plugin", "some-key"), PersistentDataType.STRING, "some value");
PersistentDataContainer customContainer = customTags.getAdapterContext().newPersistentDataContainer();
customContainer.set(new NamespacedKey("inner_plugin", "inner-key"), PersistentDataType.FLOAT, 0.3F);
customTags.set(new NamespacedKey("some_plugin", "some-other-key"), PersistentDataType.TAG_CONTAINER, customContainer);
itemStack.setItemMeta(itemMeta);
return itemStack;
}
@Test
public void testSerializationFull() {
ItemStack itemStack = createItemStackFull();
ItemData itemData = new ItemData(itemStack);
Object serialized = itemData.serialize();
Assert.assertEquals("{type=DIAMOND_SWORD, display-name=&cCustom Name, loc-name=loc name, lore=[&alore1, lore2],"
+ " custom-model-data=1, enchants={DURABILITY=1, DAMAGE_ALL=2}, attribute-modifiers="
+ "{GENERIC_ATTACK_SPEED=[AttributeModifier{uuid=00000000-0000-0001-0000-000000000001, name=attack speed bonus, operation=ADD_NUMBER, amount=2.0, slot=HAND},"
+ " AttributeModifier{uuid=00000000-0000-0002-0000-000000000002, name=attack speed bonus 2, operation=MULTIPLY_SCALAR_1, amount=0.5, slot=OFF_HAND}],"
+ " GENERIC_MAX_HEALTH=[AttributeModifier{uuid=00000000-0000-0003-0000-000000000003, name=attack speed bonus, operation=ADD_NUMBER, amount=2.0, slot=HAND}]},"
+ " ItemFlags=[HIDE_ENCHANTS], Unbreakable=true, Damage=2,"
+ " PublicBukkitValues={some_plugin:some-other-key={inner_plugin:inner-key=0.3f}, some_plugin:some-key=some value}}",
serialized.toString());
}
@Test
public void testYAMLSerializationFull() {
ItemStack itemStack = createItemStackFull();
ItemData itemData = new ItemData(itemStack);
Object serialized = itemData.serialize();
YamlConfiguration config = new YamlConfiguration();
config.set("item", serialized);
String yamlString = config.saveToString();
Assert.assertEquals("item:" + yamlLineBreak() +
" type: DIAMOND_SWORD" + yamlLineBreak() +
" display-name: '&cCustom Name'" + yamlLineBreak() +
" loc-name: loc name" + yamlLineBreak() +
" lore:" + yamlLineBreak() +
" - '&alore1'" + yamlLineBreak() +
" - lore2" + yamlLineBreak() +
" custom-model-data: 1" + yamlLineBreak() +
" enchants:" + yamlLineBreak() +
" DURABILITY: 1" + yamlLineBreak() +
" DAMAGE_ALL: 2" + yamlLineBreak() +
" attribute-modifiers:" + yamlLineBreak() +
" GENERIC_ATTACK_SPEED:" + yamlLineBreak() +
" - ==: org.bukkit.attribute.AttributeModifier" + yamlLineBreak() +
" amount: 2.0" + yamlLineBreak() +
" name: attack speed bonus" + yamlLineBreak() +
" slot: HAND" + yamlLineBreak() +
" uuid: 00000000-0000-0001-0000-000000000001" + yamlLineBreak() +
" operation: 0" + yamlLineBreak() +
" - ==: org.bukkit.attribute.AttributeModifier" + yamlLineBreak() +
" amount: 0.5" + yamlLineBreak() +
" name: attack speed bonus 2" + yamlLineBreak() +
" slot: OFF_HAND" + yamlLineBreak() +
" uuid: 00000000-0000-0002-0000-000000000002" + yamlLineBreak() +
" operation: 2" + yamlLineBreak() +
" GENERIC_MAX_HEALTH:" + yamlLineBreak() +
" - ==: org.bukkit.attribute.AttributeModifier" + yamlLineBreak() +
" amount: 2.0" + yamlLineBreak() +
" name: attack speed bonus" + yamlLineBreak() +
" slot: HAND" + yamlLineBreak() +
" uuid: 00000000-0000-0003-0000-000000000003" + yamlLineBreak() +
" operation: 0" + yamlLineBreak() +
" ItemFlags:" + yamlLineBreak() +
" - HIDE_ENCHANTS" + yamlLineBreak() +
" Unbreakable: true" + yamlLineBreak() +
" Damage: 2" + yamlLineBreak() +
" PublicBukkitValues:" + yamlLineBreak() +
" some_plugin:some-other-key:" + yamlLineBreak() +
" inner_plugin:inner-key: 0.3f" + yamlLineBreak() +
" some_plugin:some-key: some value" + yamlLineBreak(), yamlString);
}
@Test
public void testDeserializationFull() {
ItemStack itemStack = createItemStackFull();
ItemData itemData = new ItemData(itemStack);
testDeserialization(itemData);
}
// UNCOMMON
public static ItemStack createItemStackUncommon() {
ItemStack itemStack = new ItemStack(Material.LEATHER_CHESTPLATE);
LeatherArmorMeta itemMeta = (LeatherArmorMeta) itemStack.getItemMeta();
itemMeta.setDisplayName(ChatColor.RED + "Custom Name");
itemMeta.setColor(Color.BLUE);
itemStack.setItemMeta(itemMeta);
return itemStack;
}
@Test
public void testSerializationUncommon() {
ItemStack itemStack = createItemStackUncommon();
ItemData itemData = new ItemData(itemStack);
Object serialized = itemData.serialize();
Assert.assertEquals("{type=LEATHER_CHESTPLATE, display-name=&cCustom Name, color=Color:[rgb0x00FF]}", serialized.toString());
}
@Test
public void testYAMLSerializationUncommon() {
ItemStack itemStack = createItemStackUncommon();
ItemData itemData = new ItemData(itemStack);
Object serialized = itemData.serialize();
YamlConfiguration config = new YamlConfiguration();
config.set("item", serialized);
String yamlString = config.saveToString();
Assert.assertEquals("item:" + yamlLineBreak() +
" type: LEATHER_CHESTPLATE" + yamlLineBreak() +
" display-name: '&cCustom Name'" + yamlLineBreak() +
" color:" + yamlLineBreak() +
" ==: Color" + yamlLineBreak() +
" RED: 0" + yamlLineBreak() +
" BLUE: 255" + yamlLineBreak() +
" GREEN: 0" + yamlLineBreak(), yamlString);
}
@Test
public void testDeserializationUncommon() {
ItemStack itemStack = createItemStackUncommon();
ItemData itemData = new ItemData(itemStack);
testDeserialization(itemData);
}
// TILE ENTITY SIMPLE
public static ItemStack createItemStackTileEntitySimple() {
ItemStack itemStack = new ItemStack(Material.CHEST);
return itemStack;
}
@Test
public void testSerializationTileEntitySimple() {
ItemStack itemStack = createItemStackTileEntitySimple();
ItemData itemData = new ItemData(itemStack);
Object serialized = itemData.serialize();
Assert.assertEquals("CHEST", serialized.toString());
}
@Test
public void testYAMLSerializationTileEntitySimple() {
ItemStack itemStack = createItemStackTileEntitySimple();
ItemData itemData = new ItemData(itemStack);
Object serialized = itemData.serialize();
YamlConfiguration config = new YamlConfiguration();
config.set("item", serialized);
String yamlString = config.saveToString();
Assert.assertEquals("item: CHEST" + yamlLineBreak(), yamlString);
}
@Test
public void testDeserializationTileEntitySimple() {
ItemStack itemStack = createItemStackTileEntitySimple();
ItemData itemData = new ItemData(itemStack);
testDeserialization(itemData);
}
// TILE ENTITY MINIMAL
public static ItemStack createItemStackTileEntityMinimal() {
ItemStack itemStack = new ItemStack(Material.CHEST);
ItemMeta itemMeta = itemStack.getItemMeta();
itemMeta.setDisplayName(ChatColor.RED + "Custom Name");
itemStack.setItemMeta(itemMeta);
return itemStack;
}
@Test
public void testSerializationTileEntityMinimal() {
ItemStack itemStack = createItemStackTileEntityMinimal();
ItemData itemData = new ItemData(itemStack);
Object serialized = itemData.serialize();
Assert.assertEquals("{type=CHEST, display-name=&cCustom Name}", serialized.toString());
}
@Test
public void testYAMLSerializationTileEntityMinimal() {
ItemStack itemStack = createItemStackTileEntityMinimal();
ItemData itemData = new ItemData(itemStack);
Object serialized = itemData.serialize();
YamlConfiguration config = new YamlConfiguration();
config.set("item", serialized);
String yamlString = config.saveToString();
Assert.assertEquals("item:" + yamlLineBreak() +
" type: CHEST" + yamlLineBreak() +
" display-name: '&cCustom Name'" + yamlLineBreak(), yamlString);
}
@Test
public void testDeserializationTileEntityMinimal() {
ItemStack itemStack = createItemStackTileEntityMinimal();
ItemData itemData = new ItemData(itemStack);
testDeserialization(itemData);
}
// ITEMDATA MATCHES
@Test
public void testItemDataMatches() {
ItemStack itemStack = createItemStackFull();
ItemData itemData = new ItemData(itemStack);
ItemStack differentItemType = itemStack.clone();
differentItemType.setType(Material.IRON_SWORD);
ItemStack differentItemData = ItemUtils.setItemStackName(itemStack.clone(), "different name");
Assert.assertTrue("ItemData#matches(ItemStack)", itemData.matches(itemStack));
Assert.assertTrue("ItemData#matches(ItemData)", itemData.matches(new ItemData(itemStack)));
Assert.assertFalse("!ItemData#matches(different item type)", itemData.matches(new ItemData(differentItemType)));
Assert.assertFalse("!ItemData#matches(different item data)", itemData.matches(new ItemData(differentItemData)));
}
}