Added: It is possible to use barrels and shulker boxes as containers for player shops now.

Also added a message (msg-unsupported-container) when a player tries to select a type of container which is not supported by shopkeepers (i.e. hopper, dropper, dispenser, brewing stand, ender chest, or a type of furnace).

Other related changes:
* API: Deprecated PlayerShopkeeper#getChestX/Y/Z, #get/setChest, #getCurrencyInChest, #openChestWindow and PlayerShopCreationData#getShopChest and added corresponding replacements methods with more general names.
* Various internal renaming related to shop containers.
* Various internal formatting of code comments.

Config changes:
* Changed a few comments inside the default config related to the shop container changes.
* Bumped config version to '3'. A few settings were renamed which get automatically migrated:
  * `require-chest-recently-placed` (now `require-container-recently-placed`)
  * `max-chest-distance` (now `max-container-distance`)
  * `protect-chests` (now `protect-containers`)
  * `delete-shopkeeper-on-break-chest` (now `delete-shopkeeper-on-break-container`)
  * `enable-chest-option-on-player-shop` (now `enable-container-option-on-player-shop`)
  * `chest-item` (now `container-item`)

Added messages:
* msg-unsupported-container

Changed messages:
* Some message settings were renamed. If you don't use a custom / separate language file, they get automatically migrated as part of the config migration to version 3. However, most of these messages also had changes to their default contents which need to be applied manually.
  * msg-button-chest (now msg-button-container)
  * msg-button-chest-lore (now msg-button-container-lore)
  * msg-selected-chest (now msg-container-selected)
  * msg-must-select-chest (now msg-must-select-container)
  * msg-no-chest-selected (now msg-invalid-container)
  * msg-chest-too-far (now msg-container-too-far-away)
  * msg-chest-not-placed (now msg-container-not-placed)
  * msg-chest-already-in-use (now msg-container-already-in-use)
  * msg-no-chest-access (now msg-no-container-access)
  * msg-unused-chest (now msg-unused-container)
  * msg-cant-trade-with-shop-missing-chest (now msg-cant-trade-with-shop-missing-container)
* msg-creation-item-selected
* msg-shop-setup-desc-selling
* msg-shop-setup-desc-buying
* msg-shop-setup-desc-trading
* msg-shop-setup-desc-book
* msg-trade-setup-desc-selling
* msg-trade-setup-desc-buying
* msg-trade-setup-desc-book
master
blablubbabc 2020-07-30 01:50:22 +02:00
parent 6136e93fb3
commit dffbe6bcd9
50 changed files with 1242 additions and 888 deletions

View File

@ -27,18 +27,31 @@ Date format: (YYYY-MM-DD)
* Removed: The legacy permissions `shopkeeper.player.normal`, `shopkeeper.villager`, `shopkeeper.witch` and `shopkeeper.creeper` have been removed. Use the corresponding replacement permissions instead.
* Changed: All players have access to all mob types (permission `shopkeeper.entity.*`) by default now.
* Added: The new 'all' argument for the `/shopkeeper list` command will list all shops now (admin and player shops). (Thanks to Mippy, PR 669)
* Added: It is possible to use barrels and shulker boxes as containers for player shops now.
* Added a message (msg-unsupported-container) when a player tries to select a type of container which is not supported by shopkeepers (i.e. hopper, dropper, dispenser, brewing stand, ender chest, or a type of furnace).
* API: Deprecated PlayerShopkeeper#getChestX/Y/Z, #get/setChest, #getCurrencyInChest, #openChestWindow and PlayerShopCreationData#getShopChest and added corresponding replacements methods with more general names.
Internal changes:
* Slightly changed how we cycle through the villager levels (badge colors).
* Added support for Lists of ItemData inside the config.
* We throw an exception now when we encounter an unexpected / not yet handled config setting type.
* We load all plugin classes up front now. This should avoid issues when the plugin jar gets replaced during runtime (eg. for hot reloads).
* Various internal renaming related to shop containers.
* Various internal formatting of code comments.
Config changes:
* The default value of the `prevent-shop-creation-item-regular-usage` setting was changed to `true`.
* The default value of the `shop-creation-item` setting was changed to a villager spawn egg with display name `&aShopkeeper`. You can give yourself this item in game via the `/shopkeeper give` command.
* The `use-legacy-mob-behavior` setting was broken since MC 1.14 and has been removed now. All shopkeeper entities always use the NoAI flag now.
* The default value of the `enable-citizen-shops` setting was changed to `true`.
* Changed a few comments inside the default config related to the shop container changes.
* Bumped config version to '3'. A few settings were renamed which get automatically migrated:
* `require-chest-recently-placed` (now `require-container-recently-placed`)
* `max-chest-distance` (now `max-container-distance`)
* `protect-chests` (now `protect-containers`)
* `delete-shopkeeper-on-break-chest` (now `delete-shopkeeper-on-break-container`)
* `enable-chest-option-on-player-shop` (now `enable-container-option-on-player-shop`)
* `chest-item` (now `container-item`)
Added messages:
* msg-currency-items-given
@ -54,6 +67,29 @@ Added messages:
* msg-button-slime-size-lore
* msg-button-magma-cube-size
* msg-button-magma-cube-size-lore
* msg-unsupported-container
Changed messages:
* Some message settings were renamed. If you don't use a custom / separate language file, they get automatically migrated as part of the config migration to version 3. However, most of these messages also had changes to their default contents which need to be applied manually.
* msg-button-chest (now msg-button-container)
* msg-button-chest-lore (now msg-button-container-lore)
* msg-selected-chest (now msg-container-selected)
* msg-must-select-chest (now msg-must-select-container)
* msg-no-chest-selected (now msg-invalid-container)
* msg-chest-too-far (now msg-container-too-far-away)
* msg-chest-not-placed (now msg-container-not-placed)
* msg-chest-already-in-use (now msg-container-already-in-use)
* msg-no-chest-access (now msg-no-container-access)
* msg-unused-chest (now msg-unused-container)
* msg-cant-trade-with-shop-missing-chest (now msg-cant-trade-with-shop-missing-container)
* msg-creation-item-selected
* msg-shop-setup-desc-selling
* msg-shop-setup-desc-buying
* msg-shop-setup-desc-trading
* msg-shop-setup-desc-book
* msg-trade-setup-desc-selling
* msg-trade-setup-desc-buying
* msg-trade-setup-desc-book
## v2.10.0 (2020-06-26)
### Supported MC versions: 1.16.1, 1.15.2, 1.14.4

View File

