Few improvements (#135)

* Fix: Few improvements

* Few improvements

* Hologram/item can wait after shop creation

* Compare worlds using their name

* Fix holograms display

* Changed version to 1.12.4

* Display shop after creation

* Fix requested changed

* Improve performance for simple hologram conditions
This commit is contained in:
MineTheCube 2017-08-10 17:02:24 +02:00 committed by EpicEric
parent efced89eb1
commit 3b9d26c079
13 changed files with 722 additions and 431 deletions

View File

@ -25,7 +25,7 @@
<maven.compiler.target>${jdkVersion}</maven.compiler.target>
<!-- Versioning -->
<version.number>1.12.3</version.number>
<version.number>1.12.4</version.number>
<version.final>${version.number}-${version.git}</version.final>
<!-- Others -->

View File

@ -204,7 +204,7 @@ public class ShopChest extends JavaPlugin {
}
if (database != null) {
for (Shop shop : shopUtils.getShops()) {
for (Shop shop : shopUtils.getShopsCopy()) {
shopUtils.removeShop(shop, false);
debug("Removed shop (#" + shop.getID() + ")");
}

View File

@ -1,6 +1,7 @@
package de.epiceric.shopchest.config;
import de.epiceric.shopchest.ShopChest;
import de.epiceric.shopchest.utils.Operator;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
@ -21,6 +22,13 @@ public class HologramFormat {
NORMAL_SHOP, IN_STOCK, MAX_STACK, CHEST_SPACE, DURABILITY
}
// no "-" sign since no variable can be negative
// e.g.: 100.0 >= 50.0
private static final Pattern SIMPLE_NUMERIC_CONDITION = Pattern.compile("^(\\d+(?:\\.\\d+)?) ([<>][=]?|[=!]=) (\\d+(?:\\.\\d+)?)$");
// e.g.: "STONE" == "DIAMOND_SWORD"
private static final Pattern SIMPLE_STRING_CONDITION = Pattern.compile("^\"([^\"]*)\" ([=!]=) \"([^\"]*)\"$");
private ShopChest plugin;
private File configFile;
private YamlConfiguration config;
@ -110,32 +118,78 @@ public class HologramFormat {
* @return Result of the condition
*/
public boolean evalRequirement(String condition, Map<Requirement, Object> values) {
try {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("JavaScript");
String cond = condition;
String cond = condition;
for (HologramFormat.Requirement req : HologramFormat.Requirement.values()) {
if (cond.contains(req.toString()) && values.containsKey(req)) {
Object val = values.get(req);
String sVal = String.valueOf(val);
for (HologramFormat.Requirement req : HologramFormat.Requirement.values()) {
if (cond.contains(req.toString()) && values.containsKey(req)) {
Object val = values.get(req);
String sVal = String.valueOf(val);
if (val instanceof String && !(sVal.startsWith("\"") && sVal.endsWith("\""))) {
sVal = String.format("\"%s\"", sVal);
}
if (val instanceof String && !(sVal.startsWith("\"") && sVal.endsWith("\""))) {
sVal = String.format("\"%s\"", sVal);
cond = cond.replace(req.toString(), sVal);
}
}
if (cond.equals("true")) {
// e.g.: ADMIN_SHOP
return true;
} else if (cond.equals("false")) {
return false;
} else {
char firstChar = cond.charAt(0);
// numeric cond: first char must be a digit (no variable can be negative)
if (firstChar >= '0' && firstChar <= '9') {
Matcher matcher = SIMPLE_NUMERIC_CONDITION.matcher(cond);
if (matcher.find()) {
Double a, b;
Operator operator;
try {
a = Double.valueOf(matcher.group(1));
operator = Operator.from(matcher.group(2));
b = Double.valueOf(matcher.group(3));
return operator.compare(a, b);
} catch (IllegalArgumentException ignored) {
// should not happen, since regex checked that there is valid number and valid operator
}
cond = cond.replace(req.toString(), sVal);
}
}
return (boolean) engine.eval(cond);
} catch (ScriptException e) {
plugin.debug("Failed to eval condition: " + condition);
plugin.debug(e);
}
// string cond: first char must be a: "
if (firstChar == '"') {
Matcher matcher = SIMPLE_STRING_CONDITION.matcher(cond);
return false;
if (matcher.find()) {
String a, b;
Operator operator;
try {
a = matcher.group(1);
operator = Operator.from(matcher.group(2));
b = matcher.group(3);
return operator.compare(a, b);
} catch (IllegalArgumentException | UnsupportedOperationException ignored) {
// should not happen, since regex checked that there is valid operator
}
}
}
// complex comparison
try {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("JavaScript");
return (boolean) engine.eval(cond);
} catch (ScriptException e) {
plugin.debug("Failed to eval condition: " + condition);
plugin.debug(e);
return false;
}
}
}
/**

View File

@ -691,17 +691,9 @@ public class ShopInteractListener implements Listener {
executor.sendMessage(LanguageUtils.getMessage(LocalizedMessage.Message.SHOP_CREATED, placeholder));
}
for (Player p : location.getWorld().getPlayers()) {
if (p.getLocation().distanceSquared(location) <= Math.pow(config.maximal_distance, 2)) {
if (shop.getHologram() != null) {
shop.getHologram().showPlayer(p);
}
}
if (p.getLocation().distanceSquared(location) <= Math.pow(config.maximal_item_distance, 2)) {
if (shop.getItem() != null) {
shop.getItem().setVisible(p, true);
}
}
// next update will display the new shop
for (Player player : location.getWorld().getPlayers()) {
plugin.getShopUtils().resetPlayerLocation(player);
}
}

View File

@ -27,18 +27,6 @@ public class ShopItemListener implements Listener {
this.shopUtils = plugin.getShopUtils();
}
@EventHandler
public void onPlayerLeave(PlayerQuitEvent e) {
for (Shop shop : plugin.getShopUtils().getShops()) {
if (shop.getItem() != null) {
shop.getItem().setVisible(e.getPlayer(), false);
}
if (shop.getHologram() != null) {
shop.getHologram().hidePlayer(e.getPlayer());
}
}
}
@EventHandler(priority = EventPriority.HIGH)
public void onBlockPlace(BlockPlaceEvent e) {
Block b = e.getBlockPlaced();

View File

@ -8,6 +8,7 @@ import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
import org.bukkit.event.world.WorldLoadEvent;
import org.bukkit.scheduler.BukkitRunnable;
@ -20,6 +21,20 @@ public class ShopUpdateListener implements Listener {
this.plugin = plugin;
}
@EventHandler
public void onPlayerLeave(PlayerQuitEvent e) {
for (Shop shop : plugin.getShopUtils().getShops()) {
if (shop.hasItem()) {
shop.getItem().resetVisible(e.getPlayer());
}
if (shop.hasHologram()) {
shop.getHologram().resetVisible(e.getPlayer());
}
}
plugin.getShopUtils().resetPlayerLocation(e.getPlayer());
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onPlayerTeleport(PlayerTeleportEvent e) {
Location from = e.getFrom();
@ -28,7 +43,7 @@ public class ShopUpdateListener implements Listener {
// Wait till the chunk should have loaded on the client
// Update IF worlds are different OR chunks are different (as many teleports are in same chunk)
if (!from.getWorld().equals(to.getWorld())
if (!from.getWorld().getName().equals(to.getWorld().getName())
|| from.getChunk().getX() != to.getChunk().getX()
|| from.getChunk().getZ() != to.getChunk().getZ()) {
// Wait for 15 ticks before we actually put it in the queue
@ -40,13 +55,15 @@ public class ShopUpdateListener implements Listener {
public void run() {
if (p.isOnline()) {
for (Shop shop : plugin.getShopUtils().getShops()) {
if (shop.getItem() != null) {
shop.getItem().setVisible(p, false);
if (shop.hasItem()) {
shop.getItem().hidePlayer(p);
}
if (shop.getHologram() != null) {
if (shop.hasHologram()) {
shop.getHologram().hidePlayer(p);
}
}
// so next update will update correctly
plugin.getShopUtils().resetPlayerLocation(p);
}
}
});

View File

@ -32,7 +32,7 @@ public class ArmorStandWrapper {
private UUID uuid;
private int entityId;
public ArmorStandWrapper(ShopChest plugin, Location location, String customName) {
public ArmorStandWrapper(ShopChest plugin, Location location, String customName, boolean interactable) {
this.plugin = plugin;
this.location = location;
this.customName = customName;
@ -58,9 +58,12 @@ public class ArmorStandWrapper {
entityArmorStandClass.getMethod("setInvisible", boolean.class).invoke(entity, true);
// Adds the entity to some lists so it can call interact events
Method addEntityMethod = worldServerClass.getDeclaredMethod((Utils.getMajorVersion() == 8 ? "a" : "b"), entityClass);
addEntityMethod.setAccessible(true);
addEntityMethod.invoke(worldServerClass.cast(nmsWorld), entity);
// It will also automatically load/unload it when far away
if (interactable) {
Method addEntityMethod = worldServerClass.getDeclaredMethod((Utils.getMajorVersion() == 8 ? "a" : "b"), entityClass);
addEntityMethod.setAccessible(true);
addEntityMethod.invoke(worldServerClass.cast(nmsWorld), entity);
}
uuid = (UUID) entityClass.getMethod("getUniqueID").invoke(entity);
entityId = (int) entityArmorStandClass.getMethod("getId").invoke(entity);

View File

@ -2,27 +2,49 @@ package de.epiceric.shopchest.nms;
import de.epiceric.shopchest.ShopChest;
import de.epiceric.shopchest.config.Config;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.Location;
import org.bukkit.entity.ArmorStand;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitRunnable;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public class Hologram {
private static List<Hologram> holograms = new ArrayList<>();
private static final List<Hologram> HOLOGRAMS = new ArrayList<>();
private final Set<UUID> visibility = Collections.newSetFromMap(new ConcurrentHashMap<UUID, Boolean>());
/**
* @param armorStand Armor stand that's part of a hologram
* @return Hologram, the armor stand is part of
*/
public static Hologram getHologram(ArmorStand armorStand) {
for (Hologram hologram : HOLOGRAMS) {
if (hologram.contains(armorStand)) {
return hologram;
}
}
return null;
}
/**
* @param armorStand Armor stand to check
* @return Whether the armor stand is part of a hologram
*/
public static boolean isPartOfHologram(ArmorStand armorStand) {
return getHologram(armorStand) != null;
}
// concurrent since update task is in async thread
// since this is a fake entity, hologram is hidden per default
private final Set<UUID> viewers = Collections.newSetFromMap(new ConcurrentHashMap<UUID, Boolean>());
private final List<ArmorStandWrapper> wrappers = new ArrayList<>();
private final Location location;
private final ShopChest plugin;
private final Config config;
private boolean exists = false;
private boolean exists;
private ArmorStandWrapper interactArmorStandWrapper;
public Hologram(ShopChest plugin, String[] lines, Location location) {
@ -30,98 +52,6 @@ public class Hologram {
this.config = plugin.getShopChestConfig();
this.location = location;
create(lines);
}
public void addLine(int line, String text) {
addLine(line, text, false);
}
private void addLine(int line, String text, boolean forceUpdateLine) {
if (text == null || text.isEmpty()) return;
if (line >= wrappers.size()) {
line = wrappers.size();
}
text = ChatColor.translateAlternateColorCodes('&', text);
if (config.hologram_fixed_bottom) {
for (int i = 0; i < line; i++) {
ArmorStandWrapper wrapper = wrappers.get(i);
wrapper.setLocation(wrapper.getLocation().add(0, 0.25, 0));
}
} else {
for (int i = line; i < wrappers.size(); i++) {
ArmorStandWrapper wrapper = wrappers.get(i);
wrapper.setLocation(wrapper.getLocation().subtract(0, 0.25, 0));
}
}
Location loc = getLocation();
if (!config.hologram_fixed_bottom) {
loc.subtract(0, line * 0.25, 0);
}
ArmorStandWrapper wrapper = new ArmorStandWrapper(plugin, loc, text);
wrappers.add(line, wrapper);
if (forceUpdateLine) {
for (UUID uuid : visibility) {
Player player = Bukkit.getPlayer(uuid);
if (player != null) {
wrapper.setVisible(player, true);
}
}
}
}
public void setLine(int line, String text) {
if (text == null ||text.isEmpty()) {
removeLine(line);
return;
}
text = ChatColor.translateAlternateColorCodes('&', text);
if (line >= wrappers.size()) {
addLine(line, text, true);
return;
}
wrappers.get(line).setCustomName(text);
}
public void removeLine(int line) {
if (line < wrappers.size()) {
if (config.hologram_fixed_bottom) {
for (int i = 0; i < line; i++) {
ArmorStandWrapper wrapper = wrappers.get(i);
wrapper.setLocation(wrapper.getLocation().subtract(0, 0.25, 0));
}
} else {
for (int i = line + 1; i < wrappers.size(); i++) {
ArmorStandWrapper wrapper = wrappers.get(i);
wrapper.setLocation(wrapper.getLocation().add(0, 0.25, 0));
}
}
wrappers.get(line).remove();
wrappers.remove(line);
}
}
public String[] getLines() {
List<String> lines = new ArrayList<>();
for (ArmorStandWrapper wrapper : wrappers) {
lines.add(wrapper.getCustomName());
}
return lines.toArray(new String[lines.size()]);
}
private void create(String[] lines) {
for (int i = 0; i < lines.length; i++) {
addLine(i, lines[i]);
}
@ -131,15 +61,11 @@ public class Hologram {
if (config.hologram_fixed_bottom) y = 0.85;
Location loc = getLocation().add(0, y, 0);
interactArmorStandWrapper = new ArmorStandWrapper(plugin, loc, null);
interactArmorStandWrapper = new ArmorStandWrapper(plugin, loc, null, true);
}
for (Player player : location.getWorld().getPlayers()) {
plugin.getShopUtils().updateShops(player, true);
}
holograms.add(this);
exists = true;
this.exists = true;
HOLOGRAMS.add(this);
}
/**
@ -149,58 +75,6 @@ public class Hologram {
return location.clone();
}
/**
* @param p Player to which the hologram should be shown
*/
public void showPlayer(final Player p) {
if (!isVisible(p)) {
new BukkitRunnable() {
@Override
public void run() {
for (ArmorStandWrapper wrapper : wrappers) {
wrapper.setVisible(p, true);
}
if (interactArmorStandWrapper != null) {
interactArmorStandWrapper.setVisible(p, true);
}
}
}.runTaskAsynchronously(plugin);
visibility.add(p.getUniqueId());
}
}
/**
* @param p Player from which the hologram should be hidden
*/
public void hidePlayer(final Player p) {
if (isVisible(p)) {
new BukkitRunnable() {
@Override
public void run() {
for (ArmorStandWrapper wrapper : wrappers) {
wrapper.setVisible(p, false);
}
if (interactArmorStandWrapper != null) {
interactArmorStandWrapper.setVisible(p, false);
}
}
}.runTaskAsynchronously(plugin);
visibility.remove(p.getUniqueId());
}
}
/**
* @param p Player to check
* @return Whether the hologram is visible to the player
*/
public boolean isVisible(Player p) {
return visibility.contains(p.getUniqueId());
}
/**
* @return Whether the hologram exists and is not dead
*/
@ -231,45 +105,189 @@ public class Hologram {
return interactArmorStandWrapper;
}
/**
* @param p Player to check
* @return Whether the hologram is visible to the player
*/
public boolean isVisible(Player p) {
return viewers.contains(p.getUniqueId());
}
/**
* @param p Player to which the hologram should be shown
*/
public void showPlayer(Player p) {
showPlayer(p, false);
}
/**
* @param p Player to which the hologram should be shown
* @param force whether to force or not
*/
public void showPlayer(Player p, boolean force) {
if (viewers.add(p.getUniqueId()) || force) {
togglePlayer(p, true);
}
}
/**
* @param p Player from which the hologram should be hidden
*/
public void hidePlayer(Player p) {
hidePlayer(p, false);
}
/**
* @param p Player from which the hologram should be hidden
* @param force whether to force or not
*/
public void hidePlayer(Player p, boolean force) {
if (viewers.remove(p.getUniqueId()) || force) {
togglePlayer(p, false);
}
}
/**
* Removes the hologram. <br>
* Hologram will be hidden from all players and will be killed
*/
public void remove() {
viewers.clear();
for (ArmorStandWrapper wrapper : wrappers) {
wrapper.remove();
}
wrappers.clear();
if (interactArmorStandWrapper != null) {
interactArmorStandWrapper.remove();
}
wrappers.clear();
interactArmorStandWrapper = null;
exists = false;
holograms.remove(this);
HOLOGRAMS.remove(this);
}
public void resetVisible(Player p) {
viewers.remove(p.getUniqueId());
}
private void togglePlayer(Player p, boolean visible) {
for (ArmorStandWrapper wrapper : wrappers) {
wrapper.setVisible(p, visible);
}
if (interactArmorStandWrapper != null) {
interactArmorStandWrapper.setVisible(p, visible);
}
}
/**
* @param armorStand Armor stand that's part of a hologram
* @return Hologram, the armor stand is part of
* Get all hologram lines
*
* @return Hologram lines
*/
public static Hologram getHologram(ArmorStand armorStand) {
for (Hologram hologram : holograms) {
if (hologram.contains(armorStand)) {
return hologram;
public String[] getLines() {
List<String> lines = new ArrayList<>();
for (ArmorStandWrapper wrapper : wrappers) {
lines.add(wrapper.getCustomName());
}
return lines.toArray(new String[lines.size()]);
}
/**
* Add a line
*
* @param line where to insert
* @param text text to display
*/
public void addLine(int line, String text) {
addLine(line, text, false);
}
private void addLine(int line, String text, boolean forceUpdateLine) {
if (text == null || text.isEmpty()) return;
if (line >= wrappers.size()) {
line = wrappers.size();
}
text = ChatColor.translateAlternateColorCodes('&', text);
if (config.hologram_fixed_bottom) {
for (int i = 0; i < line; i++) {
ArmorStandWrapper wrapper = wrappers.get(i);
wrapper.setLocation(wrapper.getLocation().add(0, 0.25, 0));
}
} else {
for (int i = line; i < wrappers.size(); i++) {
ArmorStandWrapper wrapper = wrappers.get(i);
wrapper.setLocation(wrapper.getLocation().subtract(0, 0.25, 0));
}
}
return null;
Location loc = getLocation();
if (!config.hologram_fixed_bottom) {
loc.subtract(0, line * 0.25, 0);
}
ArmorStandWrapper wrapper = new ArmorStandWrapper(plugin, loc, text, false);
wrappers.add(line, wrapper);
if (forceUpdateLine) {
for (Player player : location.getWorld().getPlayers()) {
if (viewers.contains(player.getUniqueId())) {
wrapper.setVisible(player, true);
}
}
}
}
/**
* @param armorStand Armor stand to check
* @return Whether the armor stand is part of a hologram
* Set a line
*
* @param line index to change
* @param text text to display
*/
public static boolean isPartOfHologram(ArmorStand armorStand) {
return getHologram(armorStand) != null;
public void setLine(int line, String text) {
if (text == null ||text.isEmpty()) {
removeLine(line);
return;
}
text = ChatColor.translateAlternateColorCodes('&', text);
if (line >= wrappers.size()) {
addLine(line, text, true);
return;
}
wrappers.get(line).setCustomName(text);
}
/**
* Remove a line
*
* @param line index to remove
*/
public void removeLine(int line) {
if (line < wrappers.size()) {
if (config.hologram_fixed_bottom) {
for (int i = 0; i < line; i++) {
ArmorStandWrapper wrapper = wrappers.get(i);
wrapper.setLocation(wrapper.getLocation().subtract(0, 0.25, 0));
}
} else {
for (int i = line + 1; i < wrappers.size(); i++) {
ArmorStandWrapper wrapper = wrappers.get(i);
wrapper.setLocation(wrapper.getLocation().add(0, 0.25, 0));
}
}
wrappers.get(line).remove();
wrappers.remove(line);
}
}
}

View File

@ -10,7 +10,6 @@ import de.epiceric.shopchest.language.LanguageUtils;
import de.epiceric.shopchest.nms.Hologram;
import de.epiceric.shopchest.utils.ItemUtils;
import de.epiceric.shopchest.utils.Utils;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.OfflinePlayer;
@ -19,29 +18,31 @@ import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.block.Chest;
import org.bukkit.block.DoubleChest;
import org.bukkit.entity.Player;
import org.bukkit.inventory.InventoryHolder;
import org.bukkit.inventory.ItemStack;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
public class Shop {
public enum ShopType {
NORMAL,
ADMIN,
}
private final ShopChest plugin;
private final OfflinePlayer vendor;
private final ItemStack product;
private final Location location;
private final double buyPrice;
private final double sellPrice;
private final ShopType shopType;
private final Config config;
private boolean created;
private int id;
private ShopChest plugin;
private OfflinePlayer vendor;
private ItemStack product;
private Location location;
private Hologram hologram;
private ShopItem item;
private double buyPrice;
private double sellPrice;
private ShopType shopType;
private Config config;
public Shop(int id, ShopChest plugin, OfflinePlayer vendor, ItemStack product, Location location, double buyPrice, double sellPrice, ShopType shopType) {
this.id = id;
@ -59,6 +60,34 @@ public class Shop {
this(-1, plugin, vendor, product, location, buyPrice, sellPrice, shopType);
}
/**
* Test if this shop is equals to another
*
* @param o Other object to test against
* @return true if we are sure they are the same, false otherwise
*/
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Shop shop = (Shop) o;
// id = -1 means temp shop
return id != -1 && id == shop.id;
}
@Override
public int hashCode() {
return id != -1 ? id : super.hashCode();
}
/**
* Create the shop
*
* @param showConsoleMessages to log exceptions to console
* @return Whether is was created or not
*/
public boolean create(boolean showConsoleMessages) {
if (created) return false;
@ -123,11 +152,7 @@ public class Shop {
itemStack = product.clone();
itemStack.setAmount(1);
this.item = new ShopItem(plugin, itemStack, itemLocation);
for (Player p : Bukkit.getOnlinePlayers()) {
item.setVisible(p, true);
}
item = new ShopItem(plugin, itemStack, itemLocation);
}
}
@ -163,6 +188,9 @@ public class Shop {
hologram = new Hologram(plugin, holoText, holoLocation);
}
/**
* Keep hologram text up to date
*/
public void updateHologramText() {
String[] lines = getHologramText();
String[] currentLines = hologram.getLines();
@ -181,7 +209,7 @@ public class Shop {
private String[] getHologramText() {
List<String> lines = new ArrayList<>();
Map<HologramFormat.Requirement, Object> requirements = new HashMap<>();
Map<HologramFormat.Requirement, Object> requirements = new EnumMap<>(HologramFormat.Requirement.class);
requirements.put(HologramFormat.Requirement.VENDOR, getVendor().getName());
requirements.put(HologramFormat.Requirement.AMOUNT, getProduct().getAmount());
requirements.put(HologramFormat.Requirement.ITEM_TYPE, getProduct().getType() + (getProduct().getDurability() > 0 ? ":" + getProduct().getDurability() : ""));
@ -200,7 +228,7 @@ public class Shop {
requirements.put(HologramFormat.Requirement.CHEST_SPACE, Utils.getFreeSpaceForItem(getInventoryHolder().getInventory(), getProduct()));
requirements.put(HologramFormat.Requirement.DURABILITY, getProduct().getDurability());
Map<Placeholder, Object> placeholders = new HashMap<>();
Map<Placeholder, Object> placeholders = new EnumMap<>(Placeholder.class);
placeholders.put(Placeholder.VENDOR, getVendor().getName());
placeholders.put(Placeholder.AMOUNT, getProduct().getAmount());
placeholders.put(Placeholder.ITEM_NAME, LanguageUtils.getItemName(getProduct()));
@ -220,7 +248,7 @@ public class Shop {
for (int i = 0; i < lineCount; i++) {
String format = plugin.getHologramFormat().getFormat(i, requirements, placeholders);
for (Placeholder placeholder : placeholders.keySet()) {
String replace = "";
String replace;
switch (placeholder) {
case BUY_PRICE:
@ -241,7 +269,7 @@ public class Shop {
}
}
return lines.toArray(new String[lines.size()]);
return lines.toArray(new String[0]);
}
private Location getHologramLocation(boolean doubleChest, Chest[] chests) {
@ -374,6 +402,14 @@ public class Shop {
return item;
}
public boolean hasHologram() {
return hologram != null;
}
public boolean hasItem() {
return item != null;
}
/**
* @return {@link InventoryHolder} of the shop or <b>null</b> if the shop has no chest.
*/
@ -388,9 +424,4 @@ public class Shop {
return null;
}
public enum ShopType {
NORMAL,
ADMIN
}
}

View File

@ -10,39 +10,40 @@ import org.bukkit.inventory.ItemStack;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.Collections;
import java.util.Set;
import java.util.UUID;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public class ShopItem {
private final ShopChest plugin;
private final Set<UUID> visibility = Collections.newSetFromMap(new ConcurrentHashMap<UUID, Boolean>());
// concurrent since update task is in async thread
// since this is a fake entity, item is hidden per default
private final Set<UUID> viewers = Collections.newSetFromMap(new ConcurrentHashMap<UUID, Boolean>());
private final ItemStack itemStack;
private final Location location;
private Object entityItem;
private int entityId;
private Object[] creationPackets = new Object[3];
private Class<?> nmsWorldClass = Utils.getNMSClass("World");
private Class<?> craftWorldClass = Utils.getCraftClass("CraftWorld");
private Class<?> nmsItemStackClass = Utils.getNMSClass("ItemStack");
private Class<?> craftItemStackClass = Utils.getCraftClass("inventory.CraftItemStack");
private Class<?> entityItemClass = Utils.getNMSClass("EntityItem");
private Class<?> entityClass = Utils.getNMSClass("Entity");
private Class<?> packetPlayOutSpawnEntityClass = Utils.getNMSClass("PacketPlayOutSpawnEntity");
private Class<?> packetPlayOutEntityMetadataClass = Utils.getNMSClass("PacketPlayOutEntityMetadata");
private Class<?> dataWatcherClass = Utils.getNMSClass("DataWatcher");
private Class<?> packetPlayOutEntityDestroyClass = Utils.getNMSClass("PacketPlayOutEntityDestroy");
private Class<?> packetPlayOutEntityVelocityClass = Utils.getNMSClass("PacketPlayOutEntityVelocity");
private final Class<?> packetPlayOutEntityDestroyClass = Utils.getNMSClass("PacketPlayOutEntityDestroy");
private final Object[] creationPackets = new Object[3];
private final Object entityItem;
private final int entityId;
public ShopItem(ShopChest plugin, ItemStack itemStack, Location location) {
this.plugin = plugin;
this.itemStack = itemStack;
this.location = location;
Class<?> packetPlayOutEntityVelocityClass = Utils.getNMSClass("PacketPlayOutEntityVelocity");
Class<?> dataWatcherClass = Utils.getNMSClass("DataWatcher");
Class<?> packetPlayOutEntityMetadataClass = Utils.getNMSClass("PacketPlayOutEntityMetadata");
Class<?> packetPlayOutSpawnEntityClass = Utils.getNMSClass("PacketPlayOutSpawnEntity");
Class<?> entityClass = Utils.getNMSClass("Entity");
Class<?> entityItemClass = Utils.getNMSClass("EntityItem");
Class<?> craftItemStackClass = Utils.getCraftClass("inventory.CraftItemStack");
Class<?> nmsItemStackClass = Utils.getNMSClass("ItemStack");
Class<?> craftWorldClass = Utils.getCraftClass("CraftWorld");
Class<?> nmsWorldClass = Utils.getNMSClass("World");
Class[] requiredClasses = new Class[] {
nmsWorldClass, craftWorldClass, nmsItemStackClass, craftItemStackClass, entityItemClass,
packetPlayOutSpawnEntityClass, packetPlayOutEntityMetadataClass, dataWatcherClass,
@ -52,14 +53,15 @@ public class ShopItem {
for (Class c : requiredClasses) {
if (c == null) {
plugin.debug("Failed to create shop item: Could not find all required classes");
entityItem = null;
entityId = -1;
return;
}
}
create();
}
Object tmpEntityItem = null;
int tmpEntityId = -1;
private void create() {
try {
Object craftWorld = craftWorldClass.cast(location.getWorld());
Object nmsWorld = craftWorldClass.getMethod("getHandle").invoke(craftWorld);
@ -67,74 +69,32 @@ public class ShopItem {
Object nmsItemStack = craftItemStackClass.getMethod("asNMSCopy", ItemStack.class).invoke(null, itemStack);
Constructor<?> entityItemConstructor = entityItemClass.getConstructor(nmsWorldClass);
entityItem = entityItemConstructor.newInstance(nmsWorld);
tmpEntityItem = entityItemConstructor.newInstance(nmsWorld);
entityItemClass.getMethod("setPosition", double.class, double.class, double.class).invoke(entityItem, location.getX(), location.getY(), location.getZ());
entityItemClass.getMethod("setItemStack", nmsItemStackClass).invoke(entityItem, nmsItemStack);
if (Utils.getMajorVersion() >= 10) entityItemClass.getMethod("setNoGravity", boolean.class).invoke(entityItem, true);
entityItemClass.getMethod("setPosition", double.class, double.class, double.class).invoke(tmpEntityItem, location.getX(), location.getY(), location.getZ());
entityItemClass.getMethod("setItemStack", nmsItemStackClass).invoke(tmpEntityItem, nmsItemStack);
if (Utils.getMajorVersion() >= 10) entityItemClass.getMethod("setNoGravity", boolean.class).invoke(tmpEntityItem, true);
Field ageField = entityItemClass.getDeclaredField("age");
ageField.setAccessible(true);
ageField.setInt(entityItem, -32768);
ageField.setInt(tmpEntityItem, -32768);
entityId = (int) entityItemClass.getMethod("getId").invoke(entityItem);
Object dataWatcher = entityItemClass.getMethod("getDataWatcher").invoke(entityItem);
tmpEntityId = (int) entityItemClass.getMethod("getId").invoke(tmpEntityItem);
Object dataWatcher = entityItemClass.getMethod("getDataWatcher").invoke(tmpEntityItem);
creationPackets[0] = packetPlayOutSpawnEntityClass.getConstructor(entityClass, int.class).newInstance(entityItem, 2);
creationPackets[1] = packetPlayOutEntityMetadataClass.getConstructor(int.class, dataWatcherClass, boolean.class).newInstance(entityId, dataWatcher, true);
creationPackets[2] = packetPlayOutEntityVelocityClass.getConstructor(int.class, double.class, double.class, double.class).newInstance(entityId, 0D, 0D, 0D);
creationPackets[0] = packetPlayOutSpawnEntityClass.getConstructor(entityClass, int.class).newInstance(tmpEntityItem, 2);
creationPackets[1] = packetPlayOutEntityMetadataClass.getConstructor(int.class, dataWatcherClass, boolean.class).newInstance(tmpEntityId, dataWatcher, true);
creationPackets[2] = packetPlayOutEntityVelocityClass.getConstructor(int.class, double.class, double.class, double.class).newInstance(tmpEntityId, 0D, 0D, 0D);
} catch (NoSuchMethodException | NoSuchFieldException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
plugin.getLogger().severe("Failed to create shop item");
plugin.debug("Failed to create shop item with reflection");
plugin.debug(e);
}
entityItem = tmpEntityItem;
entityId = tmpEntityId;
}
public void remove() {
for (UUID uuid : visibility) {
Player p = Bukkit.getPlayer(uuid);
if (p != null) setVisible(p, false);
}
}
/**
* Respawns the item at the set location for a player
* @param p Player, for which the item should be reset
*/
public void resetForPlayer(Player p) {
setVisible(p, false);
setVisible(p, true);
}
public boolean isVisible(Player p) {
return visibility.contains(p.getUniqueId());
}
public void setVisible(final Player p, boolean visible) {
if (isVisible(p) == visible)
return;
if (visible) {
for (Object packet : this.creationPackets) {
Utils.sendPacket(plugin, packet, p);
}
visibility.add(p.getUniqueId());
} else {
try {
if (p.isOnline()) {
Object packetPlayOutEntityDestroy = packetPlayOutEntityDestroyClass.getConstructor(int[].class).newInstance((Object) new int[]{entityId});
Utils.sendPacket(plugin, packetPlayOutEntityDestroy, p);
}
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | InstantiationException e) {
plugin.getLogger().severe("Failed to destroy shop item");
plugin.debug("Failed to destroy shop item with reflection");
plugin.debug(e);
}
visibility.remove(p.getUniqueId());
}
}
/**
* @return Clone of the location, where the shop item should be (it could have been moved by something, even though it shouldn't)
* To get the exact location, use reflection and extract the location of the {@code EntityItem}
@ -157,4 +117,83 @@ public class ShopItem {
public ItemStack getItemStack() {
return itemStack.clone();
}
/**
* @param p Player to check
* @return Whether the item is visible to the player
*/
public boolean isVisible(Player p) {
return viewers.contains(p.getUniqueId());
}
/**
* @param p Player to which the item should be shown
*/
public void showPlayer(Player p) {
showPlayer(p, false);
}
/**
* @param p Player to which the item should be shown
* @param force whether to force or not
*/
public void showPlayer(Player p, boolean force) {
if (viewers.add(p.getUniqueId()) || force) {
for (Object packet : creationPackets) {
Utils.sendPacket(plugin, packet, p);
}
}
}
/**
* @param p Player from which the item should be hidden
*/
public void hidePlayer(Player p) {
hidePlayer(p, false);
}
/**
* @param p Player from which the item should be hidden
* @param force whether to force or not
*/
public void hidePlayer(Player p, boolean force) {
if (viewers.remove(p.getUniqueId()) || force) {
try {
if (p.isOnline()) {
Object packetPlayOutEntityDestroy = packetPlayOutEntityDestroyClass.getConstructor(int[].class).newInstance((Object) new int[]{entityId});
Utils.sendPacket(plugin, packetPlayOutEntityDestroy, p);
}
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | InstantiationException e) {
plugin.getLogger().severe("Failed to destroy shop item");
plugin.debug("Failed to destroy shop item with reflection");
plugin.debug(e);
}
}
}
public void resetVisible(Player p) {
viewers.remove(p.getUniqueId());
}
/**
* Removes the item. <br>
* Item will be hidden from all players
*/
public void remove() {
// Avoid ConcurrentModificationException
for (UUID uuid : new ArrayList<>(viewers)) {
Player p = Bukkit.getPlayer(uuid);
if (p != null) hidePlayer(p);
}
}
/**
* Respawns the item at the set location for a player
* @param p Player, for which the item should be reset
*/
public void resetForPlayer(Player p) {
hidePlayer(p);
showPlayer(p);
}
}

View File

@ -0,0 +1,42 @@
package de.epiceric.shopchest.utils;
public class FastMath {
/**
* Fast sqrt, 1.57% precision
*
* @param n value to calculate square root from
* @return the square root of n
*/
public static double sqrt(double n) {
return n * Double.longBitsToDouble(6910470738111508698L - (Double.doubleToRawLongBits(n) >> 1));
}
/**
* Fast acos, 2.9% precision
*
* @param n value to calculate arc cosine from
* @return the arc cosine of n
*/
public static double acos(double n) {
int v = (int) (n * MULTIPLIER + OFFSET);
while (v > PRECISION) v -= PRECISION;
while (v < 0) v += PRECISION;
return acos[v];
}
// Below is lookup table generation
// It is only executed once at initialization
private static final int PRECISION = 512;
private static final double MULTIPLIER = PRECISION / 2D;
private static final double OFFSET = MULTIPLIER + 0.5D; // + 0.5 as cast truncate and don't round
private static final double[] acos = new double[PRECISION + 1];
static {
for (int i = 0; i <= PRECISION; i++) {
acos[i] = Math.acos(i * (2D / PRECISION) - 1);
}
}
}

View File

@ -0,0 +1,75 @@
package de.epiceric.shopchest.utils;
public enum Operator {
EQUAL("==") {
@Override
public boolean compare(double a, double b) {
return Double.compare(a, b) == 0;
}
@Override
public boolean compare(String a, String b) {
return a.equals(b);
}
},
NOT_EQUAL("!=") {
@Override
public boolean compare(double a, double b) {
return Double.compare(a, b) != 0;
}
@Override
public boolean compare(String a, String b) {
return !a.equals(b);
}
},
GREATER_THAN(">") {
@Override
public boolean compare(double a, double b) {
return a > b;
}
},
GREATER_THAN_OR_EQUAL(">=") {
@Override
public boolean compare(double a, double b) {
return a >= b;
}
},
LESS_THAN("<") {
@Override
public boolean compare(double a, double b) {
return a < b;
}
},
LESS_THAN_OR_EQUAL("<=") {
@Override
public boolean compare(double a, double b) {
return a <= b;
}
};
private final String symbol;
Operator(String symbol) {
this.symbol = symbol;
}
public static Operator from(String symbol) {
for (Operator operator : values()) {
if (operator.symbol.equals(symbol)) {
return operator;
}
}
throw new IllegalArgumentException();
}
public abstract boolean compare(double a, double b);
public boolean compare(String a, String b) {
throw new UnsupportedOperationException();
}
}

View File

@ -13,18 +13,15 @@ import org.bukkit.inventory.InventoryHolder;
import org.bukkit.permissions.PermissionAttachmentInfo;
import org.bukkit.util.Vector;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public class ShopUtils {
private final HashMap<Location, Shop> shopLocation = new HashMap<>();
private final HashMap<UUID, Location> playerLocation = new HashMap<>();
// concurrent since it is updated in async task
private final Map<UUID, Location> playerLocation = new ConcurrentHashMap<>();
private final Map<Location, Shop> shopLocation = new HashMap<>();
private final Collection<Shop> shopLocationValues = Collections.unmodifiableCollection(shopLocation.values());
private final ShopChest plugin;
public ShopUtils(ShopChest plugin) {
@ -55,10 +52,24 @@ public class ShopUtils {
/**
* Get all shops
* Do not use for removing while iteration!
*
* @see #getShopsCopy()
* @return Read-only collection of all shops, may contain duplicates
*/
public Collection<Shop> getShops() {
return Collections.unmodifiableCollection(new ArrayList<>(shopLocation.values()));
return shopLocationValues;
}
/**
* Get all shops
* Same as {@link #getShops()} but this is safe to remove while iterating
*
* @see #getShops()
* @return Copy of collection of all shops, may contain duplicates
*/
public Collection<Shop> getShopsCopy() {
return new ArrayList<>(getShops());
}
/**
@ -226,7 +237,7 @@ public class ShopUtils {
plugin.getShopDatabase().connect(new Callback<Integer>(plugin) {
@Override
public void onResult(Integer result) {
for (Shop shop : getShops()) {
for (Shop shop : getShopsCopy()) {
removeShop(shop, false);
plugin.debug("Removed shop (#" + shop.getID() + ")");
}
@ -278,36 +289,7 @@ public class ShopUtils {
}
if (plugin.getShopChestConfig().only_show_shops_in_sight) {
Set<Shop> sight = getShopsInSight(player);
Set<Shop> _sight = new HashSet<>();
for (Shop shop : sight) {
_sight.add(shop);
if (shop.getHologram() != null && !shop.getHologram().isVisible(player)) {
shop.getHologram().showPlayer(player);
}
if (plugin.getShopChestConfig().only_show_first_shop_in_sight) break;
}
double itemDistSqr = Math.pow(plugin.getShopChestConfig().maximal_item_distance, 2);
for (Shop shop : getShops()) {
if (shop.getItem() != null && shop.getLocation().getWorld().getName().equals(player.getWorld().getName())) {
if (shop.getLocation().distanceSquared(player.getEyeLocation()) <= itemDistSqr) {
shop.getItem().setVisible(player, true);
} else {
shop.getItem().setVisible(player, false);
}
}
if (!_sight.contains(shop)) {
if (shop.getHologram() != null) {
shop.getHologram().hidePlayer(player);
}
}
}
updateVisibleShops(player);
} else {
updateNearestShops(player);
}
@ -315,55 +297,109 @@ public class ShopUtils {
playerLocation.put(player.getUniqueId(), player.getLocation());
}
private Set<Shop> getShopsInSight(Player player) {
double dist = plugin.getShopChestConfig().maximal_distance;
Location loc = player.getEyeLocation();
Vector direction = loc.getDirection();
Set<Shop> shops = new HashSet<>();
double i = 0;
do {
Location below = loc.clone().subtract(0, 1, 0);
Shop shop = getShop(loc);
if (shop != null) {
shops.add(shop);
} else if ((shop = getShop(below)) != null) {
shops.add(shop);
}
loc.add(direction);
i++;
} while (i <= dist - (dist%1));
direction.multiply(dist - (dist%1));
loc.add(direction);
Location below = loc.clone().subtract(0, 1, 0);
Shop shop = getShop(loc);
if (shop != null) {
shops.add(shop);
} else if ((shop = getShop(below)) != null) {
shops.add(shop);
}
return shops;
/**
* Remove a player from the {@code playerLocation} map.
* This should only be called when really needed
*/
public void resetPlayerLocation(Player player) {
playerLocation.remove(player.getUniqueId());
}
/**
* Update hologram and item of the shop for a player based on their distance to each other
* @param shop Shop to update
* @param player Player to show the update
*/
public void updateShop(Shop shop, Player player) {
double holoDistSqr = Math.pow(plugin.getShopChestConfig().maximal_distance, 2);
double itemDistSqr = Math.pow(plugin.getShopChestConfig().maximal_item_distance, 2);
private static final double TARGET_THRESHOLD = 1;
updateShop(shop, player, holoDistSqr, itemDistSqr);
private void updateVisibleShops(Player player) {
double itemDistSquared = Math.pow(plugin.getShopChestConfig().maximal_item_distance, 2);
double hologramDistSquared = Math.pow(plugin.getShopChestConfig().maximal_distance, 2);
boolean firstShopInSight = plugin.getShopChestConfig().only_show_first_shop_in_sight;
// used if only_show_first_shop_in_sight
List<Shop> otherShopsInSight = firstShopInSight ? new ArrayList<Shop>() : null;
double nearestDistance = 0;
Shop nearestShop = null;
Location pLoc = player.getEyeLocation();
double pX = pLoc.getX();
double pY = pLoc.getY();
double pZ = pLoc.getZ();
Vector pDir = pLoc.getDirection();
double dirLength = pDir.length();
for (Shop shop : getShops()) {
Location shopLocation = shop.getLocation();
if (shopLocation.getWorld().getName().equals(player.getWorld().getName())) {
double distanceSquared = shop.getLocation().distanceSquared(player.getLocation());
// Display item based on distance
if (shop.hasItem()) {
if (distanceSquared <= itemDistSquared) {
shop.getItem().showPlayer(player);
} else {
shop.getItem().hidePlayer(player);
}
}
// Display hologram based on sight
if (shop.hasHologram()) {
if (distanceSquared < hologramDistSquared) {
Location holoLocation = shop.getHologram().getLocation();
double x = holoLocation.getX() - pX;
double y = shopLocation.getY() - pY + 1.15; // chest block + item offset
double z = holoLocation.getZ() - pZ;
// See: org.bukkit.util.Vector#angle(Vector)
double angle = FastMath.acos(
(x * pDir.getX() + y * pDir.getY() + z * pDir.getZ())
/ (FastMath.sqrt(x * x + y * y + z * z) * dirLength)
);
double distance = FastMath.sqrt(distanceSquared);
// Check if is targeted
if (angle * distance < TARGET_THRESHOLD) {
// Display even if not the nearest
if (!firstShopInSight) {
shop.getHologram().showPlayer(player);
continue;
}
if (nearestShop == null) {
// nearestShop is null
// => we guess this one will be the nearest
nearestShop = shop;
nearestDistance = distance;
continue;
} else if (distance < nearestDistance) {
// nearestShop is NOT null && this shop is nearest
// => we'll hide nearestShop, and guess this one will be the nearest
otherShopsInSight.add(nearestShop);
nearestShop = shop;
nearestDistance = distance;
continue;
}
// else: hologram is farther than nearest, so we hide it
}
}
// If not in sight
shop.getHologram().hidePlayer(player);
}
}
}
if (firstShopInSight) {
// we hide other shop as we wan't to display only the first
for (Shop shop : otherShopsInSight) {
// we already checked hasHologram() before adding it
shop.getHologram().hidePlayer(player);
}
}
if (nearestShop != null && nearestShop.hasHologram()) {
nearestShop.getHologram().showPlayer(player);
}
}
private void updateNearestShops(Player p) {
@ -371,27 +407,23 @@ public class ShopUtils {
double itemDistSqr = Math.pow(plugin.getShopChestConfig().maximal_item_distance, 2);
for (Shop shop : getShops()) {
updateShop(shop, p, holoDistSqr, itemDistSqr);
}
}
if (p.getLocation().getWorld().getName().equals(shop.getLocation().getWorld().getName())) {
double distSqr = shop.getLocation().distanceSquared(p.getLocation());
private void updateShop(Shop shop, Player player, double holoDistSqr, double itemDistSqr) {
if (player.getLocation().getWorld().getName().equals(shop.getLocation().getWorld().getName())) {
double distSqr = shop.getLocation().distanceSquared(player.getLocation());
if (shop.getHologram() != null) {
if (distSqr <= holoDistSqr) {
shop.getHologram().showPlayer(player);
} else {
shop.getHologram().hidePlayer(player);
if (shop.hasHologram()) {
if (distSqr <= holoDistSqr) {
shop.getHologram().showPlayer(p);
} else {
shop.getHologram().hidePlayer(p);
}
}
}
if (shop.getItem() != null) {
if (distSqr <= itemDistSqr) {
shop.getItem().setVisible(player, true);
} else {
shop.getItem().setVisible(player, false);
if (shop.hasItem()) {
if (distSqr <= itemDistSqr) {
shop.getItem().showPlayer(p);
} else {
shop.getItem().hidePlayer(p);
}
}
}
}