@ -6,7 +6,7 @@ Shopkeepers [![Build Status](https://travis-ci.com/Shopkeepers/Shopkeepers.svg?b
===========
Shopkeepers is a Bukkit plugin which allows you to set up custom villager shopkeepers that sell exactly what you want them to sell and for what price.
You can set up admin shops, which have infinite supply, and you can also set up player shops, which pull supply from a chest.
You can set up admin shops, which have infinite supply, and you can also set up player shops, which pull supply from a container.
**BukkitDev Page**: https://dev.bukkit.org/projects/shopkeepers
**Wiki**: https://github.com/Shopkeepers/Shopkeepers-Wiki/wiki

View File

@ -10,8 +10,8 @@ import com.nisovin.shopkeepers.api.shopkeeper.Shopkeeper;
* This event is called whenever a player is about to (explicitly) delete a {@link Shopkeeper}.
* <p>
* Note: This event may not be called for all actions a player is able to take which may result in the deletion of a
* shopkeeper. For instance, this event is not called when a player shopkeeper is deleted due to the shop's chest being
* broken, or when a Citizens shopkeeper is deleted to a player deleting the corresponding Citizens NPC or trait.
* shopkeeper. For instance, this event is not called when a player shopkeeper is deleted due to the shop's container
* being broken, or when a Citizens shopkeeper is deleted to a player deleting the corresponding Citizens NPC or trait.
* <p>
* If you want to react to all shopkeeper deletions, take a look at {@link ShopkeeperRemoveEvent}.
*/

View File

@ -19,8 +19,8 @@ import com.nisovin.shopkeepers.api.shopkeeper.TradingRecipe;
* All other preconditions regarding the trade have already been checked before this event gets called. So if this event
* does not get cancelled you can assume that the trade is going to get applied.
* <p>
* DO NOT modify the corresponding {@link InventoryClickEvent}, any affected inventories (player, merchant, chest, ..),
* or any other state which might be affected by the trade during the handling of this event!
* DO NOT modify the corresponding {@link InventoryClickEvent}, any affected inventories (player, merchant, shop
* container, ..), or any other state which might be affected by the trade during the handling of this event!
*/
public class ShopkeeperTradeEvent extends ShopkeeperEvent implements Cancellable {

View File

@ -166,8 +166,8 @@ public interface Shopkeeper {
/**
* Gets the shopkeeper's currently available trading recipes for the given player.
* <p>
* Depending on the type of shopkeeper this might access the world data to determine available stock (chest
* contents).<br>
* Depending on the type of shopkeeper this might access the world data to determine the available stock (shop
* container contents).<br>
* Managing (adding, removing, editing, validating) the overall available trading recipes of this shopkeeper might
* differ between different shopkeeper types and is therefore in their responsibility.
* <p>

View File

@ -17,36 +17,52 @@ import com.nisovin.shopkeepers.api.shopobjects.virtual.VirtualShopObjectType;
public class PlayerShopCreationData extends ShopCreationData {
public static PlayerShopCreationData create(Player creator, ShopType<?> shopType, ShopObjectType<?> objectType,
Location spawnLocation, BlockFace targetedBlockFace, Block chest) {
return new PlayerShopCreationData(creator, shopType, objectType, spawnLocation, targetedBlockFace, chest);
Location spawnLocation, BlockFace targetedBlockFace, Block shopContainer) {
return new PlayerShopCreationData(creator, shopType, objectType, spawnLocation, targetedBlockFace, shopContainer);
}
private final Block chest; // not null
private final Block shopContainer; // not null
protected PlayerShopCreationData( Player creator, ShopType<?> shopType, ShopObjectType<?> objectType,
Location spawnLocation, BlockFace targetedBlockFace, Block chest) {
Location spawnLocation, BlockFace targetedBlockFace, Block shopContainer) {
super(creator, shopType, objectType, spawnLocation, targetedBlockFace);
Validate.isTrue(shopType instanceof PlayerShopType, "Shop type has to be a PlayerShopType!");
// chest needs to be located in a world, which is only available for non-virtual shops:
// TODO decouple shopkeeper/shop object location from chest location? (allows chest in different world, and
// virtual player shopkeepers connected to a chest located in a world)
// The shop container needs to be located in a world, which is only available for non-virtual shops:
// TODO Decouple shopkeeper/shop object location from shop container location? (allows containers in different
// world, and virtual player shopkeepers connected to a container located in a world)
Validate.isTrue(!(objectType instanceof VirtualShopObjectType), "Cannot create virtual player shops!");
Validate.notNull(chest, "Chest is null!");
Validate.isTrue(spawnLocation.getWorld().equals(chest.getWorld()),
"Chest is located in a different world than the spawn location!");
Validate.notNull(shopContainer, "shopContainer is null");
Validate.isTrue(spawnLocation.getWorld().equals(shopContainer.getWorld()),
"The shop container is located in a different world than the spawn location!");
// the creator cannot be null for player shopkeepers:
Validate.notNull(creator, "Creator cannot be null!");
this.chest = chest;
this.shopContainer = shopContainer;
}
/**
* The chest which is backing the player shop.
* The container which is backing the player shop.
* <p>
* Has to be located in the same world the shopkeeper.
* <p>
* This does not necessarily have to be a chest, but could be another type of supported shop container as well.
*
* @return the shop chest
* @return the shop container
* @deprecated {@link #getShopContainer()}
*/
public Block getShopChest() {
return chest;
return getShopContainer();
}
/**
* The container which is backing the player shop.
* <p>
* Has to be located in the same world the shopkeeper.
* <p>
* This does not necessarily have to be a chest, but could be another type of supported shop container as well.
*
* @return the shop container
*/
public Block getShopContainer() {
return shopContainer;
}
}

View File

@ -9,8 +9,8 @@ import org.bukkit.inventory.ItemStack;
import com.nisovin.shopkeepers.api.shopkeeper.Shopkeeper;
/**
* A shopkeeper that is managed by a player. This shopkeeper draws its supplies from a chest and will deposit earnings
* back into that chest.
* A shopkeeper that is managed by a player. This shopkeeper draws its supplies from a container and will deposit
* earnings back into that container.
*/
public interface PlayerShopkeeper extends Shopkeeper {
@ -88,32 +88,95 @@ public interface PlayerShopkeeper extends Shopkeeper {
public ItemStack getHireCost();
/**
* Gets the chest's x coordinate.
* Gets the container's x coordinate.
*
* @return the chest's x coordinate
* @return the container's x coordinate
* @deprecated Use {@link #getContainerX()}
*/
public int getChestX();
/**
* Gets the chest's y coordinate.
* Gets the container's y coordinate.
*
* @return the chest's y coordinate
* @return the container's y coordinate
* @deprecated Use {@link #getContainerY()}
*/
public int getChestY();
/**
* Gets the chest's z coordinate.
* Gets the container's z coordinate.
*
* @return the chest's z coordinate.
* @return the container's z coordinate.
* @deprecated Use {@link #getContainerZ()}
*/
public int getChestZ();
public void setChest(int chestX, int chestY, int chestZ);
/**
* @param containerX
* @param containerY
* @param containerZ
* @deprecated Use {@link #setContainer(int, int, int)}
*/
public void setChest(int containerX, int containerY, int containerZ);
/**
*
* @return
* @deprecated Use {@link #getContainer()}
*/
public Block getChest();
/**
*
* @return
* @deprecated Use {@link #getCurrencyInContainer()}
*/
public int getCurrencyInChest();
/**
* Gets the container's x coordinate.
*
* @return the container's x coordinate
*/
public int getContainerX();
/**
* Gets the container's y coordinate.
*
* @return the container's y coordinate
*/
public int getContainerY();
/**
* Gets the container's z coordinate.
*
* @return the container's z coordinate.
*/
public int getContainerZ();
public void setContainer(int containerX, int containerY, int containerZ);
/**
* Gets the block of the shop's container.
* <p>
* This does not necessarily have to be a chest, but could be another type of supported shop container as well.
* <p>
* The block might not actually be a valid container type currently (for example if something has broken or changed
* the type of the block in the meantime).
*
* @return the shop's container block
*/
public Block getContainer();
/**
* Gets the amount of currency stored inside the shop's container.
* <p>
* Returns <code>0</code> if the container does not exist currently.
*
* @return the amount of currency inside the shop's container
*/
public int getCurrencyInContainer();
// SHOPKEEPER UIs - shortcuts for common UI types:
/**
@ -122,17 +185,27 @@ public interface PlayerShopkeeper extends Shopkeeper {
* Fails if this shopkeeper type doesn't support hiring (ex. admin shops).
*
* @param player
* the player requesting the hiring interface
* @return <code>true</code> if the interface was successfully opened for the player
* the player
* @return <code>true</code> if the interface was successfully opened
*/
public boolean openHireWindow(Player player);
/**
* Attempts to open the chest inventory of this shopkeeper for the specified player.
* Attempts to open the container inventory of this shopkeeper for the specified player.
*
* @param player
* the player requesting the chest inventory window
* @return <code>true</code> if the interface was successfully opened for the player
* the player
* @return <code>true</code> if the interface was successfully opened
* @deprecated {@link #openContainerWindow(Player)}
*/
public boolean openChestWindow(Player player);
/**
* Attempts to open the container inventory of this shopkeeper for the specified player.
*
* @param player
* the player
* @return <code>true</code> if the interface was successfully opened
*/
public boolean openContainerWindow(Player player);
}

View File

@ -11,7 +11,7 @@ import com.nisovin.shopkeepers.api.shopkeeper.player.PlayerShopkeeper;
* Sells copies of written books in exchange for currency items.
* <p>
* Books are identified solely based on their title. There exists at most one offer for a certain book. If there are
* multiple books with the same title in the chest, the shopkeeper uses only the first book it finds.
* multiple books with the same title in the shop's container, the shopkeeper uses only the first book it finds.
*/
public interface BookPlayerShopkeeper extends PlayerShopkeeper {

View File

@ -32,12 +32,12 @@ import com.nisovin.shopkeepers.api.shopkeeper.offers.BookOffer;
import com.nisovin.shopkeepers.api.shopkeeper.offers.PriceOffer;
import com.nisovin.shopkeepers.api.shopkeeper.offers.TradingOffer;
import com.nisovin.shopkeepers.api.shopkeeper.player.PlayerShopkeeper;
import com.nisovin.shopkeepers.chestprotection.ProtectedChests;
import com.nisovin.shopkeepers.chestprotection.RemoveShopOnChestBreak;
import com.nisovin.shopkeepers.commands.Commands;
import com.nisovin.shopkeepers.compat.MC_1_16_Utils;
import com.nisovin.shopkeepers.compat.NMSManager;
import com.nisovin.shopkeepers.config.ConfigLoadException;
import com.nisovin.shopkeepers.container.protection.ProtectedContainers;
import com.nisovin.shopkeepers.container.protection.RemoveShopOnContainerBreak;
import com.nisovin.shopkeepers.itemconversion.ItemConversions;
import com.nisovin.shopkeepers.metrics.CitizensChart;
import com.nisovin.shopkeepers.metrics.FeaturesChart;
@ -116,8 +116,8 @@ public class SKShopkeepersPlugin extends JavaPlugin implements ShopkeepersPlugin
private final ShopkeeperNaming shopkeeperNaming = new ShopkeeperNaming(this);
private final ShopkeeperCreation shopkeeperCreation = new ShopkeeperCreation(this);
private final ProtectedChests protectedChests = new ProtectedChests(this);
private final RemoveShopOnChestBreak removeShopOnChestBreak = new RemoveShopOnChestBreak(this, protectedChests);
private final ProtectedContainers protectedContainers = new ProtectedContainers(this);
private final RemoveShopOnContainerBreak removeShopOnContainerBreak = new RemoveShopOnContainerBreak(this, protectedContainers);
private final LivingShops livingShops = new LivingShops(this);
private final SignShops signShops = new SignShops(this);
private final CitizensShops citizensShops = new CitizensShops(this);
@ -346,9 +346,9 @@ public class SKShopkeepersPlugin extends JavaPlugin implements ShopkeepersPlugin
// inform ui registry (registers ui event handlers):
uiRegistry.onEnable();
// enable ProtectedChests:
protectedChests.enable();
removeShopOnChestBreak.onEnable();
// enable container protection:
protectedContainers.enable();
removeShopOnContainerBreak.onEnable();
// register events:
PluginManager pm = Bukkit.getPluginManager();
@ -468,9 +468,9 @@ public class SKShopkeepersPlugin extends JavaPlugin implements ShopkeepersPlugin
// save shopkeepers:
shopkeeperStorage.saveImmediateIfDirty();
// disable protected chests:
protectedChests.disable();
removeShopOnChestBreak.onDisable();
// disable protected containers:
protectedContainers.disable();
removeShopOnContainerBreak.onDisable();
// disable shopkeeper registry: unloads all shopkeepers
shopkeeperRegistry.onDisable();
@ -576,16 +576,16 @@ public class SKShopkeepersPlugin extends JavaPlugin implements ShopkeepersPlugin
return defaultUITypes;
}
// PROTECTED CHESTS:
// PROTECTED CONTAINERS
public ProtectedChests getProtectedChests() {
return protectedChests;
public ProtectedContainers getProtectedContainers() {
return protectedContainers;
}
// SHOPKEEPR REMOVAL ON CHEST BREAKING
// SHOPKEEPR REMOVAL ON CONTAINER BREAKING
public RemoveShopOnChestBreak getRemoveShopOnChestBreak() {
return removeShopOnChestBreak;
public RemoveShopOnContainerBreak getRemoveShopOnContainerBreak() {
return removeShopOnContainerBreak;
}
// LIVING ENTITY SHOPS

View File

@ -97,7 +97,7 @@ public class Settings {
/*
* General Settings
*/
public static int configVersion = 2;
public static int configVersion = 3;
public static boolean debug = false;
// See DebugOptions for all available options.
public static List<String> debugOptions = new ArrayList<>(0);
@ -135,14 +135,14 @@ public class Settings {
public static boolean createPlayerShopWithCommand = false;
public static boolean requireChestRecentlyPlaced = true;
public static int maxChestDistance = 15;
public static boolean requireContainerRecentlyPlaced = true;
public static int maxContainerDistance = 15;
public static int maxShopsPerPlayer = 0;
public static String maxShopsPermOptions = "10,15,25";
public static boolean protectChests = true;
public static boolean protectContainers = true;
public static boolean preventItemMovement = true;
public static boolean deleteShopkeeperOnBreakChest = false;
public static boolean deleteShopkeeperOnBreakContainer = false;
public static int playerShopkeeperInactiveDays = 0;
@ -249,8 +249,8 @@ public class Settings {
public static ItemData nameItem = new ItemData(Material.NAME_TAG);
public static boolean enableChestOptionOnPlayerShop = true;
public static ItemData chestItem = new ItemData(Material.CHEST);
public static boolean enableContainerOptionOnPlayerShop = true;
public static ItemData containerItem = new ItemData(Material.CHEST);
public static ItemData deleteItem = new ItemData(Material.BONE);
@ -326,7 +326,7 @@ public class Settings {
public static Text msgCreationItemSelected = Text.parse("&aShop creation:\n"
+ "&e Left/Right-click to select the shop type.\n"
+ "&e Sneak + left/right-click to select the object type.\n"
+ "&e Right-click a chest to select it.\n"
+ "&e Right-click a container to select it.\n"
+ "&e Then right-click a block to place the shopkeeper.");
public static String msgButtonPreviousPage = "&6<- Previous page ({prev_page} of {max_page})";
@ -338,8 +338,8 @@ public class Settings {
public static String msgButtonName = "&aSet shop name";
public static List<String> msgButtonNameLore = Arrays.asList("Lets you rename", "your shopkeeper");
public static String msgButtonChest = "&aView chest inventory";
public static List<String> msgButtonChestLore = Arrays.asList("Lets you view the inventory", " your shopkeeper is using");
public static String msgButtonContainer = "&aView shop inventory";
public static List<String> msgButtonContainerLore = Arrays.asList("Lets you view the inventory", " your shopkeeper is using");
public static String msgButtonDelete = "&4Delete";
public static List<String> msgButtonDeleteLore = Arrays.asList("Closes and removes", "this shopkeeper");
public static String msgButtonHire = "&aHire";
@ -407,13 +407,14 @@ public class Settings {
public static String msgTradingTitlePrefix = "&2";
public static String msgTradingTitleDefault = "Shopkeeper";
public static Text msgSelectedChest = Text.parse("&aChest selected! Right-click a block to place your shopkeeper.");
public static Text msgMustSelectChest = Text.parse("&7You must right-click a chest before placing your shopkeeper.");
public static Text msgNoChestSelected = Text.parse("&7The selected block is not a chest!");
public static Text msgChestTooFar = Text.parse("&7The shopkeeper's chest is too far away!");
public static Text msgChestNotPlaced = Text.parse("&7You must select a chest you have recently placed!");
public static Text msgChestAlreadyInUse = Text.parse("&7Another shopkeeper is already using the selected chest!");
public static Text msgNoChestAccess = Text.parse("&7You cannot access the selected chest!");
public static Text msgContainerSelected = Text.parse("&aContainer selected! Right-click a block to place your shopkeeper.");
public static Text msgUnsupportedContainer = Text.parse("&7This type of container cannot be used for shops.");
public static Text msgMustSelectContainer = Text.parse("&7You must right-click a container before placing your shopkeeper.");
public static Text msgInvalidContainer = Text.parse("&7The selected block is not a valid container!");
public static Text msgContainerTooFarAway = Text.parse("&7The shopkeeper's container is too far away!");
public static Text msgContainerNotPlaced = Text.parse("&7You must select a container you have recently placed!");
public static Text msgContainerAlreadyInUse = Text.parse("&7Another shopkeeper is already using the selected container!");
public static Text msgNoContainerAccess = Text.parse("&7You cannot access the selected container!");
public static Text msgTooManyShops = Text.parse("&7You have too many shops!");
public static Text msgNoAdminShopTypeSelected = Text.parse("&7You have to select an admin shop type!");
public static Text msgNoPlayerShopTypeSelected = Text.parse("&7You have to select a player shop type!");
@ -434,7 +435,7 @@ public class Settings {
public static Text msgTargetEntityIsNoShop = Text.parse("&7The targeted entity is no shopkeeper.");
public static Text msgTargetShopIsNoAdminShop = Text.parse("&7The targeted shopkeeper is no admin shopkeeper.");
public static Text msgTargetShopIsNoPlayerShop = Text.parse("&7The targeted shopkeeper is no player shopkeeper.");
public static Text msgUnusedChest = Text.parse("&7No shopkeeper is using this chest.");
public static Text msgUnusedContainer = Text.parse("&7No shopkeeper is using this container.");
public static Text msgNotOwner = Text.parse("&7You are not the owner of this shopkeeper.");
// placeholders: {owner} -> new owners name
public static Text msgOwnerSet = Text.parse("&aNew owner was set to &e{owner}");
@ -466,26 +467,26 @@ public class Settings {
public static Text msgMissingCustomTradePerm = Text.parse("&7You do not have the permission to trade with this shop.");
public static Text msgCantTradeWithOwnShop = Text.parse("&7You cannot trade with your own shop.");
public static Text msgCantTradeWhileOwnerOnline = Text.parse("&7You cannot trade while the owner of this shop ('&e{owner}&7') is online.");
public static Text msgCantTradeWithShopMissingChest = Text.parse("&7You cannot trade with this shop, because its chest is missing.");
public static Text msgCantTradeWithShopMissingContainer = Text.parse("&7You cannot trade with this shop, because its container is missing.");
public static Text msgShopkeeperCreated = Text.parse("&aShopkeeper created: &6{type} &7({description})\n{setupDesc}");
public static String msgShopSetupDescSelling = "&e Add items you want to sell to your chest, then\n"
public static String msgShopSetupDescSelling = "&e Add items you want to sell to your container, then\n"
+ "&e right-click the shop while sneaking to modify costs.";
public static String msgShopSetupDescBuying = "&e Add one of each item you want to buy to your chest, then\n"
public static String msgShopSetupDescBuying = "&e Add one of each item you want to buy to your container,\n"
+ "&e then right-click the shop while sneaking to modify costs.";
public static String msgShopSetupDescTrading = "&e Add items you want to sell to your container, then\n"
+ "&e right-click the shop while sneaking to modify costs.";
public static String msgShopSetupDescTrading = "&e Add items you want to sell to your chest, then\n"
+ "&e right-click the shop while sneaking to modify costs.";
public static String msgShopSetupDescBook = "&e Add written books and blank books to your chest, then\n"
public static String msgShopSetupDescBook = "&e Add written books and blank books to your container, then\n"
+ "&e right-click the shop while sneaking to modify costs.";
public static String msgShopSetupDescAdminRegular = "&e Right-click the shop while sneaking to modify trades.";
public static String msgTradeSetupDescHeader = "&6{shopType}";
public static List<String> msgTradeSetupDescAdminRegular = Arrays.asList("Has unlimited stock.", "Insert items from your inventory.", "Top row: Result items", "Bottom rows: Cost items");
public static List<String> msgTradeSetupDescSelling = Arrays.asList("Sells items.", "Insert items to sell into the chest.", "Left/Right click to adjust amounts.", "Top row: Items being sold", "Bottom rows: Cost items");
public static List<String> msgTradeSetupDescBuying = Arrays.asList("Buys items.", "Insert one of each item you want to", "buy and plenty of currency items", "into the chest.", "Left/Right click to adjust amounts.", "Top row: Cost items", "Bottom row: Items being bought");
public static List<String> msgTradeSetupDescSelling = Arrays.asList("Sells items.", "Insert items to sell into the container.", "Left/Right click to adjust amounts.", "Top row: Items being sold", "Bottom rows: Cost items");
public static List<String> msgTradeSetupDescBuying = Arrays.asList("Buys items.", "Insert one of each item you want to", "buy and plenty of currency items", "into the container.", "Left/Right click to adjust amounts.", "Top row: Cost items", "Bottom row: Items being bought");
public static List<String> msgTradeSetupDescTrading = Arrays.asList("Trades items.", "Pickup an item from your inventory", "and then click a slot to place it.", "Left/Right click to adjust amounts.", "Top row: Result items", "Bottom rows: Cost items");
public static List<String> msgTradeSetupDescBook = Arrays.asList("Sells book copies.", "Insert written and blank books", "into the chest.", "Left/Right click to adjust costs.", "Top row: Books being sold", "Bottom rows: Cost items");
public static List<String> msgTradeSetupDescBook = Arrays.asList("Sells book copies.", "Insert written and blank books", "into the container.", "Left/Right click to adjust costs.", "Top row: Books being sold", "Bottom rows: Cost items");
public static Text msgListAdminShopsHeader = Text.parse("&9There are &e{shopsCount} &9admin shops: &e(Page {page} of {maxPage})");
public static Text msgListAllShopsHeader = Text.parse("&9There are &e{shopsCount} &9shops in total: &e(Page {page} of {maxPage})");
@ -632,9 +633,9 @@ public class Settings {
Log.warning("Config: All existing entity type names can be found here: https://hub.spigotmc.org/javadocs/spigot/org/bukkit/entity/EntityType.html");
}
if (maxChestDistance > 50) {
Log.warning("Config: 'max-chest-distance' can be at most 50.");
maxChestDistance = 50;
if (maxContainerDistance > 50) {
Log.warning("Config: 'max-container-distance' can be at most 50.");
maxContainerDistance = 50;
}
if (gravityChunkRange < 0) {
Log.warning("Config: 'gravity-chunk-range' cannot be negative.");
@ -838,7 +839,7 @@ public class Settings {
// button items:
public static ItemData nameButtonItem = new ItemData(Material.AIR);
public static ItemData chestButtonItem = new ItemData(Material.AIR);
public static ItemData containerButtonItem = new ItemData(Material.AIR);
public static ItemData deleteButtonItem = new ItemData(Material.AIR);
public static ItemData hireButtonItem = new ItemData(Material.AIR);
@ -851,7 +852,7 @@ public class Settings {
// button items:
nameButtonItem = new ItemData(ItemUtils.setItemStackNameAndLore(nameItem.createItemStack(), msgButtonName, msgButtonNameLore));
chestButtonItem = new ItemData(ItemUtils.setItemStackNameAndLore(chestItem.createItemStack(), msgButtonChest, msgButtonChestLore));
containerButtonItem = new ItemData(ItemUtils.setItemStackNameAndLore(containerItem.createItemStack(), msgButtonContainer, msgButtonContainerLore));
deleteButtonItem = new ItemData(ItemUtils.setItemStackNameAndLore(deleteItem.createItemStack(), msgButtonDelete, msgButtonDeleteLore));
hireButtonItem = new ItemData(ItemUtils.setItemStackNameAndLore(hireItem.createItemStack(), msgButtonHire, msgButtonHireLore));
@ -883,9 +884,9 @@ public class Settings {
return DerivedSettings.nameButtonItem.createItemStack();
}
// chest button:
public static ItemStack createChestButtonItem() {
return DerivedSettings.chestButtonItem.createItemStack();
// Container button:
public static ItemStack createContainerButtonItem() {
return DerivedSettings.containerButtonItem.createItemStack();
}
// delete button:

View File

@ -1,314 +0,0 @@
package com.nisovin.shopkeepers.chestprotection;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.block.data.type.Chest;
import org.bukkit.block.data.type.Chest.Type;
import org.bukkit.entity.Player;
import org.bukkit.event.HandlerList;
import com.nisovin.shopkeepers.SKShopkeepersPlugin;
import com.nisovin.shopkeepers.Settings;
import com.nisovin.shopkeepers.api.ShopkeepersPlugin;
import com.nisovin.shopkeepers.api.shopkeeper.player.PlayerShopkeeper;
import com.nisovin.shopkeepers.util.ItemUtils;
import com.nisovin.shopkeepers.util.PermissionUtils;
import com.nisovin.shopkeepers.util.Validate;
/**
* <b>Chest protection:</b>
* <p>
* <b>Protected chests:</b><br>
* A chest directly used by a player shopkeeper is 'directly protected'. Any adjacent chests (N, W, S, E) that are
* connected to a directly protected chest are protected as well.
* <p>
* <b>Bypass:</b><br>
* The owner of a shop corresponding to a protected chest and players with the bypass permission are not affected by the
* listed protections. Also, certain protections can be disabled via config settings.
* <p>
* <b>Protections:</b><br>
* <ul>
* <li>Protected chests cannot be accessed.
* <li>Protected chests cannot be broken or destroyed by explosions.
* <li>Protected chests don't transfer items, ex. into or from hoppers, droppers, etc.
* <li>Chests cannot be placed adjacent to a directly protected chest, if they would connect to it.
* <li>Droppers cannot be placed adjacent to a protected chest, if they would be able to receive or inject items.
* <li>Hoppers cannot be placed adjacent to a protected chest, if they would be able to inject items.
* <li>Rails cannot be placed below a protected chest (to prevent hopper carts stealing items).
* </ul>
* Note that the following cases are not protected for:
* <ul>
* <li>Even though rails cannot be placed below protected chests, hopper carts are still able to be placed / maneuvered
* to end up below protected chests. The item movement protection will however still prevent items from being extracted
* from the chest.
* <li>Adjacent unconnected chests are not protected. It should however not be possible to access the protected adjacent
* chest by that.
* <li>Adjacent or chains of droppers and hoppers are not protected. So if item movement is enabled in the config and
* the shop owner places those connected to his shop chest, other players will be able to access (or break) them.
* </ul>
*/
public class ProtectedChests {
private final SKShopkeepersPlugin plugin;
private final ChestProtectionListener chestProtectionListener = new ChestProtectionListener(this);
private final InventoryMoveItemListener inventoryMoveItemListener = new InventoryMoveItemListener(this);
// player shopkeepers by location key:
private final Map<String, List<PlayerShopkeeper>> protectedChests = new HashMap<>();
public ProtectedChests(SKShopkeepersPlugin plugin) {
this.plugin = plugin;
}
public void enable() {
if (Settings.protectChests) {
Bukkit.getPluginManager().registerEvents(chestProtectionListener, plugin);
if (Settings.preventItemMovement) {
Bukkit.getPluginManager().registerEvents(inventoryMoveItemListener, plugin);
}
}
}
public void disable() {
// cleanup:
HandlerList.unregisterAll(chestProtectionListener);
HandlerList.unregisterAll(inventoryMoveItemListener);
protectedChests.clear();
}
private String getKey(String worldName, int x, int y, int z) {
return worldName + ";" + x + ";" + y + ";" + z;
}
public void addChest(String worldName, int x, int y, int z, PlayerShopkeeper shopkeeper) {
Validate.notNull(shopkeeper);
String key = this.getKey(worldName, x, y, z);
List<PlayerShopkeeper> shopkeepers = protectedChests.get(key);
if (shopkeepers == null) {
shopkeepers = new ArrayList<>(1);
protectedChests.put(key, shopkeepers);
}
shopkeepers.add(shopkeeper);
}
public void removeChest(String worldName, int x, int y, int z, PlayerShopkeeper shopkeeper) {
Validate.notNull(shopkeeper);
String key = this.getKey(worldName, x, y, z);
List<PlayerShopkeeper> shopkeepers = protectedChests.get(key);
if (shopkeepers == null) return;
shopkeepers.remove(shopkeeper);
if (shopkeepers.isEmpty()) {
protectedChests.remove(key);
}
}
// gets the shopkeepers which are directly using the chest at the specified location
private List<PlayerShopkeeper> _getShopkeepers(String worldName, int x, int y, int z) {
String key = this.getKey(worldName, x, y, z);
return protectedChests.get(key);
}
// gets the shopkeepers which are directly using the specified chest block
private List<PlayerShopkeeper> _getShopkeepers(Block block) {
return this._getShopkeepers(block.getWorld().getName(), block.getX(), block.getY(), block.getZ());
}
//
// gets the shopkeepers which are directly using the chest at the specified location:
public List<PlayerShopkeeper> getShopkeepers(String worldName, int x, int y, int z) {
List<PlayerShopkeeper> shopkeepers = this._getShopkeepers(worldName, x, y, z);
return (shopkeepers == null ? Collections.emptyList() : Collections.unmodifiableList(shopkeepers));
}
public List<PlayerShopkeeper> getShopkeepers(Block block) {
return this.getShopkeepers(block.getWorld().getName(), block.getX(), block.getY(), block.getZ());
}
//
// checks if this exact block is protected
public boolean isChestDirectlyProtected(String worldName, int x, int y, int z, Player player) {
List<PlayerShopkeeper> shopkeepers = this._getShopkeepers(worldName, x, y, z);
// is there any shopkeeper using this chest?
if (shopkeepers == null) return false;
assert !shopkeepers.isEmpty();
if (player != null) {
// check whether the player is affected by the protection:
// note: the bypass permission does not get checked here but needs to be checked separately
// always allow shop owners to access their shop chest (regardless of other shopkeepers using the same
// chest):
for (PlayerShopkeeper shopkeeper : shopkeepers) {
if (shopkeeper.isOwner(player)) return false;
}
}
// there exists a protection for this chest, and the player doesn't own any shopkeeper using the chest:
return true;
}
public boolean isChestDirectlyProtected(Block block, Player player) {
return this.isChestDirectlyProtected(block.getWorld().getName(), block.getX(), block.getY(), block.getZ(), player);
}
//
// gets reused by isChestProtected calls:
private final List<PlayerShopkeeper> tempResultsList = new ArrayList<>();
/**
* Checks if the given chest block is protected.
* <p>
* The block is protected if either:
* <ul>
* <li>The block is directly used by a shopkeeper.
* <li>The block is a chest that is connected to a block that is directly used by a shopkeeper.
* </ul>
*
* @param chest
* the chest block (the block might not actually be a chest right now though)
* @param player
* the player to check the protection for, or <code>null</code> to check for protection without taking
* shop owners into account
* @return <code>true</code> if the block is protected
*/
public boolean isChestProtected(Block chest, Player player) {
Validate.notNull(chest, "Chest block is null!");
// reuse logic from getShopkeeperOwnersOfChest:
this.getShopkeepersUsingChest(chest, tempResultsList);
if (tempResultsList.isEmpty()) {
// no protection found:
return false;
} else {
// protection found:
boolean result = true;
// check if the player is affected by the protection:
if (player != null) {
// note: the bypass permission does not get checked here but needs to be checked separately
// always allow shop owners to access their shop chest (regardless of other shopkeepers using the same
// chest):
for (PlayerShopkeeper shopkeeper : tempResultsList) {
if (shopkeeper.isOwner(player)) {
result = false;
break;
}
}
}
// cleanup temporary results list:
tempResultsList.clear();
return result;
}
}
/**
* Checks if the given block is a protected chest.
* <p>
* This makes sure that the specified block is actually a chest and it takes the bypass permission into account.
*
* @param block
* the block
* @param player
* the player to check the protection for, or <code>null</code> to check for protection without taking
* shop owners and players with bypass permission into account
* @return <code>true</code> if the block is a protected chest
*/
public boolean isProtectedChest(Block block, Player player) {
if (block == null || !ItemUtils.isChest(block.getType())) return false;
if (!this.isChestProtected(block, player)) return false;
if (player != null && PermissionUtils.hasPermission(player, ShopkeepersPlugin.BYPASS_PERMISSION)) return false;
return true;
}
// gets the shopkeepers which use the chest at the given location (directly or by a connected chest):
public List<PlayerShopkeeper> getShopkeepersUsingChest(Block chest) {
return this.getShopkeepersUsingChest(chest, null);
}
// gets the shopkeepers which use the chest at the given location (directly or by a connected chest), and adds them
// to the provided list:
private List<PlayerShopkeeper> getShopkeepersUsingChest(Block chest, List<PlayerShopkeeper> results) {
Validate.notNull(chest, "Chest block is null!");
// create results list if none is provided:
if (results == null) {
results = new ArrayList<>();
}
// checking if this block is used directly by shopkeepers:
List<PlayerShopkeeper> shopkeepers = this._getShopkeepers(chest);
if (shopkeepers != null) {
assert !shopkeepers.isEmpty();
results.addAll(shopkeepers);
}
// if the block is actually a chest, check for a connected chest:
Material chestType = chest.getType();
if (ItemUtils.isChest(chestType)) {
Chest chestData = (Chest) chest.getBlockData();
BlockFace chestFacing = chestData.getFacing();
BlockFace connectedFace = getConnectedBlockFace(chestFacing, chestData.getType());
if (connectedFace != null) {
Block connectedChest = chest.getRelative(connectedFace);
// In case of inconsistency of the block data (i.e. connected chest missing or not mutually connected),
// we consider the block to be connected (and by that protected) anyways, because such inconsistencies
// might also occur during handling of block placements.
// Minecraft determines double chests by these consistency criteria:
// Same chest type, same facing, opposite chest type (opposite connected block faces)
shopkeepers = this._getShopkeepers(connectedChest);
if (shopkeepers != null) {
results.addAll(shopkeepers);
}
}
}
return results;
}
private static BlockFace getConnectedBlockFace(BlockFace chestFacing, Type chestType) {
switch (chestFacing) {
case NORTH:
switch (chestType) {
case RIGHT:
return BlockFace.WEST;
case LEFT:
return BlockFace.EAST;
default:
return null; // not connected
}
case EAST:
switch (chestType) {
case RIGHT:
return BlockFace.NORTH;
case LEFT:
return BlockFace.SOUTH;
default:
return null; // not connected
}
case SOUTH:
switch (chestType) {
case RIGHT:
return BlockFace.EAST;
case LEFT:
return BlockFace.WEST;
default:
return null; // not connected
}
case WEST:
switch (chestType) {
case RIGHT:
return BlockFace.SOUTH;
case LEFT:
return BlockFace.NORTH;
default:
return null; // not connected
}
default:
return null; // invalid chest facing
}
}
}

View File

@ -1,69 +0,0 @@
package com.nisovin.shopkeepers.chestprotection;
import java.util.List;
import org.bukkit.Bukkit;
import org.bukkit.block.Block;
import org.bukkit.event.HandlerList;
import org.bukkit.inventory.ItemStack;
import com.nisovin.shopkeepers.SKShopkeepersPlugin;
import com.nisovin.shopkeepers.Settings;
import com.nisovin.shopkeepers.api.shopkeeper.player.PlayerShopkeeper;
import com.nisovin.shopkeepers.util.ItemUtils;
public class RemoveShopOnChestBreak {
private final SKShopkeepersPlugin plugin;
private final ProtectedChests protectedChests;
private final RemoveShopOnChestBreakListener removeShopOnChestBreakListener;
public RemoveShopOnChestBreak(SKShopkeepersPlugin plugin, ProtectedChests protectedChests) {
this.plugin = plugin;
this.protectedChests = protectedChests;
removeShopOnChestBreakListener = new RemoveShopOnChestBreakListener(plugin, this);
}
public void onEnable() {
if (Settings.deleteShopkeeperOnBreakChest) {
Bukkit.getPluginManager().registerEvents(removeShopOnChestBreakListener, plugin);
}
}
public void onDisable() {
HandlerList.unregisterAll(removeShopOnChestBreakListener);
}
// does not trigger saving on its own, returns true if there were shopkeepers using the chest, that got removed now
// does not check the delete-shopkeeper-on-break-chest setting, this has to be checked by clients beforehand
public boolean handleBlockBreakage(Block block) {
List<PlayerShopkeeper> shopkeepers = protectedChests.getShopkeepers(block);
if (shopkeepers.isEmpty()) return false;
// copy to deal with concurrent modifications:
for (PlayerShopkeeper shopkeeper : shopkeepers.toArray(new PlayerShopkeeper[shopkeepers.size()])) {
if (!shopkeeper.isValid()) continue; // skip if no longer valid
// return creation item for player shopkeepers:
if (Settings.deletingPlayerShopReturnsCreationItem) {
ItemStack shopCreationItem = Settings.createShopCreationItem();
block.getWorld().dropItemNaturally(block.getLocation(), shopCreationItem);
}
// Note: We do not pass the player responsible for breaking the chest here, because we cannot determine the
// player in all situations anyways (eg. if a player indirectly breaks the chest by causing an explosion).
shopkeeper.delete();
}
return true;
}
public void handleBlocksBreakage(List<Block> blockList) {
boolean dirty = false;
for (Block block : blockList) {
if (ItemUtils.isChest(block.getType()) && this.handleBlockBreakage(block)) {
dirty = true;
}
}
if (dirty) {
plugin.getShopkeeperStorage().save();
}
}
}

View File

@ -33,7 +33,7 @@ import com.nisovin.shopkeepers.commands.lib.CommandInput;
import com.nisovin.shopkeepers.commands.lib.CommandRegistry;
import com.nisovin.shopkeepers.commands.lib.PlayerCommand;
import com.nisovin.shopkeepers.commands.lib.arguments.OptionalArgument;
import com.nisovin.shopkeepers.util.ItemUtils;
import com.nisovin.shopkeepers.container.ShopContainers;
import com.nisovin.shopkeepers.util.PermissionUtils;
import com.nisovin.shopkeepers.util.TextUtils;
@ -123,7 +123,7 @@ public class ShopkeepersCommand extends BaseCommand {
ShopType<?> shopType = context.get(ARGUMENT_SHOP_TYPE);
ShopObjectType<?> shopObjType = context.get(ARGUMENT_OBJECT_TYPE);
boolean createPlayerShop = (Settings.createPlayerShopWithCommand && ItemUtils.isChest(targetBlock.getType()));
boolean createPlayerShop = (Settings.createPlayerShopWithCommand && ShopContainers.isSupportedContainer(targetBlock.getType()));
if (createPlayerShop) {
// create player shopkeeper:

View File

@ -0,0 +1,47 @@
package com.nisovin.shopkeepers.config.migration;
import org.bukkit.configuration.Configuration;
import com.nisovin.shopkeepers.util.Log;
/**
* Migrates the config from version 2 to version 3.
*/
public class ConfigMigration3 implements ConfigMigration {
@Override
public void apply(Configuration config) {
// Convert any settings with 'chest' in their name to new settings with 'container' in their name:
migrateSetting(config, "require-chest-recently-placed", "require-container-recently-placed");
migrateSetting(config, "max-chest-distance", "max-container-distance");
migrateSetting(config, "protect-chests", "protect-containers");
migrateSetting(config, "delete-shopkeeper-on-break-chest", "delete-shopkeeper-on-break-container");
migrateSetting(config, "enable-chest-option-on-player-shop", "enable-container-option-on-player-shop");
migrateSetting(config, "chest-item", "container-item");
// Note: Most of the following message also had changes to their contents which need to be applied manually.
// We migrate them here anyways, so that any old message settings get removed from the config and thereby don't
// cause confusion by there being many no longer used message settings. Note that this migration is not invoked
// for custom / separate language files.
migrateSetting(config, "msg-button-chest", "msg-button-container");
migrateSetting(config, "msg-button-chest-lore", "msg-button-container-lore");
migrateSetting(config, "msg-selected-chest", "msg-container-selected");
migrateSetting(config, "msg-must-select-chest", "msg-must-select-container");
migrateSetting(config, "msg-no-chest-selected", "msg-invalid-container");
migrateSetting(config, "msg-chest-too-far", "msg-container-too-far-away");
migrateSetting(config, "msg-chest-not-placed", "msg-container-not-placed");
migrateSetting(config, "msg-chest-already-in-use", "msg-container-already-in-use");
migrateSetting(config, "msg-no-chest-access", "msg-no-container-access");
migrateSetting(config, "msg-unused-chest", "msg-unused-container");
migrateSetting(config, "msg-cant-trade-with-shop-missing-chest", "msg-cant-trade-with-shop-missing-container");
}
private static void migrateSetting(Configuration config, String oldKey, String newKey) {
assert config != null && oldKey != null && newKey != null;
if (config.isSet(oldKey) && !config.isSet(newKey)) {
Object oldValue = config.get(oldKey);
Log.info(" Migrating setting '" + oldKey + "' to '" + newKey + "'. Value: " + oldValue);
config.set(newKey, oldValue);
config.set(oldKey, null);
}
}
}

View File

@ -15,7 +15,11 @@ public class ConfigMigrations {
private static final int FIRST_VERSION = 0; // also applies if the config version is missing
// 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());
private static final List<ConfigMigration> migrations = Arrays.asList(
new ConfigMigration1(),
new ConfigMigration2(),
new ConfigMigration3()
);
public static int getLatestVersion() {
return migrations.size();

View File

@ -0,0 +1,55 @@
package com.nisovin.shopkeepers.container;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.block.BlockState;
import org.bukkit.block.Container;
import org.bukkit.inventory.Inventory;
import com.nisovin.shopkeepers.util.ItemUtils;
import com.nisovin.shopkeepers.util.Validate;
/**
* Utilities related to shop containers.
*/
public class ShopContainers {
private ShopContainers() {
}
/**
* Checks if the given material is a supported shop container.
*
* @param material
* the material
* @return <code>true</code> if the material is a supported shop container
*/
public static boolean isSupportedContainer(Material material) {
return ItemUtils.isChest(material)
|| material == Material.BARREL
|| ItemUtils.isShulkerBox(material);
}
/**
* Gets the {@link Inventory} of a supported type of shop container block.
* <p>
* For double chests this returns the complete double chest inventory.
* <p>
* The returned inventory is directly backed by the container in the world and any changes to the inventory are
* therefore directly reflected.
*
* @param containerBlock
* the container block
* @return the inventory
*/
public static Inventory getInventory(Block containerBlock) {
Validate.notNull(containerBlock, "containerBlock is null");
Validate.isTrue(isSupportedContainer(containerBlock.getType()),
() -> "containerBlock is of unsupported type: " + containerBlock.getType());
BlockState state = containerBlock.getState();
assert state instanceof Container;
Container container = (Container) state;
// Note: For double chests this returns the complete double chest inventory.
return container.getInventory();
}
}

View File

@ -1,4 +1,4 @@
package com.nisovin.shopkeepers.chestprotection;
package com.nisovin.shopkeepers.container.protection;
import java.util.Iterator;
import java.util.List;
@ -23,29 +23,28 @@ import com.nisovin.shopkeepers.util.Log;
import com.nisovin.shopkeepers.util.TextUtils;
/**
* Handles chest protection. Can be disabled via a config setting.
* Handles container protection. Can be disabled via a config setting.
*/
class ChestProtectionListener implements Listener {
class ContainerProtectionListener implements Listener {
private final ProtectedChests protectedChests;
private final ProtectedContainers protectedContainers;
ChestProtectionListener(ProtectedChests protectedChests) {
this.protectedChests = protectedChests;
ContainerProtectionListener(ProtectedContainers protectedContainers) {
this.protectedContainers = protectedContainers;
}
/**
* Prevents unauthorized opening of protected chests.
* Prevents unauthorized opening of protected containers.
*/
@EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
void onPlayerInteract(PlayerInteractEvent event) {
// prevent opening shop chests
if (event.getAction() != Action.RIGHT_CLICK_BLOCK) return;
Block block = event.getClickedBlock();
Player player = event.getPlayer();
if (protectedChests.isProtectedChest(block, player)) {
// TODO always allow access to own shop chests, even if cancelled by other plugins?
Log.debug(() -> "Cancelled chest opening by '" + player.getName() + "' at '"
+ TextUtils.getLocationString(block) + "': Protected chest");
if (protectedContainers.isProtectedContainer(block, player)) {
// TODO always allow access to own shop containers, even if cancelled by other plugins?
Log.debug(() -> "Cancelled container opening by '" + player.getName() + "' at '"
+ TextUtils.getLocationString(block) + "': Protected container.");
event.setCancelled(true);
}
}
@ -54,9 +53,9 @@ class ChestProtectionListener implements Listener {
void onBlockBreak(BlockBreakEvent event) {
Block block = event.getBlock();
Player player = event.getPlayer();
if (protectedChests.isProtectedChest(block, player)) {
Log.debug(() -> "Cancelled breaking of chest block by '" + player.getName() + "' at '"
+ TextUtils.getLocationString(block) + "': Protected chest");
if (protectedContainers.isProtectedContainer(block, player)) {
Log.debug(() -> "Cancelled breaking of container block by '" + player.getName() + "' at '"
+ TextUtils.getLocationString(block) + "': Protected container.");
event.setCancelled(true);
}
}
@ -68,32 +67,34 @@ class ChestProtectionListener implements Listener {
Player player = event.getPlayer();
if (ItemUtils.isChest(type)) {
// note: unconnected chests can be placed
if (protectedChests.isProtectedChest(block, player)) {
Log.debug(() -> "Cancelled placing of chest block by '" + player.getName() + "' at '"
+ TextUtils.getLocationString(block) + "': Protected chest nearby");
// Note: Unconnected chests can be placed.
if (protectedContainers.isProtectedContainer(block, player)) {
Log.debug(() -> "Cancelled placing of (double) chest block by '" + player.getName() + "' at '"
+ TextUtils.getLocationString(block) + "': Protected chest nearby.");
event.setCancelled(true);
}
} else if (type == Material.HOPPER) {
// prevent placement of hoppers that could be used to extract or inject items from/into a protected chest:
// Prevent placement of hoppers that could be used to extract or inject items from/into a protected
// container:
Block upperBlock = block.getRelative(BlockFace.UP);
if (protectedChests.isProtectedChest(upperBlock, player) || protectedChests.isProtectedChest(this.getFacedBlock(block), player)) {
if (protectedContainers.isProtectedContainer(upperBlock, player)
|| protectedContainers.isProtectedContainer(this.getFacedBlock(block), player)) {
Log.debug(() -> "Cancelled placing of hopper block by '" + player.getName() + "' at '"
+ TextUtils.getLocationString(block) + "': Protected chest nearby");
+ TextUtils.getLocationString(block) + "': Protected container nearby.");
event.setCancelled(true);
}
} else if (type == Material.DROPPER) {
// prevent placement of droppers that could be used to inject items into a protected chest:
if (protectedChests.isProtectedChest(this.getFacedBlock(block), player)) {
// Prevent placement of droppers that could be used to inject items into a protected container:
if (protectedContainers.isProtectedContainer(this.getFacedBlock(block), player)) {
Log.debug(() -> "Cancelled placing of dropper block by '" + player.getName() + "' at '"
+ TextUtils.getLocationString(block) + "': Protected chest nearby");
+ TextUtils.getLocationString(block) + "': Protected container nearby.");
event.setCancelled(true);
}
} else if (type == Material.RAIL || type == Material.POWERED_RAIL || type == Material.DETECTOR_RAIL || type == Material.ACTIVATOR_RAIL) {
} else if (ItemUtils.isRail(type)) {
Block upperBlock = block.getRelative(BlockFace.UP);
if (protectedChests.isProtectedChest(upperBlock, player)) {
if (protectedContainers.isProtectedContainer(upperBlock, player)) {
Log.debug(() -> "Cancelled placing of rail block by '" + player.getName() + "' at '"
+ TextUtils.getLocationString(block) + "': Protected chest nearby");
+ TextUtils.getLocationString(block) + "': Protected container nearby.");
event.setCancelled(true);
}
}
@ -120,7 +121,7 @@ class ChestProtectionListener implements Listener {
Iterator<Block> iterator = blockList.iterator();
while (iterator.hasNext()) {
Block block = iterator.next();
if (protectedChests.isProtectedChest(block, null)) {
if (protectedContainers.isProtectedContainer(block, null)) {
iterator.remove();
}
}

View File

@ -1,4 +1,4 @@
package com.nisovin.shopkeepers.chestprotection;
package com.nisovin.shopkeepers.container.protection;
import org.bukkit.Location;
import org.bukkit.block.Block;
@ -8,22 +8,22 @@ import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryMoveItemEvent;
import org.bukkit.inventory.Inventory;
import com.nisovin.shopkeepers.util.ItemUtils;
import com.nisovin.shopkeepers.container.ShopContainers;
/**
* Prevents item movement from/to protected shop chests. Can be disabled via a config setting.
* Prevents item movement from/to protected containers. Can be disabled via a config setting.
*/
class InventoryMoveItemListener implements Listener {
private final ProtectedChests protectedChests;
private final ProtectedContainers protectedContainers;
InventoryMoveItemListener(ProtectedChests protectedChests) {
this.protectedChests = protectedChests;
InventoryMoveItemListener(ProtectedContainers protectedContainers) {
this.protectedContainers = protectedContainers;
}
@EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
void onInventoryMoveItem(InventoryMoveItemEvent event) {
// source and destination inventories are not null
assert event.getSource() != null && event.getDestination() != null;
if (this.isProtectedInventory(event.getSource()) || this.isProtectedInventory(event.getDestination())) {
event.setCancelled(true);
}
@ -31,12 +31,13 @@ class InventoryMoveItemListener implements Listener {
private boolean isProtectedInventory(Inventory inventory) {
assert inventory != null;
// Note: We are avoiding calling Inventory#getHolder here for performance reasons
// Note: We avoid calling Inventory#getHolder here for performance reasons. For block inventories this creates a
// snapshot of the block's BlockState.
Location inventoryLocation = inventory.getLocation(); // can be null
if (inventoryLocation == null) return false;
Block block = inventoryLocation.getBlock(); // not null
if (!ItemUtils.isChest(block.getType())) return false;
// also checks for protected connected chests (double chests):
return protectedChests.isChestProtected(block, null);
if (!ShopContainers.isSupportedContainer(block.getType())) return false;
// Also checks for protected connected chests (double chests):
return protectedContainers.isContainerProtected(block, null);
}
}

View File

@ -0,0 +1,315 @@
package com.nisovin.shopkeepers.container.protection;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.block.data.type.Chest;
import org.bukkit.block.data.type.Chest.Type;
import org.bukkit.entity.Player;
import org.bukkit.event.HandlerList;
import com.nisovin.shopkeepers.SKShopkeepersPlugin;
import com.nisovin.shopkeepers.Settings;
import com.nisovin.shopkeepers.api.ShopkeepersPlugin;
import com.nisovin.shopkeepers.api.shopkeeper.player.PlayerShopkeeper;
import com.nisovin.shopkeepers.container.ShopContainers;
import com.nisovin.shopkeepers.util.ItemUtils;
import com.nisovin.shopkeepers.util.PermissionUtils;
import com.nisovin.shopkeepers.util.Validate;
/**
* <b>Container protection.</b>
* <p>
* <b>Protected containers:</b><br>
* A container directly used by a player shopkeeper is 'directly protected'. Any adjacent chests (N, W, S, E) that form
* a double chest with a directly protected chest are protected as well.
* <p>
* <b>Bypass:</b><br>
* The owner of a shop corresponding to a protected container and players with the bypass permission are not affected
* by the listed protections. Also, certain protections can be disabled via config settings.
* <p>
* <b>Protections:</b><br>
* <ul>
* <li>Protected containers cannot be accessed.
* <li>Protected containers cannot be broken or destroyed by explosions.
* <li>Protected containers don't transfer items, ex. into or from hoppers, droppers, etc.
* <li>Chests cannot be placed adjacent to a directly protected chest, if they would connect to it and form a double
* chest.
* <li>Hoppers and droppers cannot be placed adjacent to a protected container, if they would be able to receive or
* inject items.
* <li>Rails cannot be placed below a protected container (to prevent hopper carts stealing items).
* </ul>
* Note that there is no protection for the following cases:
* <ul>
* <li>Even though rails cannot be placed below protected containers, hopper carts are still able to be placed / pushed
* to end up below protected containers. The item movement protection will however still prevent items from being
* extracted from the container.
* <li>Adjacent unconnected chests are not protected. It should however not be possible to access any protected adjacent
* chest by that.
* <li>Adjacent or chains of droppers and hoppers are not protected. So if item movement is enabled in the config and
* the shop owner places those connected to his shop container, other players will be able to access (or break) them.
* </ul>
*/
public class ProtectedContainers {
private final SKShopkeepersPlugin plugin;
private final ContainerProtectionListener containerProtectionListener = new ContainerProtectionListener(this);
private final InventoryMoveItemListener inventoryMoveItemListener = new InventoryMoveItemListener(this);
// Player shopkeepers by location key:
private final Map<String, List<PlayerShopkeeper>> protectedContainers = new HashMap<>();
public ProtectedContainers(SKShopkeepersPlugin plugin) {
this.plugin = plugin;
}
public void enable() {
if (Settings.protectContainers) {
Bukkit.getPluginManager().registerEvents(containerProtectionListener, plugin);
if (Settings.preventItemMovement) {
Bukkit.getPluginManager().registerEvents(inventoryMoveItemListener, plugin);
}
}
}
public void disable() {
// Cleanup:
HandlerList.unregisterAll(containerProtectionListener);
HandlerList.unregisterAll(inventoryMoveItemListener);
protectedContainers.clear();
}
private String getKey(String worldName, int x, int y, int z) {
return worldName + ";" + x + ";" + y + ";" + z;
}
public void addContainer(String worldName, int x, int y, int z, PlayerShopkeeper shopkeeper) {
Validate.notNull(shopkeeper, "shopkeeper is null");
String key = this.getKey(worldName, x, y, z);
List<PlayerShopkeeper> shopkeepers = protectedContainers.get(key);
if (shopkeepers == null) {
shopkeepers = new ArrayList<>(1);
protectedContainers.put(key, shopkeepers);
}
shopkeepers.add(shopkeeper);
}
public void removeContainer(String worldName, int x, int y, int z, PlayerShopkeeper shopkeeper) {
Validate.notNull(shopkeeper, "shopkeeper is null");
String key = this.getKey(worldName, x, y, z);
List<PlayerShopkeeper> shopkeepers = protectedContainers.get(key);
if (shopkeepers == null) return;
shopkeepers.remove(shopkeeper);
if (shopkeepers.isEmpty()) {
protectedContainers.remove(key);
}
}
// Gets the shopkeepers which are directly using the container at the specified location:
private List<PlayerShopkeeper> _getShopkeepers(String worldName, int x, int y, int z) {
String key = this.getKey(worldName, x, y, z);
return protectedContainers.get(key);
}
// Gets the shopkeepers which are directly using the specified container block:
private List<PlayerShopkeeper> _getShopkeepers(Block block) {
return this._getShopkeepers(block.getWorld().getName(), block.getX(), block.getY(), block.getZ());
}
// Gets the shopkeepers which are directly using the container at the specified location:
public List<PlayerShopkeeper> getShopkeepers(String worldName, int x, int y, int z) {
List<PlayerShopkeeper> shopkeepers = this._getShopkeepers(worldName, x, y, z);
return (shopkeepers == null) ? Collections.emptyList() : Collections.unmodifiableList(shopkeepers);
}
public List<PlayerShopkeeper> getShopkeepers(Block block) {
return this.getShopkeepers(block.getWorld().getName(), block.getX(), block.getY(), block.getZ());
}
//
// Checks if this exact block is protected:
public boolean isContainerDirectlyProtected(String worldName, int x, int y, int z, Player player) {
List<PlayerShopkeeper> shopkeepers = this._getShopkeepers(worldName, x, y, z);
// Check if there are any shopkeepers using this container:
if (shopkeepers == null) return false;
assert !shopkeepers.isEmpty();
if (player != null) {
// Check whether the player is affected by the protection:
// Note: The bypass permission does not get checked here but needs to be checked separately.
// We always allow shop owners to access their shop container (regardless of other shopkeepers using the
// same container):
for (PlayerShopkeeper shopkeeper : shopkeepers) {
if (shopkeeper.isOwner(player)) return false;
}
}
// There exists a protection for this container and the player doesn't own any shopkeeper using it:
return true;
}
public boolean isContainerDirectlyProtected(Block block, Player player) {
return this.isContainerDirectlyProtected(block.getWorld().getName(), block.getX(), block.getY(), block.getZ(), player);
}
//
// Gets reused by isContainerProtected calls:
private final List<PlayerShopkeeper> tempResultsList = new ArrayList<>();
/**
* Checks if the given container block is protected.
* <p>
* The block is protected if either:
* <ul>
* <li>The container block is directly used by a shopkeeper.
* <li>The block is a chest that is connected to another chest block (forms a double chest) which is directly used
* by a shopkeeper.
* </ul>
*
* @param containerBlock
* the container block (the block might not actually be a container anymore though)
* @param player
* the player to check the protection for, or <code>null</code> to check for protection without taking
* shop owners into account
* @return <code>true</code> if the block is protected
*/
public boolean isContainerProtected(Block containerBlock, Player player) {
Validate.notNull(containerBlock, "containerBlock is null!");
this.getShopkeepersUsingContainer(containerBlock, tempResultsList);
if (tempResultsList.isEmpty()) {
// No protection found:
return false;
}
// Protection found:
boolean result = true;
// Check if the player is affected by the protection:
if (player != null) {
// Note: The bypass permission does not get checked here but needs to be checked separately.
// We always allow shop owners to access their shop container (regardless of other shopkeepers using the
// same container):
for (PlayerShopkeeper shopkeeper : tempResultsList) {
if (shopkeeper.isOwner(player)) {
result = false;
break;
}
}
}
// Cleanup temporary results list:
tempResultsList.clear();
return result;
}
/**
* Checks if the given block is a protected shop container.
* <p>
* This checks if the specified block is actually a supported shop container and takes the bypass permission into
* account.
*
* @param block
* the block
* @param player
* the player to check the protection for, or <code>null</code> to check for protection without taking
* shop owners and players with bypass permission into account
* @return <code>true</code> if the block is a protected container
*/
public boolean isProtectedContainer(Block block, Player player) {
if (block == null || !ShopContainers.isSupportedContainer(block.getType())) return false;
if (!this.isContainerProtected(block, player)) return false;
if (player != null && PermissionUtils.hasPermission(player, ShopkeepersPlugin.BYPASS_PERMISSION)) return false;
return true;
}
// Gets the shopkeepers which use the container at the given location (directly or by a connected chest):
public List<PlayerShopkeeper> getShopkeepersUsingContainer(Block containerBlock) {
return this.getShopkeepersUsingContainer(containerBlock, null);
}
// Gets the shopkeepers which use the container at the given location (directly or by a connected chest), and adds
// them to the provided list:
private List<PlayerShopkeeper> getShopkeepersUsingContainer(Block containerBlock, List<PlayerShopkeeper> results) {
Validate.notNull(containerBlock, "containerBlock is null!");
// Create results list if none is provided:
if (results == null) {
results = new ArrayList<>();
}
// Check if the block is directly used by shopkeepers:
List<PlayerShopkeeper> shopkeepers = this._getShopkeepers(containerBlock);
if (shopkeepers != null) {
assert !shopkeepers.isEmpty();
results.addAll(shopkeepers);
}
// If the block actually is a chest, check for a connected chest:
Material chestType = containerBlock.getType();
if (ItemUtils.isChest(chestType)) {
Chest chestData = (Chest) containerBlock.getBlockData();
BlockFace chestFacing = chestData.getFacing();
BlockFace connectedFace = getConnectedBlockFace(chestFacing, chestData.getType());
if (connectedFace != null) {
Block connectedChest = containerBlock.getRelative(connectedFace);
// In case of inconsistency of the block data (i.e. connected chest missing or not mutually connected),
// we consider the block to be connected (and by that protected) anyways, because such inconsistencies
// might also occur during handling of block placements.
// Minecraft determines double chests by these consistency criteria:
// Same chest type, same facing, opposite chest type (opposite connected block faces)
shopkeepers = this._getShopkeepers(connectedChest);
if (shopkeepers != null) {
results.addAll(shopkeepers);
}
}
}
return results;
}
private static BlockFace getConnectedBlockFace(BlockFace chestFacing, Type chestType) {
switch (chestFacing) {
case NORTH:
switch (chestType) {
case RIGHT:
return BlockFace.WEST;
case LEFT:
return BlockFace.EAST;
default:
return null; // not connected
}
case EAST:
switch (chestType) {
case RIGHT:
return BlockFace.NORTH;
case LEFT:
return BlockFace.SOUTH;
default:
return null; // not connected
}
case SOUTH:
switch (chestType) {
case RIGHT:
return BlockFace.EAST;
case LEFT:
return BlockFace.WEST;
default:
return null; // not connected
}
case WEST:
switch (chestType) {
case RIGHT:
return BlockFace.SOUTH;
case LEFT:
return BlockFace.NORTH;
default:
return null; // not connected
}
default:
return null; // invalid chest facing
}
}
}

View File

@ -0,0 +1,72 @@
package com.nisovin.shopkeepers.container.protection;
import java.util.List;
import org.bukkit.Bukkit;
import org.bukkit.block.Block;
import org.bukkit.event.HandlerList;
import org.bukkit.inventory.ItemStack;
import com.nisovin.shopkeepers.SKShopkeepersPlugin;
import com.nisovin.shopkeepers.Settings;
import com.nisovin.shopkeepers.api.shopkeeper.player.PlayerShopkeeper;
import com.nisovin.shopkeepers.container.ShopContainers;
public class RemoveShopOnContainerBreak {
private final SKShopkeepersPlugin plugin;
private final ProtectedContainers protectedContainers;
private final RemoveShopOnContainerBreakListener removeShopOnContainerBreakListener;
public RemoveShopOnContainerBreak(SKShopkeepersPlugin plugin, ProtectedContainers protectedContainers) {
this.plugin = plugin;
this.protectedContainers = protectedContainers;
removeShopOnContainerBreakListener = new RemoveShopOnContainerBreakListener(plugin, this);
}
public void onEnable() {
if (Settings.deleteShopkeeperOnBreakContainer) {
Bukkit.getPluginManager().registerEvents(removeShopOnContainerBreakListener, plugin);
}
}
public void onDisable() {
HandlerList.unregisterAll(removeShopOnContainerBreakListener);
}
// Does not trigger saving on its own, returns true if there were shopkeepers using the container, that got removed
// now.
// Does not check the delete-shopkeeper-on-break-container setting, this has to be checked by clients beforehand.
// Does not check whether the block is still a valid container type.
public boolean handleBlockBreakage(Block block) {
List<PlayerShopkeeper> shopkeepers = protectedContainers.getShopkeepers(block);
if (shopkeepers.isEmpty()) return false;
// Copy to deal with concurrent modifications:
for (PlayerShopkeeper shopkeeper : shopkeepers.toArray(new PlayerShopkeeper[shopkeepers.size()])) {
if (!shopkeeper.isValid()) continue; // skip if no longer valid
// Return the shop creation item for player shopkeepers:
if (Settings.deletingPlayerShopReturnsCreationItem) {
ItemStack shopCreationItem = Settings.createShopCreationItem();
block.getWorld().dropItemNaturally(block.getLocation(), shopCreationItem);
}
// Note: We do not pass the player responsible for breaking the container here, because we cannot determine
// the player in all situations anyways (eg. if a player indirectly breaks the container by causing an
// explosion).
shopkeeper.delete();
}
return true;
}
public void handleBlocksBreakage(List<Block> blockList) {
boolean dirty = false;
for (Block block : blockList) {
if (ShopContainers.isSupportedContainer(block.getType()) && this.handleBlockBreakage(block)) {
dirty = true;
}
}
if (dirty) {
plugin.getShopkeeperStorage().save();
}
}
}

View File

@ -1,4 +1,4 @@
package com.nisovin.shopkeepers.chestprotection;
package com.nisovin.shopkeepers.container.protection;
import org.bukkit.block.Block;
import org.bukkit.event.EventHandler;
@ -9,34 +9,35 @@ import org.bukkit.event.block.BlockExplodeEvent;
import org.bukkit.event.entity.EntityExplodeEvent;
import com.nisovin.shopkeepers.SKShopkeepersPlugin;
import com.nisovin.shopkeepers.util.ItemUtils;
import com.nisovin.shopkeepers.container.ShopContainers;
class RemoveShopOnChestBreakListener implements Listener {
class RemoveShopOnContainerBreakListener implements Listener {
private final SKShopkeepersPlugin plugin;
private final RemoveShopOnChestBreak removeShopOnChestBreak;
private final RemoveShopOnContainerBreak removeShopOnContainerBreak;
RemoveShopOnChestBreakListener(SKShopkeepersPlugin plugin, RemoveShopOnChestBreak removeShopOnChestBreak) {
assert plugin != null && removeShopOnChestBreak != null;
RemoveShopOnContainerBreakListener(SKShopkeepersPlugin plugin, RemoveShopOnContainerBreak removeShopOnContainerBreak) {
assert plugin != null && removeShopOnContainerBreak != null;
this.plugin = plugin;
this.removeShopOnChestBreak = removeShopOnChestBreak;
this.removeShopOnContainerBreak = removeShopOnContainerBreak;
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
void onBlockBreak(BlockBreakEvent event) {
Block block = event.getBlock();
if (ItemUtils.isChest(block.getType()) && removeShopOnChestBreak.handleBlockBreakage(block)) {
if (ShopContainers.isSupportedContainer(block.getType())
&& removeShopOnContainerBreak.handleBlockBreakage(block)) {
plugin.getShopkeeperStorage().save();
}
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
void onEntityExplosion(EntityExplodeEvent event) {
removeShopOnChestBreak.handleBlocksBreakage(event.blockList());
removeShopOnContainerBreak.handleBlocksBreakage(event.blockList());
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
void onBlockExplosion(BlockExplodeEvent event) {
removeShopOnChestBreak.handleBlocksBreakage(event.blockList());
removeShopOnContainerBreak.handleBlocksBreakage(event.blockList());
}
}

View File

@ -4,7 +4,6 @@ import java.util.function.Predicate;
import org.bukkit.Bukkit;
import org.bukkit.block.Block;
import org.bukkit.block.Chest;
import org.bukkit.entity.Player;
import org.bukkit.event.HandlerList;
import org.bukkit.inventory.Inventory;
@ -14,6 +13,7 @@ import com.nisovin.shopkeepers.Settings;
import com.nisovin.shopkeepers.api.ShopkeepersPlugin;
import com.nisovin.shopkeepers.api.shopkeeper.Shopkeeper;
import com.nisovin.shopkeepers.api.shopkeeper.player.PlayerShopkeeper;
import com.nisovin.shopkeepers.container.ShopContainers;
import com.nisovin.shopkeepers.util.ItemUtils;
import com.nisovin.shopkeepers.util.Log;
import com.nisovin.shopkeepers.util.TextUtils;
@ -68,24 +68,26 @@ public class ItemConversions {
// Convert player items:
int convertedStacks = convertAffectedPlayerItems(player);
// Convert shop chest items:
// Convert shop container items:
if (shopkeeper instanceof PlayerShopkeeper) {
PlayerShopkeeper playerShopkeeper = (PlayerShopkeeper) shopkeeper;
Block chestBlock = playerShopkeeper.getChest();
assert chestBlock != null;
if (ItemUtils.isChest(chestBlock.getType())) {
Block containerBlock = playerShopkeeper.getContainer();
assert containerBlock != null;
if (ShopContainers.isSupportedContainer(containerBlock.getType())) {
long start = System.nanoTime();
Chest chest = (Chest) chestBlock.getState();
int convertedChestStacks = convertAffectedChestItems(chest);
// Note: Returns the complete inventory for double chests.
Inventory containerInventory = ShopContainers.getInventory(containerBlock);
int convertedContainerStacks = convertAffectedItems(containerInventory);
// Note: Inventory changes are directly reflected by the container block in the world.
long durationMillis = (System.nanoTime() - start) / 1000000L;
// Note: The conversion always has some performance impact, even if no items got actually converted. We
// therefore always print the debug messages to allow debugging the item conversion times.
Log.debug(Settings.DebugOptions.itemConversions,
() -> "Converted " + convertedChestStacks + " affected item stacks in the chest of shopkeeper "
() -> "Converted " + convertedContainerStacks + " affected item stacks in the container of shopkeeper "
+ shopkeeper.getId() + ", triggered by player '" + player.getName()
+ "' (took " + durationMillis + " ms)."
);
convertedStacks += convertedChestStacks;
convertedStacks += convertedContainerStacks;
}
}
@ -114,15 +116,6 @@ public class ItemConversions {
return convertedStacks;
}
private static int convertAffectedChestItems(Chest chest) {
Validate.notNull(chest, "chest is null");
Inventory inventory = chest.getInventory(); // the complete inventory for double chests
int convertedStacks = convertAffectedItems(inventory);
// Note: We don't need to apply the block state, because the used inventory is backed directly by the chest in
// the world.
return convertedStacks;
}
private static int convertAffectedItems(Inventory inventory) {
Validate.notNull(inventory, "inventory is null");
int convertedStacks = 0;

View File

@ -38,9 +38,9 @@ public class FeaturesChart extends Metrics.DrilldownPie {
// others:
addFeatureEntry(allFeatures, "save-instantly", Settings.saveInstantly);
addFeatureEntry(allFeatures, "colored names allowed", Settings.nameRegex.contains("&"));
addFeatureEntry(allFeatures, "protect-chests", Settings.protectChests);
addFeatureEntry(allFeatures, "protect-containers", Settings.protectContainers);
addFeatureEntry(allFeatures, "prevent-item-movement", Settings.preventItemMovement);
addFeatureEntry(allFeatures, "delete-shopkeeper-on-break-chest", Settings.deleteShopkeeperOnBreakChest);
addFeatureEntry(allFeatures, "delete-shopkeeper-on-break-container", Settings.deleteShopkeeperOnBreakContainer);
addFeatureEntry(allFeatures, "player-shopkeeper-inactive-days", Settings.playerShopkeeperInactiveDays > 0);
addFeatureEntry(allFeatures, "tax-rate", Settings.taxRate > 0);
addFeatureEntry(allFeatures, "use-strict-item-comparison", Settings.useStrictItemComparison);

View File

@ -28,6 +28,7 @@ import com.nisovin.shopkeepers.api.shopkeeper.Shopkeeper;
import com.nisovin.shopkeepers.api.shopkeeper.player.PlayerShopCreationData;
import com.nisovin.shopkeepers.api.shopkeeper.player.PlayerShopType;
import com.nisovin.shopkeepers.api.shopobjects.ShopObjectType;
import com.nisovin.shopkeepers.container.ShopContainers;
import com.nisovin.shopkeepers.util.ItemUtils;
import com.nisovin.shopkeepers.util.Log;
import com.nisovin.shopkeepers.util.PermissionUtils;
@ -65,8 +66,8 @@ class CreateListener implements Listener {
TextUtils.sendMessage(player, Settings.msgCreationItemSelected);
}
// Since this might check chest access by calling another dummy interaction event, we handle (cancel) this event as
// early as possible, so that other plugins (eg. protection plugins) can ignore it and don't handle it twice. In
// Since this might check container access by calling another dummy interaction event, we handle (cancel) this event
// as early as possible, so that other plugins (eg. protection plugins) can ignore it and don't handle it twice. In
// case some other event handler managed to already cancel the event on LOWEST priority, we ignore the interaction.
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = false)
void onPlayerInteract(PlayerInteractEvent event) {
@ -151,29 +152,39 @@ class CreateListener implements Listener {
} else if (action == Action.RIGHT_CLICK_BLOCK) {
Block clickedBlock = event.getClickedBlock();
Block selectedChest = shopkeeperCreation.getSelectedChest(player);
// validate old selected chest:
if (selectedChest != null && !ItemUtils.isChest(selectedChest.getType())) {
shopkeeperCreation.selectChest(player, null);
selectedChest = null;
Block selectedContainer = shopkeeperCreation.getSelectedContainer(player);
// Validate old selected container:
if (selectedContainer != null && !ShopContainers.isSupportedContainer(selectedContainer.getType())) {
shopkeeperCreation.selectContainer(player, null);
selectedContainer = null;
}
// handle chest selection:
if (ItemUtils.isChest(clickedBlock.getType()) && !clickedBlock.equals(selectedChest)) {
// check if the chest can be used for a shop:
if (shopkeeperCreation.handleCheckChest(player, clickedBlock)) {
// select chest:
shopkeeperCreation.selectChest(player, clickedBlock);
TextUtils.sendMessage(player, Settings.msgSelectedChest);
// Handle container selection:
boolean isContainerSelection = false;
if (!clickedBlock.equals(selectedContainer)) {
if (ShopContainers.isSupportedContainer(clickedBlock.getType())) {
isContainerSelection = true;
// Check if the container can be used for a shop:
if (shopkeeperCreation.handleCheckContainer(player, clickedBlock)) {
// Select container:
shopkeeperCreation.selectContainer(player, clickedBlock);
TextUtils.sendMessage(player, Settings.msgContainerSelected);
}
} else if (ItemUtils.isContainer(clickedBlock.getType())) {
// Player clicked a type of container which cannot be used for shops:
isContainerSelection = true;
TextUtils.sendMessage(player, Settings.msgUnsupportedContainer);
}
} else {
// player shop creation:
if (selectedChest == null) {
// clicked a location without having a chest selected:
TextUtils.sendMessage(player, Settings.msgMustSelectChest);
}
if (!isContainerSelection) {
// Player shop creation:
if (selectedContainer == null) {
// Clicked a location without having a container selected:
TextUtils.sendMessage(player, Settings.msgMustSelectContainer);
return;
}
assert ItemUtils.isChest(selectedChest.getType()); // we have checked that above already
assert ShopContainers.isSupportedContainer(selectedContainer.getType()); // Checked above already
// validate the selected shop type:
if (!(shopType instanceof PlayerShopType)) {
@ -187,13 +198,13 @@ class CreateListener implements Listener {
Location spawnLocation = shopkeeperCreation.determineSpawnLocation(player, clickedBlock, clickedBlockFace);
// create player shopkeeper:
ShopCreationData creationData = PlayerShopCreationData.create(player, shopType, shopObjType, spawnLocation, clickedBlockFace, selectedChest);
ShopCreationData creationData = PlayerShopCreationData.create(player, shopType, shopObjType, spawnLocation, clickedBlockFace, selectedContainer);
Shopkeeper shopkeeper = plugin.handleShopkeeperCreation(creationData);
if (shopkeeper != null) {
// shopkeeper creation was successful:
// reset selected chest:
shopkeeperCreation.selectChest(player, null);
// Reset selected container:
shopkeeperCreation.selectContainer(player, null);
// manually remove creation item from player's hand after this event is processed:
Bukkit.getScheduler().runTask(plugin, () -> {

View File

@ -6,21 +6,21 @@ import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockPlaceEvent;
import com.nisovin.shopkeepers.util.ItemUtils;
import com.nisovin.shopkeepers.container.ShopContainers;
class RecentlyPlacedChestsListener implements Listener {
class RecentlyPlacedContainersListener implements Listener {
private final ShopkeeperCreation shopkeeperCreation;
RecentlyPlacedChestsListener(ShopkeeperCreation shopkeeperCreation) {
RecentlyPlacedContainersListener(ShopkeeperCreation shopkeeperCreation) {
this.shopkeeperCreation = shopkeeperCreation;
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
void onBlockPlace(BlockPlaceEvent event) {
Block block = event.getBlock();
if (ItemUtils.isChest(block.getType())) {
shopkeeperCreation.addRecentlyPlacedChest(event.getPlayer(), block);
if (ShopContainers.isSupportedContainer(block.getType())) {
shopkeeperCreation.addRecentlyPlacedContainer(event.getPlayer(), block);
}
}
}

View File

@ -13,97 +13,98 @@ import org.bukkit.entity.Player;
import com.nisovin.shopkeepers.SKShopkeepersPlugin;
import com.nisovin.shopkeepers.Settings;
import com.nisovin.shopkeepers.util.ItemUtils;
import com.nisovin.shopkeepers.container.ShopContainers;
import com.nisovin.shopkeepers.util.TextUtils;
import com.nisovin.shopkeepers.util.Utils;
public class ShopkeeperCreation {
private final SKShopkeepersPlugin plugin;
private final Map<String, List<String>> recentlyPlacedChests = new HashMap<>();
private final Map<String, Block> selectedChest = new HashMap<>();
private final Map<String, List<String>> recentlyPlacedContainers = new HashMap<>();
private final Map<String, Block> selectedContainer = new HashMap<>();
public ShopkeeperCreation(SKShopkeepersPlugin plugin) {
this.plugin = plugin;
}
public void onEnable() {
Bukkit.getPluginManager().registerEvents(new RecentlyPlacedChestsListener(this), plugin);
Bukkit.getPluginManager().registerEvents(new RecentlyPlacedContainersListener(this), plugin);
Bukkit.getPluginManager().registerEvents(new CreateListener(plugin, this), plugin);
}
public void onDisable() {
selectedChest.clear();
// note: recentlyPlacedChests does not get cleared here to persist across plugin reloads
selectedContainer.clear();
// Note: recentlyPlacedContainers does not get cleared here to persist across plugin reloads.
}
public void onPlayerQuit(Player player) {
assert player != null;
String playerName = player.getName();
selectedChest.remove(playerName);
recentlyPlacedChests.remove(playerName);
selectedContainer.remove(playerName);
recentlyPlacedContainers.remove(playerName);
}
// RECENTLY PLACED CHESTS
// RECENTLY PLACED CONTAINERS
public void addRecentlyPlacedChest(Player player, Block chest) {
assert player != null && chest != null;
public void addRecentlyPlacedContainer(Player player, Block container) {
assert player != null && container != null;
String playerName = player.getName();
List<String> recentlyPlaced = recentlyPlacedChests.get(playerName);
List<String> recentlyPlaced = recentlyPlacedContainers.get(playerName);
if (recentlyPlaced == null) {
recentlyPlaced = new LinkedList<>();
recentlyPlacedChests.put(playerName, recentlyPlaced);
recentlyPlacedContainers.put(playerName, recentlyPlaced);
}
recentlyPlaced.add(TextUtils.getLocationString(chest));
recentlyPlaced.add(TextUtils.getLocationString(container));
if (recentlyPlaced.size() > 5) {
recentlyPlaced.remove(0);
}
}
public boolean isRecentlyPlacedChest(Player player, Block chest) {
assert player != null && chest != null;
public boolean isRecentlyPlacedContainer(Player player, Block container) {
assert player != null && container != null;
String playerName = player.getName();
List<String> recentlyPlaced = recentlyPlacedChests.get(playerName);
return recentlyPlaced != null && recentlyPlaced.contains(TextUtils.getLocationString(chest));
List<String> recentlyPlaced = recentlyPlacedContainers.get(playerName);
return recentlyPlaced != null && recentlyPlaced.contains(TextUtils.getLocationString(container));
}
// SELECTED CHEST
// SELECTED CONTAINER
public void selectChest(Player player, Block chest) {
public void selectContainer(Player player, Block container) {
assert player != null;
String playerName = player.getName();
if (chest == null) selectedChest.remove(playerName);
else {
assert ItemUtils.isChest(chest.getType());
selectedChest.put(playerName, chest);
if (container == null) {
selectedContainer.remove(playerName);
} else {
assert ShopContainers.isSupportedContainer(container.getType());
selectedContainer.put(playerName, container);
}
}
public Block getSelectedChest(Player player) {
public Block getSelectedContainer(Player player) {
assert player != null;
return selectedChest.get(player.getName());
return selectedContainer.get(player.getName());
}
// SHOPKEEPER CREATION
// checks if the player can use the given chest for a player shopkeeper:
public boolean handleCheckChest(Player player, Block chestBlock) {
// check if this chest is already used by some other shopkeeper:
if (SKShopkeepersPlugin.getInstance().getProtectedChests().isChestProtected(chestBlock, null)) {
TextUtils.sendMessage(player, Settings.msgChestAlreadyInUse);
// Checks if the player can use the given container for a player shopkeeper:
public boolean handleCheckContainer(Player player, Block containerBlock) {
// Check if the container is already used by some other shopkeeper:
if (SKShopkeepersPlugin.getInstance().getProtectedContainers().isContainerProtected(containerBlock, null)) {
TextUtils.sendMessage(player, Settings.msgContainerAlreadyInUse);
return false;
}
// check for recently placed:
if (Settings.requireChestRecentlyPlaced && !plugin.getShopkeeperCreation().isRecentlyPlacedChest(player, chestBlock)) {
// chest was not recently placed:
TextUtils.sendMessage(player, Settings.msgChestNotPlaced);
if (Settings.requireContainerRecentlyPlaced && !plugin.getShopkeeperCreation().isRecentlyPlacedContainer(player, containerBlock)) {
// Container was not recently placed:
TextUtils.sendMessage(player, Settings.msgContainerNotPlaced);
return false;
}
// check if the player can access the chest:
if (!Utils.checkBlockInteract(player, chestBlock)) { // checks access via dummy interact event
TextUtils.sendMessage(player, Settings.msgNoChestAccess);
// Check if the player can access the container:
if (!Utils.checkBlockInteract(player, containerBlock)) { // checks access via dummy interact event
TextUtils.sendMessage(player, Settings.msgNoContainerAccess);
return false;
}
return true;

View File

@ -527,8 +527,8 @@ public abstract class AbstractShopkeeper implements Shopkeeper {
Validate.notNull(world, "Location's world is null!");
ChunkCoords oldChunk = this.getChunkCoords();
// TODO changing the world is not safe (at least not for all types of shops)! consider for example player shops
// which currently use the worldname to locate their chest
// TODO Changing the world is not safe (at least not for all types of shops)! Consider for example player shops
// which currently use the world name to locate their container
worldName = world.getName();
x = location.getBlockX();
y = location.getBlockY();
@ -703,7 +703,7 @@ public abstract class AbstractShopkeeper implements Shopkeeper {
/**
* This is called periodically (roughly once per second) for shopkeepers in active chunks.
* <p>
* This can for example be used for checks that need to happen periodically, such as checking if the chest for a
* This can for example be used for checks that need to happen periodically, such as checking if the container of a
* player shop still exists.
* <p>
* If the check to perform is potentially heavy or not required to happen every second, the shopkeeper may decide to

View File

@ -13,10 +13,10 @@ import com.nisovin.shopkeepers.api.events.PlayerCreatePlayerShopkeeperEvent;
import com.nisovin.shopkeepers.api.shopkeeper.ShopCreationData;
import com.nisovin.shopkeepers.api.shopkeeper.player.PlayerShopCreationData;
import com.nisovin.shopkeepers.api.shopkeeper.player.PlayerShopType;
import com.nisovin.shopkeepers.container.ShopContainers;
import com.nisovin.shopkeepers.pluginhandlers.TownyHandler;
import com.nisovin.shopkeepers.pluginhandlers.WorldGuardHandler;
import com.nisovin.shopkeepers.shopkeeper.AbstractShopType;
import com.nisovin.shopkeepers.util.ItemUtils;
import com.nisovin.shopkeepers.util.Log;
import com.nisovin.shopkeepers.util.TextUtils;
import com.nisovin.shopkeepers.util.Validate;
@ -41,23 +41,23 @@ public abstract class AbstractPlayerShopType<T extends AbstractPlayerShopkeeper>
Location spawnLocation = shopCreationData.getSpawnLocation();
// validate chest block:
Block chestBlock = playerShopCreationData.getShopChest();
if (!ItemUtils.isChest(chestBlock.getType())) {
// the block is not / no longer a chest:
TextUtils.sendMessage(creator, Settings.msgNoChestSelected);
// Validate container block:
Block containerBlock = playerShopCreationData.getShopContainer();
if (!ShopContainers.isSupportedContainer(containerBlock.getType())) {
// The block is not / no longer a supported container:
TextUtils.sendMessage(creator, Settings.msgInvalidContainer);
return false;
}
// check for selected chest being too far away:
if (!chestBlock.getWorld().equals(spawnLocation.getWorld())
|| (int) chestBlock.getLocation().distanceSquared(spawnLocation) > (Settings.maxChestDistance * Settings.maxChestDistance)) {
TextUtils.sendMessage(creator, Settings.msgChestTooFar);
// Check if the selected container is too far away:
if (!containerBlock.getWorld().equals(spawnLocation.getWorld())
|| (int) containerBlock.getLocation().distanceSquared(spawnLocation) > (Settings.maxContainerDistance * Settings.maxContainerDistance)) {
TextUtils.sendMessage(creator, Settings.msgContainerTooFarAway);
return false;
}
// check selected chest:
if (!SKShopkeepersPlugin.getInstance().getShopkeeperCreation().handleCheckChest(creator, chestBlock)) {
// Check selected container:
if (!SKShopkeepersPlugin.getInstance().getShopkeeperCreation().handleCheckContainer(creator, containerBlock)) {
return false;
}

View File

@ -7,7 +7,6 @@ import java.util.UUID;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.block.Block;
import org.bukkit.block.Chest;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
@ -28,6 +27,7 @@ import com.nisovin.shopkeepers.api.shopkeeper.player.PlayerShopCreationData;
import com.nisovin.shopkeepers.api.shopkeeper.player.PlayerShopkeeper;
import com.nisovin.shopkeepers.api.shopobjects.DefaultShopObjectTypes;
import com.nisovin.shopkeepers.api.ui.DefaultUITypes;
import com.nisovin.shopkeepers.container.ShopContainers;
import com.nisovin.shopkeepers.shopkeeper.AbstractShopkeeper;
import com.nisovin.shopkeepers.shopobjects.citizens.SKCitizensShopObject;
import com.nisovin.shopkeepers.shopobjects.sign.SKSignShopObject;
@ -41,19 +41,19 @@ import com.nisovin.shopkeepers.util.Validate;
public abstract class AbstractPlayerShopkeeper extends AbstractShopkeeper implements PlayerShopkeeper {
private static final int CHECK_CHEST_PERIOD_SECONDS = 5;
private static final int CHECK_CONTAINER_PERIOD_SECONDS = 5;
protected UUID ownerUUID; // not null after successful initialization
protected String ownerName; // not null after successful initialization
// TODO store chest world separately? currently it uses the shopkeeper world
// this would allow the chest and shopkeeper to be located in different worlds, and virtual player shops
protected int chestX;
protected int chestY;
protected int chestZ;
// TODO Store container world separately? Currently it uses the shopkeeper world.
// This would allow the container and shopkeeper to be located in different worlds, and virtual player shops.
protected int containerX;
protected int containerY;
protected int containerZ;
protected ItemStack hireCost = null; // null if not for hire
// random shopkeeper-specific starting offset between [1, CHECK_CHEST_PERIOD_SECONDS]
private int remainingCheckChestSeconds = (int) (Math.random() * CHECK_CHEST_PERIOD_SECONDS) + 1;
// Random shopkeeper-specific starting offset between [1, CHECK_CONTAINER_PERIOD_SECONDS]
private int remainingCheckContainerSeconds = (int) (Math.random() * CHECK_CONTAINER_PERIOD_SECONDS) + 1;
/**
* Creates a not yet initialized {@link AbstractPlayerShopkeeper} (for use in sub-classes).
@ -75,13 +75,13 @@ public abstract class AbstractPlayerShopkeeper extends AbstractShopkeeper implem
super.loadFromCreationData(shopCreationData);
PlayerShopCreationData playerShopCreationData = (PlayerShopCreationData) shopCreationData;
Player owner = playerShopCreationData.getCreator();
Block chest = playerShopCreationData.getShopChest();
Block container = playerShopCreationData.getShopContainer();
assert owner != null;
assert chest != null;
assert container != null;
this.ownerUUID = owner.getUniqueId();
this.ownerName = owner.getName();
this._setChest(chest.getX(), chest.getY(), chest.getZ());
this._setContainer(container.getX(), container.getY(), container.getZ());
}
@Override
@ -109,11 +109,12 @@ public abstract class AbstractPlayerShopkeeper extends AbstractShopkeeper implem
}
if (!configSection.isInt("chestx") || !configSection.isInt("chesty") || !configSection.isInt("chestz")) {
throw new ShopkeeperCreateException("Missing chest coordinate(s)");
throw new ShopkeeperCreateException("Missing container coordinate(s)");
}
// update chest:
this._setChest(configSection.getInt("chestx"), configSection.getInt("chesty"), configSection.getInt("chestz"));
// Update container:
// TODO Rename to storage keys to containerx/y/z?
this._setContainer(configSection.getInt("chestx"), configSection.getInt("chesty"), configSection.getInt("chestz"));
hireCost = configSection.getItemStack("hirecost");
// hire cost itemstack is not null, but empty -> normalize to null:
@ -143,9 +144,9 @@ public abstract class AbstractPlayerShopkeeper extends AbstractShopkeeper implem
super.save(configSection);
configSection.set("owner uuid", ownerUUID.toString());
configSection.set("owner", ownerName);
configSection.set("chestx", chestX);
configSection.set("chesty", chestY);
configSection.set("chestz", chestZ);
configSection.set("chestx", containerX);
configSection.set("chesty", containerY);
configSection.set("chestz", containerZ);
if (hireCost != null) {
configSection.set("hirecost", hireCost);
}
@ -155,16 +156,16 @@ public abstract class AbstractPlayerShopkeeper extends AbstractShopkeeper implem
protected void onAdded(ShopkeeperAddedEvent.Cause cause) {
super.onAdded(cause);
// register protected chest:
SKShopkeepersPlugin.getInstance().getProtectedChests().addChest(this.getWorldName(), chestX, chestY, chestZ, this);
// Register protected container:
SKShopkeepersPlugin.getInstance().getProtectedContainers().addContainer(this.getWorldName(), containerX, containerY, containerZ, this);
}
@Override
protected void onRemoval(ShopkeeperRemoveEvent.Cause cause) {
super.onRemoval(cause);
// unregister previously protected chest:
SKShopkeepersPlugin.getInstance().getProtectedChests().removeChest(this.getWorldName(), chestX, chestY, chestZ, this);
// Unregister previously protected container:
SKShopkeepersPlugin.getInstance().getProtectedContainers().removeContainer(this.getWorldName(), containerX, containerY, containerZ, this);
}
@Override
@ -296,47 +297,72 @@ public abstract class AbstractPlayerShopkeeper extends AbstractShopkeeper implem
return (this.isForHire() ? hireCost.clone() : null);
}
protected void _setChest(int chestX, int chestY, int chestZ) {
protected void _setContainer(int containerX, int containerY, int containerZ) {
if (this.isValid()) {
// unregister previously protected chest:
SKShopkeepersPlugin.getInstance().getProtectedChests().removeChest(this.getWorldName(), chestX, chestY, chestZ, this);
// Unregister previously protected container:
SKShopkeepersPlugin.getInstance().getProtectedContainers().removeContainer(this.getWorldName(), containerX, containerY, containerZ, this);
}
// update chest:
this.chestX = chestX;
this.chestY = chestY;
this.chestZ = chestZ;
// Update container:
this.containerX = containerX;
this.containerY = containerY;
this.containerZ = containerZ;
if (this.isValid()) {
// register new protected chest:
SKShopkeepersPlugin.getInstance().getProtectedChests().addChest(this.getWorldName(), chestX, chestY, chestZ, this);
// Register new protected container:
SKShopkeepersPlugin.getInstance().getProtectedContainers().addContainer(this.getWorldName(), containerX, containerY, containerZ, this);
}
}
@Override
public int getChestX() {
return chestX;
return this.getContainerX();
}
@Override
public int getChestY() {
return chestY;
return this.getContainerY();
}
@Override
public int getChestZ() {
return chestZ;
return this.getContainerZ();
}
@Override
public void setChest(int chestX, int chestY, int chestZ) {
this._setChest(chestX, chestY, chestZ);
public int getContainerX() {
return containerX;
}
@Override
public int getContainerY() {
return containerY;
}
@Override
public int getContainerZ() {
return containerZ;
}
@Override
public void setChest(int containerX, int containerY, int containerZ) {
this.setContainer(containerX, containerY, containerZ);
}
@Override
public void setContainer(int containerX, int containerY, int containerZ) {
this._setContainer(containerX, containerY, containerZ);
this.markDirty();
}
@Override
public Block getChest() {
return Bukkit.getWorld(this.getWorldName()).getBlockAt(chestX, chestY, chestZ);
return this.getContainer();
}
@Override
public Block getContainer() {
return Bukkit.getWorld(this.getWorldName()).getBlockAt(containerX, containerY, containerZ);
}
// returns null (and logs a warning) if the price cannot be represented correctly by currency items
@ -388,13 +414,20 @@ public abstract class AbstractPlayerShopkeeper extends AbstractShopkeeper implem
@Override
public int getCurrencyInChest() {
Block chest = this.getChest();
if (!ItemUtils.isChest(chest.getType())) return 0;
return this.getCurrencyInContainer();
}
@Override
public int getCurrencyInContainer() {
Block container = this.getContainer();
if (!ShopContainers.isSupportedContainer(container.getType())) {
return 0;
}
int totalCurrency = 0;
Inventory chestInventory = ((Chest) chest.getState()).getInventory();
ItemStack[] chestContents = chestInventory.getContents();
for (ItemStack itemStack : chestContents) {
Inventory inventory = ShopContainers.getInventory(container);
ItemStack[] contents = inventory.getContents();
for (ItemStack itemStack : contents) {
if (Settings.isCurrencyItem(itemStack)) {
totalCurrency += itemStack.getAmount();
} else if (Settings.isHighCurrencyItem(itemStack)) {
@ -404,15 +437,15 @@ public abstract class AbstractPlayerShopkeeper extends AbstractShopkeeper implem
return totalCurrency;
}
protected List<ItemCount> getItemsFromChest(Filter<ItemStack> filter) {
ItemStack[] chestContents = null;
Block chest = this.getChest();
if (ItemUtils.isChest(chest.getType())) {
Inventory chestInventory = ((Chest) chest.getState()).getInventory();
chestContents = chestInventory.getContents();
protected List<ItemCount> getItemsFromContainer(Filter<ItemStack> filter) {
ItemStack[] contents = null;
Block container = this.getContainer();
if (ShopContainers.isSupportedContainer(container.getType())) {
Inventory inventory = ShopContainers.getInventory(container);
contents = inventory.getContents();
}
// returns an empty list if the chest couldn't be found:
return ItemUtils.countItems(chestContents, filter);
// Returns an empty list if the container could not be found:
return ItemUtils.countItems(contents, filter);
}
// SHOPKEEPER UIs - shortcuts for common UI types:
@ -424,17 +457,23 @@ public abstract class AbstractPlayerShopkeeper extends AbstractShopkeeper implem
@Override
public boolean openChestWindow(Player player) {
// make sure the chest still exists
Block chest = this.getChest();
if (!ItemUtils.isChest(chest.getType())) {
Log.debug(() -> "Cannot open chest inventory for player '" + player.getName() + "': The block is no longer a chest!");
return this.openContainerWindow(player);
}
@Override
public boolean openContainerWindow(Player player) {
// Check if the container still exists:
Block container = this.getContainer();
if (!ShopContainers.isSupportedContainer(container.getType())) {
Log.debug(() -> "Cannot open container inventory for player '" + player.getName()
+ "': The block is no longer a valid container!");
return false;
}
Log.debug(() -> "Opening chest inventory for player '" + player.getName() + "'.");
// open the chest directly as the player (no need for a custom UI)
Inventory inv = ((Chest) chest.getState()).getInventory();
player.openInventory(inv);
Log.debug(() -> "Opening container inventory for player '" + player.getName() + "'.");
// Open the container directly for the player (no need for a custom UI):
Inventory inventory = ShopContainers.getInventory(container);
player.openInventory(inventory);
return true;
}
@ -442,16 +481,16 @@ public abstract class AbstractPlayerShopkeeper extends AbstractShopkeeper implem
@Override
public void tick() {
// delete the shopkeeper if the chest is no longer present (eg. if it got removed externally by another plugin,
// such as WorldEdit, etc.):
if (Settings.deleteShopkeeperOnBreakChest) {
remainingCheckChestSeconds--;
if (remainingCheckChestSeconds <= 0) {
remainingCheckChestSeconds = CHECK_CHEST_PERIOD_SECONDS;
// this checks if the block is still a chest:
Block chestBlock = this.getChest();
if (!ItemUtils.isChest(chestBlock.getType())) {
SKShopkeepersPlugin.getInstance().getRemoveShopOnChestBreak().handleBlockBreakage(chestBlock);
// Delete the shopkeeper if the container is no longer present (eg. if it got removed externally by another
// plugin, such as WorldEdit, etc.):
if (Settings.deleteShopkeeperOnBreakContainer) {
remainingCheckContainerSeconds--;
if (remainingCheckContainerSeconds <= 0) {
remainingCheckContainerSeconds = CHECK_CONTAINER_PERIOD_SECONDS;
// This checks if the block is still a valid container:
Block containerBlock = this.getContainer();
if (!ShopContainers.isSupportedContainer(containerBlock.getType())) {
SKShopkeepersPlugin.getInstance().getRemoveShopOnContainerBreak().handleBlockBreakage(containerBlock);
}
}
}

View File

@ -1,7 +1,6 @@
package com.nisovin.shopkeepers.shopkeeper.player;
import org.bukkit.block.Block;
import org.bukkit.block.Chest;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
@ -9,9 +8,9 @@ import org.bukkit.inventory.ItemStack;
import com.nisovin.shopkeepers.Settings;
import com.nisovin.shopkeepers.api.ShopkeepersPlugin;
import com.nisovin.shopkeepers.api.shopkeeper.player.PlayerShopkeeper;
import com.nisovin.shopkeepers.container.ShopContainers;
import com.nisovin.shopkeepers.ui.defaults.SKDefaultUITypes;
import com.nisovin.shopkeepers.ui.defaults.TradingHandler;
import com.nisovin.shopkeepers.util.ItemUtils;
import com.nisovin.shopkeepers.util.Log;
import com.nisovin.shopkeepers.util.PermissionUtils;
import com.nisovin.shopkeepers.util.TextUtils;
@ -19,8 +18,8 @@ import com.nisovin.shopkeepers.util.TextUtils;
public abstract class PlayerShopTradingHandler extends TradingHandler {
// state related to the currently handled trade:
protected Inventory chestInventory = null;
protected ItemStack[] newChestContents = null;
protected Inventory containerInventory = null;
protected ItemStack[] newContainerContents = null;
protected PlayerShopTradingHandler(AbstractPlayerShopkeeper shopkeeper) {
super(SKDefaultUITypes.TRADING(), shopkeeper);
@ -73,17 +72,17 @@ public abstract class PlayerShopTradingHandler extends TradingHandler {
}
}
// check for the shop's chest:
Block chest = shopkeeper.getChest();
if (!ItemUtils.isChest(chest.getType())) {
TextUtils.sendMessage(tradingPlayer, Settings.msgCantTradeWithShopMissingChest, "owner", shopkeeper.getOwnerName());
this.debugPreventedTrade(tradingPlayer, "The shop's chest is missing.");
// Check for the shop's container:
Block container = shopkeeper.getContainer();
if (!ShopContainers.isSupportedContainer(container.getType())) {
TextUtils.sendMessage(tradingPlayer, Settings.msgCantTradeWithShopMissingContainer, "owner", shopkeeper.getOwnerName());
this.debugPreventedTrade(tradingPlayer, "The shop's container is missing.");
return false;
}
// setup common state information for handling this trade:
this.chestInventory = ((Chest) chest.getState()).getInventory();
this.newChestContents = chestInventory.getContents();
this.containerInventory = ShopContainers.getInventory(container);
this.newContainerContents = containerInventory.getContents();
return true;
}
@ -92,9 +91,9 @@ public abstract class PlayerShopTradingHandler extends TradingHandler {
protected void onTradeApplied(TradeData tradeData) {
super.onTradeApplied(tradeData);
// apply chest content changes:
if (chestInventory != null && newChestContents != null) {
chestInventory.setContents(newChestContents);
// Apply container content changes:
if (containerInventory != null && newContainerContents != null) {
containerInventory.setContents(newContainerContents);
}
// reset trade related state information:
@ -108,7 +107,7 @@ public abstract class PlayerShopTradingHandler extends TradingHandler {
}
protected void resetTradeState() {
chestInventory = null;
newChestContents = null;
containerInventory = null;
newContainerContents = null;
}
}

View File

@ -34,11 +34,11 @@ public class BookPlayerShopEditorHandler extends PlayerShopEditorHandler {
Set<String> bookTitles = new HashSet<>();
// add the shopkeeper's offers:
List<ItemCount> chestItems = shopkeeper.getCopyableBooksFromChest();
List<ItemCount> containerItems = shopkeeper.getCopyableBooksFromContainer();
for (BookOffer offer : shopkeeper.getOffers()) {
String bookTitle = offer.getBookTitle();
bookTitles.add(bookTitle);
ItemStack bookItem = shopkeeper.getBookItem(chestItems, bookTitle);
ItemStack bookItem = shopkeeper.getBookItem(containerItems, bookTitle);
if (bookItem == null) {
bookItem = shopkeeper.createDummyBook(bookTitle);
}
@ -46,12 +46,12 @@ public class BookPlayerShopEditorHandler extends PlayerShopEditorHandler {
recipes.add(recipe);
}
// add empty offers for items from the chest:
for (int chestItemIndex = 0; chestItemIndex < chestItems.size(); chestItemIndex++) {
ItemCount itemCount = chestItems.get(chestItemIndex);
ItemStack itemFromChest = itemCount.getItem(); // this item is already a copy with amount 1
// add empty offers for items from the container:
for (int containerItemIndex = 0; containerItemIndex < containerItems.size(); containerItemIndex++) {
ItemCount itemCount = containerItems.get(containerItemIndex);
ItemStack itemFromContainer = itemCount.getItem(); // this item is already a copy with amount 1
String bookTitle = SKBookPlayerShopkeeper.getBookTitle(itemFromChest);
String bookTitle = SKBookPlayerShopkeeper.getBookTitle(itemFromContainer);
assert bookTitle != null; // we filtered those book items earlier
if (bookTitles.contains(bookTitle)) {
continue; // already added a recipe for a book with this name
@ -59,7 +59,7 @@ public class BookPlayerShopEditorHandler extends PlayerShopEditorHandler {
bookTitles.add(bookTitle);
// add recipe:
TradingRecipeDraft recipe = this.createTradingRecipeDraft(itemFromChest, 0);
TradingRecipeDraft recipe = this.createTradingRecipeDraft(itemFromContainer, 0);
recipes.add(recipe);
}

View File

@ -49,47 +49,47 @@ public class BookPlayerShopTradingHandler extends PlayerShopTradingHandler {
return false;
}
assert chestInventory != null & newChestContents != null;
assert containerInventory != null & newContainerContents != null;
// remove blank book from chest contents:
// remove blank book from container contents:
boolean removed = false;
for (int slot = 0; slot < newChestContents.length; slot++) {
ItemStack itemStack = newChestContents[slot];
for (int slot = 0; slot < newContainerContents.length; slot++) {
ItemStack itemStack = newContainerContents[slot];
if (ItemUtils.isEmpty(itemStack)) continue;
if (itemStack.getType() != Material.WRITABLE_BOOK) continue;
int newAmount = itemStack.getAmount() - 1;
assert newAmount >= 0;
if (newAmount == 0) {
newChestContents[slot] = null;
newContainerContents[slot] = null;
} else {
// copy the item before modifying it:
itemStack = itemStack.clone();
newChestContents[slot] = itemStack;
newContainerContents[slot] = itemStack;
itemStack.setAmount(newAmount);
}
removed = true;
break;
}
if (!removed) {
this.debugPreventedTrade(tradingPlayer, "The shop's chest doesn't contain any book-and-quill items.");
this.debugPreventedTrade(tradingPlayer, "The shop's container does not contain any writable (book-and-quill) items.");
return false;
}
// add earnings to chest contents:
// add earnings to container contents:
int amountAfterTaxes = this.getAmountAfterTaxes(offer.getPrice());
if (amountAfterTaxes > 0) {
int remaining = amountAfterTaxes;
if (Settings.isHighCurrencyEnabled() && remaining > Settings.highCurrencyMinCost) {
int highCurrencyAmount = (remaining / Settings.highCurrencyValue);
if (highCurrencyAmount > 0) {
int remainingHighCurrency = ItemUtils.addItems(newChestContents, Settings.createHighCurrencyItem(highCurrencyAmount));
int remainingHighCurrency = ItemUtils.addItems(newContainerContents, Settings.createHighCurrencyItem(highCurrencyAmount));
remaining -= ((highCurrencyAmount - remainingHighCurrency) * Settings.highCurrencyValue);
}
}
if (remaining > 0) {
if (ItemUtils.addItems(newChestContents, Settings.createCurrencyItem(remaining)) != 0) {
this.debugPreventedTrade(tradingPlayer, "The shop's chest cannot hold the traded items.");
if (ItemUtils.addItems(newContainerContents, Settings.createCurrencyItem(remaining)) != 0) {
this.debugPreventedTrade(tradingPlayer, "The shop's container cannot hold the traded items.");
return false;
}
}

View File

@ -8,7 +8,6 @@ import java.util.Objects;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.block.Chest;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
@ -23,6 +22,7 @@ import com.nisovin.shopkeepers.api.shopkeeper.offers.BookOffer;
import com.nisovin.shopkeepers.api.shopkeeper.player.PlayerShopCreationData;
import com.nisovin.shopkeepers.api.shopkeeper.player.book.BookPlayerShopkeeper;
import com.nisovin.shopkeepers.api.ui.DefaultUITypes;
import com.nisovin.shopkeepers.container.ShopContainers;
import com.nisovin.shopkeepers.shopkeeper.AbstractShopkeeper;
import com.nisovin.shopkeepers.shopkeeper.SKDefaultShopTypes;
import com.nisovin.shopkeepers.shopkeeper.offers.SKBookOffer;
@ -106,8 +106,8 @@ public class SKBookPlayerShopkeeper extends AbstractPlayerShopkeeper implements
@Override
public List<TradingRecipe> getTradingRecipes(Player player) {
List<TradingRecipe> recipes = new ArrayList<>();
boolean hasBlankBooks = this.hasChestBlankBooks();
List<ItemCount> bookItems = this.getCopyableBooksFromChest();
boolean hasBlankBooks = this.hasContainerBlankBooks();
List<ItemCount> bookItems = this.getCopyableBooksFromContainer();
for (BookOffer offer : this.getOffers()) {
String bookTitle = offer.getBookTitle();
ItemStack bookItem = this.getBookItem(bookItems, bookTitle);
@ -128,8 +128,8 @@ public class SKBookPlayerShopkeeper extends AbstractPlayerShopkeeper implements
return Collections.unmodifiableList(recipes);
}
protected List<ItemCount> getCopyableBooksFromChest() {
return this.getItemsFromChest(ITEM_FILTER);
protected List<ItemCount> getCopyableBooksFromContainer() {
return this.getItemsFromContainer(ITEM_FILTER);
}
protected ItemStack getBookItem(List<ItemCount> itemCounts, String title) {
@ -186,11 +186,11 @@ public class SKBookPlayerShopkeeper extends AbstractPlayerShopkeeper implements
return title;
}
protected boolean hasChestBlankBooks() {
Block chest = this.getChest();
if (ItemUtils.isChest(chest.getType())) {
Inventory chestInventory = ((Chest) chest.getState()).getInventory();
return chestInventory.contains(Material.WRITABLE_BOOK);
protected boolean hasContainerBlankBooks() {
Block container = this.getContainer();
if (ShopContainers.isSupportedContainer(container.getType())) {
Inventory inventory = ShopContainers.getInventory(container);
return inventory.contains(Material.WRITABLE_BOOK);
}
return false;
}

View File

@ -38,19 +38,19 @@ public class BuyingPlayerShopEditorHandler extends PlayerShopEditorHandler {
recipes.add(recipe);
}
// add empty offers for items from the chest:
List<ItemCount> chestItems = shopkeeper.getItemsFromChest();
for (int chestItemIndex = 0; chestItemIndex < chestItems.size(); chestItemIndex++) {
ItemCount itemCount = chestItems.get(chestItemIndex);
ItemStack itemFromChest = itemCount.getItem(); // this item is already a copy with amount 1
// add empty offers for items from the container:
List<ItemCount> containerItems = shopkeeper.getItemsFromContainer();
for (int containerItemIndex = 0; containerItemIndex < containerItems.size(); containerItemIndex++) {
ItemCount itemCount = containerItems.get(containerItemIndex);
ItemStack itemFromContainer = itemCount.getItem(); // this item is already a copy with amount 1
if (shopkeeper.getOffer(itemFromChest) != null) {
if (shopkeeper.getOffer(itemFromContainer) != null) {
continue; // already added
}
// add recipe:
ItemStack currencyItem = Settings.createZeroCurrencyItem();
TradingRecipeDraft recipe = new TradingRecipeDraft(currencyItem, itemFromChest, null);
TradingRecipeDraft recipe = new TradingRecipeDraft(currencyItem, itemFromContainer, null);
recipes.add(recipe);
}

View File

@ -45,27 +45,27 @@ public class BuyingPlayerShopTradingHandler extends PlayerShopTradingHandler {
return false;
}
assert chestInventory != null & newChestContents != null;
assert containerInventory != null & newContainerContents != null;
// remove currency items from chest contents:
int remaining = this.removeCurrency(newChestContents, offer.getPrice());
// remove currency items from container contents:
int remaining = this.removeCurrency(newContainerContents, offer.getPrice());
if (remaining > 0) {
this.debugPreventedTrade(tradingPlayer, "The shop's chest doesn't contain enough currency.");
this.debugPreventedTrade(tradingPlayer, "The shop's container does not contain enough currency.");
return false;
} else if (remaining < 0) {
this.debugPreventedTrade(tradingPlayer, "The shop's chest does not have enough space to split large currency items.");
this.debugPreventedTrade(tradingPlayer, "The shop's container does not have enough space to split large currency items.");
return false;
}
// add bought items to chest contents:
// add bought items to container contents:
int amountAfterTaxes = this.getAmountAfterTaxes(expectedBoughtItemAmount);
if (amountAfterTaxes > 0) {
// the item the trading player gave might slightly differ from the required item,
// but is still accepted, depending on the used item comparison logic and settings:
ItemStack receivedItem = tradeData.offeredItem1.clone(); // create a copy, just in case
receivedItem.setAmount(amountAfterTaxes);
if (ItemUtils.addItems(newChestContents, receivedItem) != 0) {
this.debugPreventedTrade(tradingPlayer, "The shop's chest cannot hold the traded items.");
if (ItemUtils.addItems(newContainerContents, receivedItem) != 0) {
this.debugPreventedTrade(tradingPlayer, "The shop's container cannot hold the traded items.");
return false;
}
}
@ -73,7 +73,7 @@ public class BuyingPlayerShopTradingHandler extends PlayerShopTradingHandler {
}
// TODO simplify this? Maybe by separating into different, general utility functions
// TODO support iterating in reverse order, for nicer looking chest contents?
// TODO support iterating in reverse order, for nicer looking container contents?
// returns the amount of currency that couldn't be removed, 0 on full success, negative if too much was removed
protected int removeCurrency(ItemStack[] contents, int amount) {
Validate.notNull(contents);

View File

@ -103,10 +103,10 @@ public class SKBuyingPlayerShopkeeper extends AbstractPlayerShopkeeper implement
@Override
public List<TradingRecipe> getTradingRecipes(Player player) {
List<TradingRecipe> recipes = new ArrayList<>();
int currencyInChest = this.getCurrencyInChest();
int currencyInContainer = this.getCurrencyInContainer();
for (PriceOffer offer : this.getOffers()) {
ItemStack tradedItem = offer.getItem();
boolean outOfStock = (currencyInChest < offer.getPrice());
boolean outOfStock = (currencyInContainer < offer.getPrice());
TradingRecipe recipe = this.createBuyingRecipe(tradedItem, offer.getPrice(), outOfStock);
if (recipe != null) {
recipes.add(recipe);
@ -115,8 +115,8 @@ public class SKBuyingPlayerShopkeeper extends AbstractPlayerShopkeeper implement
return Collections.unmodifiableList(recipes);
}
protected List<ItemCount> getItemsFromChest() {
return this.getItemsFromChest(ITEM_FILTER);
protected List<ItemCount> getItemsFromContainer() {
return this.getItemsFromContainer(ITEM_FILTER);
}
// OFFERS:

View File

@ -100,15 +100,15 @@ public class SKSellingPlayerShopkeeper extends AbstractPlayerShopkeeper implemen
@Override
public List<TradingRecipe> getTradingRecipes(Player player) {
List<TradingRecipe> recipes = new ArrayList<>();
List<ItemCount> chestItems = this.getItemsFromChest();
List<ItemCount> containerItems = this.getItemsFromContainer();
for (PriceOffer offer : this.getOffers()) {
ItemStack tradedItem = offer.getItem();
int itemAmountInChest = 0;
ItemCount itemCount = ItemCount.findSimilar(chestItems, tradedItem);
int itemAmountInContainer = 0;
ItemCount itemCount = ItemCount.findSimilar(containerItems, tradedItem);
if (itemCount != null) {
itemAmountInChest = itemCount.getAmount();
itemAmountInContainer = itemCount.getAmount();
}
boolean outOfStock = (itemAmountInChest < tradedItem.getAmount());
boolean outOfStock = (itemAmountInContainer < tradedItem.getAmount());
TradingRecipe recipe = this.createSellingRecipe(tradedItem, offer.getPrice(), outOfStock);
if (recipe != null) {
recipes.add(recipe);
@ -117,8 +117,8 @@ public class SKSellingPlayerShopkeeper extends AbstractPlayerShopkeeper implemen
return Collections.unmodifiableList(recipes);
}
protected List<ItemCount> getItemsFromChest() {
return this.getItemsFromChest(ITEM_FILTER);
protected List<ItemCount> getItemsFromContainer() {
return this.getItemsFromContainer(ITEM_FILTER);
}
// OFFERS:

View File

@ -35,18 +35,18 @@ public class SellingPlayerShopEditorHandler extends PlayerShopEditorHandler {
recipes.add(recipe);
}
// add empty offers for items from the chest:
List<ItemCount> chestItems = shopkeeper.getItemsFromChest();
for (int chestItemIndex = 0; chestItemIndex < chestItems.size(); chestItemIndex++) {
ItemCount itemCount = chestItems.get(chestItemIndex);
ItemStack itemFromChest = itemCount.getItem(); // this item is already a copy with amount 1
// add empty offers for items from the container:
List<ItemCount> containerItems = shopkeeper.getItemsFromContainer();
for (int containerItemIndex = 0; containerItemIndex < containerItems.size(); containerItemIndex++) {
ItemCount itemCount = containerItems.get(containerItemIndex);
ItemStack itemFromContainer = itemCount.getItem(); // this item is already a copy with amount 1
if (shopkeeper.getOffer(itemFromChest) != null) {
if (shopkeeper.getOffer(itemFromContainer) != null) {
continue; // already added
}
// add recipe:
TradingRecipeDraft recipe = this.createTradingRecipeDraft(itemFromChest, 0);
TradingRecipeDraft recipe = this.createTradingRecipeDraft(itemFromContainer, 0);
recipes.add(recipe);
}

View File

@ -44,15 +44,15 @@ public class SellingPlayerShopTradingHandler extends PlayerShopTradingHandler {
return false;
}
assert chestInventory != null & newChestContents != null;
assert containerInventory != null & newContainerContents != null;
// remove result items from chest contents:
if (ItemUtils.removeItems(newChestContents, soldItem) != 0) {
this.debugPreventedTrade(tradingPlayer, "The shop's chest doesn't contain the required items.");
// remove result items from container contents:
if (ItemUtils.removeItems(newContainerContents, soldItem) != 0) {
this.debugPreventedTrade(tradingPlayer, "The shop's container does not contain the required items.");
return false;
}
// add earnings to chest contents:
// add earnings to container contents:
// TODO maybe add the actual items the trading player gave, instead of creating new currency items?
int amountAfterTaxes = this.getAmountAfterTaxes(offer.getPrice());
if (amountAfterTaxes > 0) {
@ -62,13 +62,13 @@ public class SellingPlayerShopTradingHandler extends PlayerShopTradingHandler {
if (Settings.isHighCurrencyEnabled() && remaining > Settings.highCurrencyMinCost) {
int highCurrencyAmount = (remaining / Settings.highCurrencyValue);
if (highCurrencyAmount > 0) {
int remainingHighCurrency = ItemUtils.addItems(newChestContents, Settings.createHighCurrencyItem(highCurrencyAmount));
int remainingHighCurrency = ItemUtils.addItems(newContainerContents, Settings.createHighCurrencyItem(highCurrencyAmount));
remaining -= ((highCurrencyAmount - remainingHighCurrency) * Settings.highCurrencyValue);
}
}
if (remaining > 0) {
if (ItemUtils.addItems(newChestContents, Settings.createCurrencyItem(remaining)) != 0) {
this.debugPreventedTrade(tradingPlayer, "The shop's chest cannot hold the traded items.");
if (ItemUtils.addItems(newContainerContents, Settings.createCurrencyItem(remaining)) != 0) {
this.debugPreventedTrade(tradingPlayer, "The shop's container cannot hold the traded items.");
return false;
}
}

View File

@ -93,16 +93,16 @@ public class SKTradingPlayerShopkeeper extends AbstractPlayerShopkeeper implemen
@Override
public List<TradingRecipe> getTradingRecipes(Player player) {
List<TradingRecipe> recipes = new ArrayList<>();
List<ItemCount> chestItems = this.getItemsFromChest();
List<ItemCount> containerItems = this.getItemsFromContainer();
for (TradingOffer offer : this.getOffers()) {
ItemStack resultItem = offer.getResultItem();
assert !ItemUtils.isEmpty(resultItem);
int itemAmountInChest = 0;
ItemCount itemCount = ItemCount.findSimilar(chestItems, resultItem);
int itemAmountInContainer = 0;
ItemCount itemCount = ItemCount.findSimilar(containerItems, resultItem);
if (itemCount != null) {
itemAmountInChest = itemCount.getAmount();
itemAmountInContainer = itemCount.getAmount();
}
boolean outOfStock = (itemAmountInChest < resultItem.getAmount());
boolean outOfStock = (itemAmountInContainer < resultItem.getAmount());
TradingRecipe recipe = ShopkeepersAPI.createTradingRecipe(resultItem, offer.getItem1(), offer.getItem2(), outOfStock);
if (recipe != null) {
recipes.add(recipe);
@ -111,8 +111,8 @@ public class SKTradingPlayerShopkeeper extends AbstractPlayerShopkeeper implemen
return Collections.unmodifiableList(recipes);
}
protected List<ItemCount> getItemsFromChest() {
return this.getItemsFromChest(null);
protected List<ItemCount> getItemsFromContainer() {
return this.getItemsFromContainer(null);
}
// OFFERS:

View File

@ -42,18 +42,18 @@ public class TradingPlayerShopEditorHandler extends PlayerShopEditorHandler {
recipes.add(recipe);
}
// add empty offers for items from the chest:
List<ItemCount> chestItems = shopkeeper.getItemsFromChest();
for (int chestItemIndex = 0; chestItemIndex < chestItems.size(); chestItemIndex++) {
ItemCount itemCount = chestItems.get(chestItemIndex);
ItemStack itemFromChest = itemCount.getItem(); // this item is already a copy with amount 1
// add empty offers for items from the container:
List<ItemCount> containerItems = shopkeeper.getItemsFromContainer();
for (int containerItemIndex = 0; containerItemIndex < containerItems.size(); containerItemIndex++) {
ItemCount itemCount = containerItems.get(containerItemIndex);
ItemStack itemFromContainer = itemCount.getItem(); // this item is already a copy with amount 1
if (shopkeeper.getOffer(itemFromChest) != null) {
if (shopkeeper.getOffer(itemFromContainer) != null) {
continue; // already added
}
// add recipe:
TradingRecipeDraft recipe = new TradingRecipeDraft(itemFromChest, null, null);
TradingRecipeDraft recipe = new TradingRecipeDraft(itemFromContainer, null, null);
recipes.add(recipe);
}

View File

@ -34,20 +34,20 @@ public class TradingPlayerShopTradingHandler extends PlayerShopTradingHandler {
return false;
}
assert chestInventory != null & newChestContents != null;
assert containerInventory != null & newContainerContents != null;
// remove result items from chest contents:
// remove result items from container contents:
ItemStack resultItem = tradingRecipe.getResultItem();
assert resultItem != null;
if (ItemUtils.removeItems(newChestContents, resultItem) != 0) {
this.debugPreventedTrade(tradingPlayer, "The shop's chest doesn't contain the required items.");
if (ItemUtils.removeItems(newContainerContents, resultItem) != 0) {
this.debugPreventedTrade(tradingPlayer, "The shop's container does not contain the required items.");
return false;
}
// add traded items to chest contents:
if (!this.addItems(newChestContents, tradingRecipe.getItem1(), tradeData.offeredItem1)
|| !this.addItems(newChestContents, tradingRecipe.getItem2(), tradeData.offeredItem2)) {
this.debugPreventedTrade(tradingPlayer, "The shop's chest cannot hold the traded items.");
// add traded items to container contents:
if (!this.addItems(newContainerContents, tradingRecipe.getItem1(), tradeData.offeredItem1)
|| !this.addItems(newContainerContents, tradingRecipe.getItem2(), tradeData.offeredItem2)) {
this.debugPreventedTrade(tradingPlayer, "The shop's container cannot hold the traded items.");
return false;
}
return true;

View File

@ -38,8 +38,8 @@ import net.citizensnpcs.trait.LookClose;
* Citizen shopkeepers can be removed again in the following ways:
* <ol>
* <li>The shopkeeper gets deleted, either due to a player removing it via command or via the editor option, or due the
* Shopkeepers plugin removing it due to reasons such owner inactivity or when the shopkeeper's chest is broken. If the
* corresponding Citizens NPC has the 'shopkeeper' trait attached, only this trait gets removed and the NPC remains
* Shopkeepers plugin removing it due to reasons such owner inactivity or when the shopkeeper's container is broken. If
* the corresponding Citizens NPC has the 'shopkeeper' trait attached, only this trait gets removed and the NPC remains
* existing. Otherwise, the Citizens NPC is removed.
* TODO Removing the Citizens NPC only works if the Citizens plugin is currently running.
* <li>The Citizens NPC gets deleted. This deletes the corresponding shopkeeper. If the Shopkeepers plugin is not

View File

@ -5,9 +5,10 @@ import com.nisovin.shopkeepers.SKShopkeepersPlugin;
// TODO not yet used
public class VirtualShops {
// TODO not all shopkeeper types might support virtual shops (eg. player shops require a world to locate the chest)
// -> add a flag to ShopType and validation somewhere (shop creation and shop loading)
// And/Or: store player shop chest world separately
// TODO Not all shopkeeper types might support virtual shops (eg. player shops require a world to locate the
// container currently)
// -> Add a flag to ShopType and validation somewhere (shop creation and shop loading).
// And/Or: Store player shop container world separately.
private final SKShopkeepersPlugin plugin;
private final SKVirtualShopObjectType virtualShopObjectType = new SKVirtualShopObjectType(this);

View File

@ -480,7 +480,7 @@ public abstract class EditorHandler extends UIHandler {
AbstractShopkeeper shopkeeper = this.getShopkeeper();
this.addButtonOrIgnore(this.createDeleteButton(shopkeeper));
this.addButtonOrIgnore(this.createNamingButton(shopkeeper));
this.addButtonOrIgnore(this.createChestButton(shopkeeper));
this.addButtonOrIgnore(this.createContainerButton(shopkeeper));
this.addButtonsOrIgnore(shopkeeper.getShopObject().getEditorButtons());
}
@ -542,23 +542,23 @@ public abstract class EditorHandler extends UIHandler {
};
}
protected Button createChestButton(Shopkeeper shopkeeper) {
if (!Settings.enableChestOptionOnPlayerShop || !(shopkeeper.getType() instanceof PlayerShopType)) {
protected Button createContainerButton(Shopkeeper shopkeeper) {
if (!Settings.enableContainerOptionOnPlayerShop || !(shopkeeper.getType() instanceof PlayerShopType)) {
return null;
}
return new Button(shopkeeper) {
@Override
public ItemStack getIcon(Session session) {
return Settings.createChestButtonItem();
return Settings.createContainerButtonItem();
}
@Override
protected void onClick(InventoryClickEvent clickEvent, Player player) {
// chest inventory button:
// Container inventory button:
closeEditorAndRunTask(player, () -> {
// open chest inventory:
// Open the shop container inventory:
if (!player.isValid()) return;
((PlayerShopkeeper) shopkeeper).openChestWindow(player);
((PlayerShopkeeper) shopkeeper).openContainerWindow(player);
});
}
};

View File

@ -670,7 +670,7 @@ public class TradingHandler extends UIHandler {
* inventory action multiple successive trades (even using different trading recipes) might get triggered by a
* single inventory action.
* <p>
* There should be no changes of the corresponding click event and the involved inventories (player, chest) have to
* There should be no changes of the corresponding click event and the involved inventories (player, container) to
* be expected between this phase of the trade handling and the actual application of the trade.
*
* @param tradeData

View File

@ -38,15 +38,82 @@ public final class ItemUtils {
// material utilities:
/**
* Checks if the given material is a container.
*
* @param material
* the material
* @return <code>true</code> if the material is a container
*/
public static boolean isContainer(Material material) {
// TODO This list of container materials needs to be updated with each MC update.
if (material == null) return false;
if (isChest(material)) return true; // Includes trapped chest
if (isShulkerBox(material)) return true;
switch (material) {
case BARREL:
case BREWING_STAND:
case DISPENSER:
case DROPPER:
case HOPPER:
case FURNACE:
case BLAST_FURNACE:
case SMOKER:
case ENDER_CHEST: // Note: Has no BlockState of type Container.
return true;
default:
return false;
}
}
public static boolean isChest(Material material) {
return material == Material.CHEST || material == Material.TRAPPED_CHEST;
}
public static boolean isShulkerBox(Material material) {
if (material == null) return false;
switch (material) {
case SHULKER_BOX:
case WHITE_SHULKER_BOX:
case ORANGE_SHULKER_BOX:
case MAGENTA_SHULKER_BOX:
case LIGHT_BLUE_SHULKER_BOX:
case YELLOW_SHULKER_BOX:
case LIME_SHULKER_BOX:
case PINK_SHULKER_BOX:
case GRAY_SHULKER_BOX:
case LIGHT_GRAY_SHULKER_BOX:
case CYAN_SHULKER_BOX:
case PURPLE_SHULKER_BOX:
case BLUE_SHULKER_BOX:
case BROWN_SHULKER_BOX:
case GREEN_SHULKER_BOX:
case RED_SHULKER_BOX:
case BLACK_SHULKER_BOX:
return true;
default:
return false;
}
}
public static boolean isSign(Material material) {
if (material == null) return false;
return material.data == org.bukkit.block.data.type.Sign.class || material.data == org.bukkit.block.data.type.WallSign.class;
}
public static boolean isRail(Material material) {
if (material == null) return false;
switch (material) {
case RAIL:
case POWERED_RAIL:
case DETECTOR_RAIL:
case ACTIVATOR_RAIL:
return true;
default:
return false;
}
}
// itemstack utilities:
public static boolean isEmpty(ItemStack item) {

View File

@ -29,6 +29,7 @@ import com.nisovin.shopkeepers.api.shopkeeper.Shopkeeper;
import com.nisovin.shopkeepers.api.shopkeeper.TradingRecipe;
import com.nisovin.shopkeepers.api.shopkeeper.admin.AdminShopkeeper;
import com.nisovin.shopkeepers.api.shopkeeper.player.PlayerShopkeeper;
import com.nisovin.shopkeepers.container.ShopContainers;
import com.nisovin.shopkeepers.text.Text;
/**
@ -204,22 +205,22 @@ public class ShopkeeperUtils {
// get shopkeeper by targeted block:
shopkeeper = ShopkeepersAPI.getShopkeeperRegistry().getShopkeeperByBlock(targetBlock);
if (shopkeeper == null) {
// get player shopkeepers by targeted chest:
if (ItemUtils.isChest(targetBlock.getType())) {
List<PlayerShopkeeper> shopsUsingChest = SKShopkeepersPlugin.getInstance().getProtectedChests().getShopkeepersUsingChest(targetBlock);
if (shopsUsingChest.isEmpty()) {
return new TargetShopkeepersResult(Settings.msgUnusedChest);
} else {
// Get player shopkeepers by targeted container:
if (ShopContainers.isSupportedContainer(targetBlock.getType())) {
List<PlayerShopkeeper> shopsUsingContainer = SKShopkeepersPlugin.getInstance().getProtectedContainers().getShopkeepersUsingContainer(targetBlock);
if (shopsUsingContainer.isEmpty()) {
return new TargetShopkeepersResult(Settings.msgUnusedContainer);
} else {
// filter shops:
List<Shopkeeper> acceptedShops = new ArrayList<>();
for (Shopkeeper shopUsingChest : shopsUsingChest) {
if (shopkeeperFilter.test(shopUsingChest)) {
acceptedShops.add(shopUsingChest);
for (Shopkeeper shopUsingContainer : shopsUsingContainer) {
if (shopkeeperFilter.test(shopUsingContainer)) {
acceptedShops.add(shopUsingContainer);
}
}
if (acceptedShops.isEmpty()) {
// use the first shopkeeper using the chest for the error message:
return new TargetShopkeepersResult(shopkeeperFilter.getInvalidTargetErrorMsg(shopsUsingChest.get(0)));
// Use the first shopkeeper using the container for the error message:
return new TargetShopkeepersResult(shopkeeperFilter.getInvalidTargetErrorMsg(shopsUsingContainer.get(0)));
} else {
return new TargetShopkeepersResult(acceptedShops);
}

View File

@ -6,7 +6,7 @@
# *~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*
# Determines the required config migrations. Do not edit manually!
config-version: 2
config-version: 3
# The initial debugging state of the plugin.
debug: false
# Additional debugging options.
@ -45,9 +45,9 @@ file-encoding: "UTF-8"
save-instantly: true
# Enables the automatic conversion of items inside the inventories of players
# and shop chests whenever a player opens a shopkeeper UI (eg. trading, editor,
# hiring, etc.) The items are converted to conform to Spigot's internal data
# format.
# and shop containers whenever a player opens a shopkeeper UI (eg. trading,
# editor, hiring, etc.) The items are converted to conform to Spigot's internal
# data format.
# This setting helps with issues related to items which have been created on
# previous Spigot versions or via some Minecraft mechanism (eg. give command,
# loot tables, etc.) no longer getting accepted when trading with shopkeepers.
@ -97,9 +97,10 @@ enable-world-guard-restrictions: false
# If enabled (additionally to the enable-world-guard-restrictions setting),
# players will only be able to place shopkeepers in regions where the
# 'allow-shop' flag is set, but nowhere else. However, players will still
# require chest access for shop setup to work. And in case they can't place
# chests in the affected region, shop chests need to be pre-setup by someone
# else and the require-chest-recently-placed setting needs to be disabled.
# require container access to setup shops. And in case they can't place
# containers in the affected region, shop containers need to be pre-setup by
# someone else and the require-container-recently-placed setting needs to be
# disabled.
require-world-guard-allow-shop-flag: false
# Whether to register the allow-shop flag with WorldGuard (if no other plugin
# has registered it yet). Usually there should be no need to disable this.
@ -131,12 +132,12 @@ deleting-player-shop-returns-creation-item: false
# Whether to allow creating player shops with the /shopkeeper command.
create-player-shop-with-command: false
# Whether the selected chest must have been recently placed by the player
# attempting to create the shopkeeper.
require-chest-recently-placed: true
# Whether the selected container must have been recently placed by the player
# who is attempting to create the shopkeeper.
require-container-recently-placed: true
# The maximum distance a player shopkeeper can be placed from its backing
# chest. This cannot be set to a value greater than 50.
max-chest-distance: 15
# container. This cannot be set to a value greater than 50.
max-container-distance: 15
# The default maximum number of shops a player can have. Set to 0 to allow any
# number of shops.
max-shops-per-player: 0
@ -145,15 +146,15 @@ max-shops-per-player: 0
# shopkeeper.maxshops.<count> permission node pattern to use this feature.
max-shops-perm-options: 5,15,25
# Whether to protect player shop chests from being accessed or broken. Usually
# it is recommended to keep this enabled.
protect-chests: true
# Whether to prevent item movement from and to protected shop chests (via
# hoppers, droppers, etc.). Item movement will always be allowed if the chest
# protection is disabled.
# Whether to protect player shop containers from being accessed or broken. It
# is usually recommended to keep this enabled.
protect-containers: true
# Whether to prevent item movements from and to protected shop containers (via
# hoppers, droppers, etc.). Item movement will always be allowed if the
# container protection is disabled.
prevent-item-movement: true
# Whether to delete player shopkeepers when their backing chest is broken.
delete-shopkeeper-on-break-chest: false
# Whether to delete player shopkeepers when their container is broken.
delete-shopkeeper-on-break-container: false
# If enabled (set to a value greater than 0), Shopkeepers will check for and
# remove the shops of inactive players once every plugin start. This setting
@ -305,11 +306,11 @@ trade-setup-item: PAPER
# The item used for the set-name button, and the naming item (if enabled).
name-item: NAME_TAG
# 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 used for the open-chest button.
chest-item: CHEST
# Whether the editor menu of player shops contains an option to open the shop's
# container.
enable-container-option-on-player-shop: true
# The item used for the 'open container' editor button.
container-item: CHEST
# The item used for the delete button.
delete-item: BONE
@ -438,7 +439,7 @@ msg-creation-item-selected: |-
&aShop creation:
&e Left/Right-click to select the shop type.
&e Sneak + left/right-click to select the object type.
&e Right-click a chest to select it.
&e Right-click a container to select it.
&e Then right-click a block to place the shopkeeper.
msg-button-previous-page: "&6<- Previous page ({prev_page} of {max_page})"
@ -452,8 +453,8 @@ msg-button-name: "&aSet shop name"
msg-button-name-lore:
- Lets you rename
- your shopkeeper
msg-button-chest: "&aView chest inventory"
msg-button-chest-lore:
msg-button-container: "&aView shop inventory"
msg-button-container-lore:
- Lets you view the inventory
- your shopkeeper is using
msg-button-delete: "&4Delete"
@ -577,13 +578,14 @@ msg-button-magma-cube-size-lore:
msg-trading-title-prefix: "&2"
msg-trading-title-default: "Shopkeeper"
msg-selected-chest: "&aChest selected! Right-click a block to place your shopkeeper."
msg-must-select-chest: "&7You must right-click a chest before placing your shopkeeper."
msg-no-chest-selected: "&7The selected block is not a chest!"
msg-chest-too-far: "&7The shopkeeper's chest is too far away!"
msg-chest-not-placed: "&7You must select a chest you have recently placed!"
msg-chest-already-in-use: "&7Another shopkeeper is already using the selected chest!"
msg-no-chest-access: "&7You cannot access the selected chest!"
msg-container-selected: "&aContainer selected! Right-click a block to place your shopkeeper."
msg-unsupported-container: "&7This type of container cannot be used for shops."
msg-must-select-container: "&7You must right-click a container before placing your shopkeeper."
msg-invalid-container: "&7The selected block is not a valid container!"
msg-container-too-far-away: "&7The shopkeeper's container is too far away!"
msg-container-not-placed: "&7You must select a container you have recently placed!"
msg-container-already-in-use: "&7Another shopkeeper is already using the selected container!"
msg-no-container-access: "&7You cannot access the selected container!"
msg-too-many-shops: "&7You have too many shops!"
msg-no-admin-shop-type-selected: "&7You have to select an admin shop type!"
msg-no-player-shop-type-selected: "&7You have to select a player shop type!"
@ -603,7 +605,7 @@ msg-must-target-player-shop: "&7You have to target a player shopkeeper."
msg-target-entity-is-no-shop: "&7The targeted entity is no shopkeeper."
msg-target-shop-is-no-admin-shop: "&7The targeted shopkeeper is no admin shopkeeper."
msg-target-shop-is-no-player-shop: "&7The targeted shopkeeper is no player shopkeeper."
msg-unused-chest: "&7No shopkeeper is using this chest."
msg-unused-container: "&7No shopkeeper is using this container."
msg-not-owner: "&7You are not the owner of this shopkeeper."
msg-owner-set: "&aNew owner was set to &e{owner}"
msg-shop-creation-items-given: "&aPlayer &e{player}&a has received &e{amount}&a shop creation item(s)!"
@ -633,21 +635,21 @@ msg-missing-trade-perm: "&7You do not have the permission to trade with this sho
msg-missing-custom-trade-perm: "&7You do not have the permission to trade with this shop."
msg-cant-trade-with-own-shop: "&7You cannot trade with your own shop."
msg-cant-trade-while-owner-online: "&7You cannot trade while the owner of this shop ('&e{owner}&7') is online."
msg-cant-trade-with-shop-missing-chest: "&7You cannot trade with this shop, because its chest is missing."
msg-cant-trade-with-shop-missing-container: "&7You cannot trade with this shop, because its container is missing."
msg-shopkeeper-created: "&aShopkeeper created: &6{type} &7({description})\n{setupDesc}"
msg-shop-setup-desc-selling: |-
&e Add items you want to sell to your chest, then
&e Add items you want to sell to your container, then
&e right-click the shop while sneaking to modify costs.
msg-shop-setup-desc-buying: |-
&e Add one of each item you want to buy to your chest, then
&e right-click the shop while sneaking to modify costs.
&e Add one of each item you want to buy to your container,
&e then right-click the shop while sneaking to modify costs.
msg-shop-setup-desc-trading: |-
&e Add items you want to sell to your chest, then
&e Add items you want to sell to your container, then
&e right-click the shop while sneaking to modify costs.
msg-shop-setup-desc-book: |-
&e Add written books and blank books to your chest, then
&e Add written and blank books to your container, then
&e right-click the shop while sneaking to modify costs.
msg-shop-setup-desc-admin-regular: |-
&e Right-click the shop while sneaking to modify trades.
@ -660,7 +662,7 @@ msg-trade-setup-desc-admin-regular:
- 'Bottom rows: Cost items'
msg-trade-setup-desc-selling:
- Sells items to players.
- Insert items to sell into the chest.
- Insert items to sell into the container.
- Left/Right click to adjust amounts.
- 'Top row: Items being sold'
- 'Bottom rows: Cost items'
@ -668,7 +670,7 @@ msg-trade-setup-desc-buying:
- Buys items from players.
- Insert one of each item you want to
- buy and plenty of currency items
- into the chest.
- into the container.
- Left/Right click to adjust amounts.
- 'Top row: Cost items'
- 'Bottom row: Items being bought'
@ -682,7 +684,7 @@ msg-trade-setup-desc-trading:
msg-trade-setup-desc-book:
- Sells book copies.
- Insert written and blank books
- into the chest.
- into the container.
- Left/Right click to adjust costs.
- 'Top row: Books being sold'
- 'Bottom rows: Cost items'

View File

@ -23,7 +23,7 @@ msg-creation-item-selected: |-
&aShop-Erstellung:
&e Links-/Rechtsklick, um den Shop-Typ auszuwählen.
&e Shift + Links-/Rechtsklick, um den Objekt-Typ auszuwählen.
&e Rechtsklick auf eine Kiste, um diese auszuwählen.
&e Rechtsklick auf einen Behälter, um diesen auszuwählen.
&e Dann Rechtsklick auf einen Block, um dort den Händler zu erstellen.
msg-button-previous-page: "&6<- Vorherige Seite ({prev_page} von {max_page})"
@ -37,8 +37,8 @@ msg-button-name: "&aShop Namen ändern"
msg-button-name-lore:
- Gib deinem Shop
- einen Namen
msg-button-chest: "&aShop-Inventar öffnen"
msg-button-chest-lore:
msg-button-container: "&aShop-Inventar öffnen"
msg-button-container-lore:
- Zeigt dir das Inventar
- deines Verkäufers
msg-button-delete: "&4Entfernen"
@ -167,13 +167,14 @@ msg-button-magma-cube-size-lore:
msg-trading-title-prefix: "&2"
msg-trading-title-default: "Händler"
msg-selected-chest: "&aTruhe ausgewählt! Klicke mit rechts auf einen Block, um den Shop zu platzieren."
msg-must-select-chest: "&7Du musst zuerst eine Truhe mit rechts anklicken, bevor du den Shop platzieren kannst."
msg-no-chest-selected: "&7Der ausgewählte Block ist keine Truhe!"
msg-chest-too-far: "&7Die Truhe des Shops ist zu weit entfernt!"
msg-chest-not-placed: "&7Du musst eine Truhe auswählen, die du kürzlich platziert hast!"
msg-chest-already-in-use: "&7Ein anderer Shop benutzt bereits die ausgewählte Truhe!"
msg-no-chest-access: "&7Du hast keinen Zugriff auf die ausgewählte Truhe!"
msg-container-selected: "&aBehälter ausgewählt! Klicke mit rechts auf einen Block, um den Shop zu platzieren."
msg-unsupported-container: "&7Diese Art von Behälter kann nicht für Shops benutzt werden."
msg-must-select-container: "&7Du musst zuerst einen Behälter mit rechts anklicken, bevor du den Shop platzieren kannst."
msg-invalid-container: "&7Der ausgewählte Block ist kein gültiger Behälter!"
msg-container-too-far-away: "&7Der Behälter des Shops ist zu weit entfernt!"
msg-container-not-placed: "&7Du musst einen Behälter auswählen, den du kürzlich platziert hast!"
msg-container-already-in-use: "&7Ein anderer Shop benutzt bereits den ausgewählten Behälter!"
msg-no-container-access: "&7Du hast keinen Zugriff auf den ausgewählten Behälter!"
msg-too-many-shops: "&7Du hast bereits zu viele Shops!"
msg-no-admin-shop-type-selected: "&7Du musst einen Admin-Shop Typ auswählen!"
msg-no-player-shop-type-selected: "&7Du musst einen Spieler-Shop Typ auswählen!"
@ -193,7 +194,7 @@ msg-must-target-player-shop: "&7Du musst einen Spieler-Shop anvisieren."
msg-target-entity-is-no-shop: "&7Das anvisierte Objekt ist kein Shop."
msg-target-shop-is-no-admin-shop: "&7Der anvisierte Shop ist kein Admin-Shop."
msg-target-shop-is-no-player-shop: "&7Der anvisierte Shop ist kein Spieler-Shop."
msg-unused-chest: "&7Kein Shop benutzt diese Truhe."
msg-unused-container: "&7Dieser Behälter wird von keinem Shop benutzt."
msg-not-owner: "&7Du bist nicht der Besitzer von diesem Shop."
msg-owner-set: "&aDer neue Besitzer ist jetzt: &e{owner}"
msg-shop-creation-items-given: "&aSpieler &e{player}&a hat &e{amount}&a Shop-Erstellungs Items bekommen!"
@ -223,21 +224,21 @@ msg-missing-trade-perm: "&7Du hast nicht die nötige Befugnis, um mit diesem Sho
msg-missing-custom-trade-perm: "&7Du hast nicht die nötige Befugnis, um mit diesem Shop zu handeln."
msg-cant-trade-with-own-shop: "&7Du kannst nicht mit deinem eigenen Shop handeln."
msg-cant-trade-while-owner-online: "&7Du kannst nicht handeln, während der Besitzer dieses Shops ('&e{owner}&7') online ist."
msg-cant-trade-with-shop-missing-chest: "&7Du kannst nicht mit diesem Shop handeln, da seine Truhe fehlt."
msg-cant-trade-with-shop-missing-container: "&7Du kannst nicht mit diesem Shop handeln, da sein Behälter fehlt."
msg-shopkeeper-created: "&aShop erstellt: &6{type} &7({description})\n{setupDesc}"
msg-shop-setup-desc-selling: |-
&e Lege die zu verkaufenden Items in die Truhe und
&e Lege die zu verkaufenden Items in den Behälter und
&e gib mit Shift+Rechts Klick auf den Shop die Preise an.
msg-shop-setup-desc-buying: |-
&e Lege von jedem Item, das du ankaufen möchtest, eines in die Truhe.
&e Lege von jedem Item, das du ankaufen möchtest, eines in den Behälter.
&e Mit Shift+Rechts Klick auf den Shop legst du die Preise fest.
msg-shop-setup-desc-trading: |-
&e Lege die Items, die du eintauschen möchtest, in die Truhe.
&e Lege die Items, die du eintauschen möchtest, in den Behälter.
&e Mit Shift+Rechts Klick auf den Shop legst du die Angebote fest.
msg-shop-setup-desc-book: |-
&e Lege das signierte Buch und soviele leere Bücher in die Truhe, wie Kopien verkauft werden sollen.
&e Lege signierte und leere Bücher in den Behälter.
&e Mit Shift+Rechts Klick auf den Shop legst du die Preise fest.
msg-shop-setup-desc-admin-regular: |-
&e Mit Shift+Rechts Klick auf den Shop legst du die Preise fest.
@ -250,14 +251,14 @@ msg-trade-setup-desc-admin-regular:
- 'Untere Reihen: Kosten'
msg-trade-setup-desc-selling:
- Verkauft Items an Spieler.
- Lege die zu verkaufenden Items in die Truhe.
- Lege die zu verkaufenden Items in den Behälter.
- Links/Rechts Klick zum Ändern der Mengen.
- 'Obere Reihe: Verkaufte Items'
- 'Untere Reihen: Kosten'
msg-trade-setup-desc-buying:
- Kauft Items von Spielern.
- Lege von jedem anzukaufendem Item eines
- in die Truhe, sowie reichlich Währung.
- in den Behälter, sowie reichlich Währung.
- Links/Rechts Klick zum Ändern der Mengen.
- 'Obere Reihe: Kosten'
- 'Untere Reihe: Angekaufte Items'
@ -270,7 +271,7 @@ msg-trade-setup-desc-trading:
- 'Untere Reihen: Kosten'
msg-trade-setup-desc-book:
- Verkauft Kopien von Büchern.
- Lege signierte und leere Bücher in die Truhe.
- Lege signierte und leere Bücher in den Behälter.
- Links/Rechts Klick zum Ändern der Kosten.
- 'Obere Reihe: Verkaufte Bücher'
- 'Untere Reihen: Kosten'