diff --git a/src/main/java/world/bentobox/bentobox/Settings.java b/src/main/java/world/bentobox/bentobox/Settings.java index 360fd7a80..4f1f12ccd 100644 --- a/src/main/java/world/bentobox/bentobox/Settings.java +++ b/src/main/java/world/bentobox/bentobox/Settings.java @@ -138,12 +138,28 @@ public class Settings implements ConfigObject { @ConfigEntry(path = "panel.filler-material", since = "1.14.0") private Material panelFillerMaterial = Material.LIGHT_BLUE_STAINED_GLASS_PANE; + @ConfigComment("Toggle whether player head texture should be gathered from Mojang API or mc-heads.net cache server.") + @ConfigComment("Mojang API sometime may be slow and may limit requests to the player data, so this will allow to") + @ConfigComment("get player heads a bit faster then Mojang API.") + @ConfigEntry(path = "panel.use-cache-server", since = "1.16.0") + private boolean useCacheServer = false; + @ConfigComment("Defines how long player skin texture link is stored into local cache before it is requested again.") @ConfigComment("Defined value is in the minutes.") @ConfigComment("Value 0 will not clear cache until server restart.") @ConfigEntry(path = "panel.head-cache-time", since = "1.14.1") private long playerHeadCacheTime = 60; + @ConfigComment("Defines a number of player heads requested per tasks.") + @ConfigComment("Setting it too large may lead to temporarily being blocked from head gatherer API.") + @ConfigEntry(path = "panel.heads-per-call", since = "1.16.0") + private int headsPerCall = 9; + + @ConfigComment("Defines a number of ticks between each player head request task.") + @ConfigComment("Setting it too large may lead to temporarily being blocked from head gatherer API.") + @ConfigEntry(path = "panel.ticks-between-calls", since = "1.16.0", needsRestart = true) + private long ticksBetweenCalls = 10; + /* * Logs */ @@ -783,4 +799,76 @@ public class Settings implements ConfigObject { { this.playerHeadCacheTime = playerHeadCacheTime; } + + + /** + * Is use cache server boolean. + * + * @return the boolean + * @since 1.16.0 + */ + public boolean isUseCacheServer() + { + return useCacheServer; + } + + + /** + * Sets use cache server. + * + * @param useCacheServer the use cache server + * @since 1.16.0 + */ + public void setUseCacheServer(boolean useCacheServer) + { + this.useCacheServer = useCacheServer; + } + + + /** + * Gets heads per call. + * + * @return the heads per call + * @since 1.16.0 + */ + public int getHeadsPerCall() + { + return headsPerCall; + } + + + /** + * Sets heads per call. + * + * @param headsPerCall the heads per call + * @since 1.16.0 + */ + public void setHeadsPerCall(int headsPerCall) + { + this.headsPerCall = headsPerCall; + } + + + /** + * Gets ticks between calls. + * + * @return the ticks between calls + * @since 1.16.0 + */ + public long getTicksBetweenCalls() + { + return ticksBetweenCalls; + } + + + /** + * Sets ticks between calls. + * + * @param ticksBetweenCalls the ticks between calls + * @since 1.16.0 + */ + public void setTicksBetweenCalls(long ticksBetweenCalls) + { + this.ticksBetweenCalls = ticksBetweenCalls; + } } diff --git a/src/main/java/world/bentobox/bentobox/api/commands/admin/AdminSettingsCommand.java b/src/main/java/world/bentobox/bentobox/api/commands/admin/AdminSettingsCommand.java index 8c8f2dad8..d393b8357 100644 --- a/src/main/java/world/bentobox/bentobox/api/commands/admin/AdminSettingsCommand.java +++ b/src/main/java/world/bentobox/bentobox/api/commands/admin/AdminSettingsCommand.java @@ -78,9 +78,9 @@ public class AdminSettingsCommand extends CompositeCommand { // Player settings new TabbedPanelBuilder() .user(user) - .world(getWorld()) - .tab(1, new SettingsTab(getWorld(), user, island, Flag.Type.PROTECTION)) - .tab(2, new SettingsTab(getWorld(), user, island, Flag.Type.SETTING)) + .world(island.getWorld()) + .tab(1, new SettingsTab(user, island, Flag.Type.PROTECTION)) + .tab(2, new SettingsTab(user, island, Flag.Type.SETTING)) .startingSlot(1) .size(54) .build().openPanel(); diff --git a/src/main/java/world/bentobox/bentobox/api/commands/island/IslandSettingsCommand.java b/src/main/java/world/bentobox/bentobox/api/commands/island/IslandSettingsCommand.java index 83648998f..4e309ecfc 100644 --- a/src/main/java/world/bentobox/bentobox/api/commands/island/IslandSettingsCommand.java +++ b/src/main/java/world/bentobox/bentobox/api/commands/island/IslandSettingsCommand.java @@ -15,6 +15,8 @@ import world.bentobox.bentobox.util.Util; */ public class IslandSettingsCommand extends CompositeCommand { + private Island island; + public IslandSettingsCommand(CompositeCommand islandCommand) { super(islandCommand, "settings", "flags", "options"); } @@ -28,23 +30,26 @@ public class IslandSettingsCommand extends CompositeCommand { @Override public boolean canExecute(User user, String label, List args) { - // Settings are only shown if you are in the right world if (Util.getWorld(user.getWorld()).equals(getWorld())) { - return true; + // Player is in same world + island = getIslands().getIslandAt(user.getLocation()).orElseGet(() -> getIslands().getIsland(user.getWorld(), user.getUniqueId())); } else { - user.sendMessage("general.errors.wrong-world"); + island = getIslands().getIsland(getWorld(), user); + } + if (island == null) { + user.sendMessage("general.errors.no-island"); return false; } + return true; } @Override public boolean execute(User user, String label, List args) { - Island island = getIslands().getIslandAt(user.getLocation()).orElseGet(() -> getIslands().getIsland(user.getWorld(), user.getUniqueId())); new TabbedPanelBuilder() .user(user) - .world(getWorld()) - .tab(1, new SettingsTab(getWorld(), user, island, Flag.Type.PROTECTION)) - .tab(2, new SettingsTab(getWorld(), user, island, Flag.Type.SETTING)) + .world(island.getWorld()) + .tab(1, new SettingsTab(user, island, Flag.Type.PROTECTION)) + .tab(2, new SettingsTab(user, island, Flag.Type.SETTING)) .startingSlot(1) .size(54) .hideIfEmpty() diff --git a/src/main/java/world/bentobox/bentobox/api/panels/Panel.java b/src/main/java/world/bentobox/bentobox/api/panels/Panel.java index 8a9b2f92d..39d330465 100644 --- a/src/main/java/world/bentobox/bentobox/api/panels/Panel.java +++ b/src/main/java/world/bentobox/bentobox/api/panels/Panel.java @@ -4,12 +4,14 @@ import java.util.Map; import java.util.Optional; import org.bukkit.Bukkit; +import org.bukkit.World; import org.bukkit.entity.Player; import org.bukkit.event.inventory.InventoryType; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.InventoryHolder; import org.eclipse.jdt.annotation.NonNull; +import world.bentobox.bentobox.api.panels.builders.PanelBuilder; import world.bentobox.bentobox.api.user.User; import world.bentobox.bentobox.listeners.PanelListenerManager; import world.bentobox.bentobox.util.heads.HeadGetter; @@ -27,6 +29,7 @@ public class Panel implements HeadRequester, InventoryHolder { private PanelListener listener; private User user; private String name; + private World world; /** * Various types of Panel that can be created. @@ -51,6 +54,17 @@ public class Panel implements HeadRequester, InventoryHolder { makePanel(name, items, size, user, listener, type); } + /** + * @param pb - PanelBuilder + * @since 1.16.0 + */ + public Panel(PanelBuilder pb) { + this.world = pb.getWorld(); + this.makePanel(pb.getName(), pb.getItems(), + Math.max(pb.getSize(), pb.getItems().isEmpty() ? pb.getSize() : pb.getItems().lastKey() + 1), + pb.getUser(), pb.getListener(), pb.getPanelType()); + } + protected void makePanel(String name, Map items, int size, User user, PanelListener listener) { this.makePanel(name, items, size, user, listener, Type.INVENTORY); @@ -200,4 +214,23 @@ public class Panel implements HeadRequester, InventoryHolder { public String getName() { return name; } + + /** + * Get the world that applies to this panel + * @return the optional world + * @since 1.16.0 + */ + public Optional getWorld() { + return Optional.ofNullable(world); + } + + /** + * @param world the world to set + * @since 1.16.0 + */ + public void setWorld(World world) { + this.world = world; + } + + } diff --git a/src/main/java/world/bentobox/bentobox/api/panels/TabbedPanel.java b/src/main/java/world/bentobox/bentobox/api/panels/TabbedPanel.java index d2fa21eb9..d9527a0f7 100644 --- a/src/main/java/world/bentobox/bentobox/api/panels/TabbedPanel.java +++ b/src/main/java/world/bentobox/bentobox/api/panels/TabbedPanel.java @@ -42,6 +42,7 @@ public class TabbedPanel extends Panel implements PanelListener { */ public TabbedPanel(TabbedPanelBuilder tpb) { this.tpb = tpb; + this.setWorld(tpb.getWorld()); } /* (non-Javadoc) diff --git a/src/main/java/world/bentobox/bentobox/api/panels/builders/PanelBuilder.java b/src/main/java/world/bentobox/bentobox/api/panels/builders/PanelBuilder.java index a68b6d665..5b89ec22e 100644 --- a/src/main/java/world/bentobox/bentobox/api/panels/builders/PanelBuilder.java +++ b/src/main/java/world/bentobox/bentobox/api/panels/builders/PanelBuilder.java @@ -4,6 +4,7 @@ import java.util.SortedMap; import java.util.TreeMap; import org.bukkit.ChatColor; +import org.bukkit.World; import world.bentobox.bentobox.api.panels.Panel; import world.bentobox.bentobox.api.panels.PanelItem; @@ -22,6 +23,7 @@ public class PanelBuilder { private User user; private PanelListener listener; private Panel.Type type = Panel.Type.INVENTORY; + private World world; public PanelBuilder name(String name) { this.name = ChatColor.translateAlternateColorCodes('&', name); @@ -115,13 +117,22 @@ public class PanelBuilder { return items.containsKey(slot); } + /** + * Set the game world that applies this panel + * @param world + * @return PanelBuilder + */ + public PanelBuilder world(World world) { + this.world = world; + return this; + } + /** * Build the panel * @return Panel */ public Panel build() { - // items.lastKey() is a slot position, so the panel size is this value + 1 - return new Panel(name, items, Math.max(size, items.isEmpty() ? size : items.lastKey() + 1), user, listener, type); + return new Panel(this); } /** @@ -167,4 +178,14 @@ public class PanelBuilder { public Panel.Type getPanelType() { return type; } + + /** + * @return the world + * @since 1.16.0 + */ + public World getWorld() { + return world; + } + + } diff --git a/src/main/java/world/bentobox/bentobox/listeners/flags/clicklisteners/CommandCycleClick.java b/src/main/java/world/bentobox/bentobox/listeners/flags/clicklisteners/CommandCycleClick.java index 2e8d5a602..4d730fe7d 100644 --- a/src/main/java/world/bentobox/bentobox/listeners/flags/clicklisteners/CommandCycleClick.java +++ b/src/main/java/world/bentobox/bentobox/listeners/flags/clicklisteners/CommandCycleClick.java @@ -1,6 +1,7 @@ package world.bentobox.bentobox.listeners.flags.clicklisteners; import org.bukkit.Sound; +import org.bukkit.World; import org.bukkit.event.inventory.ClickType; import world.bentobox.bentobox.BentoBox; @@ -32,8 +33,9 @@ public class CommandCycleClick implements ClickHandler { public boolean onClick(Panel panel, User user, ClickType click, int slot) { // Left clicking increases the rank required // Right clicking decreases the rank required - // Get the user's island - Island island = plugin.getIslands().getIsland(user.getWorld(), user.getUniqueId()); + // Get the user's island for the game world + World world = panel.getWorld().orElse(user.getWorld()); + Island island = plugin.getIslands().getIsland(world, user.getUniqueId()); if (island != null && island.getOwner().equals(user.getUniqueId())) { RanksManager rm = plugin.getRanksManager(); int currentRank = island.getRankCommand(command); @@ -53,7 +55,7 @@ public class CommandCycleClick implements ClickHandler { user.getPlayer().playSound(user.getLocation(), Sound.BLOCK_STONE_BUTTON_CLICK_ON, 1F, 1F); } // Apply change to panel - panel.getInventory().setItem(slot, commandRankClickListener.getPanelItem(command, user).getItem()); + panel.getInventory().setItem(slot, commandRankClickListener.getPanelItem(command, user, world).getItem()); // Save island plugin.getIslands().save(island); diff --git a/src/main/java/world/bentobox/bentobox/listeners/flags/clicklisteners/CommandRankClickListener.java b/src/main/java/world/bentobox/bentobox/listeners/flags/clicklisteners/CommandRankClickListener.java index 7b8e1eb08..e98abc574 100644 --- a/src/main/java/world/bentobox/bentobox/listeners/flags/clicklisteners/CommandRankClickListener.java +++ b/src/main/java/world/bentobox/bentobox/listeners/flags/clicklisteners/CommandRankClickListener.java @@ -43,7 +43,7 @@ public class CommandRankClickListener implements ClickHandler { } // Check if has permission - String prefix = plugin.getIWM().getPermissionPrefix(Util.getWorld(user.getWorld())); + String prefix = plugin.getIWM().getPermissionPrefix(Util.getWorld(panel.getWorld().orElse(user.getWorld()))); String reqPerm = prefix + "settings." + Flags.COMMAND_RANKS.getID(); String allPerms = prefix + "settings.*"; if (!user.hasPermission(reqPerm) && !user.hasPermission(allPerms) @@ -54,7 +54,7 @@ public class CommandRankClickListener implements ClickHandler { } // Get the user's island - Island island = plugin.getIslands().getIsland(user.getWorld(), user.getUniqueId()); + Island island = plugin.getIslands().getIsland(panel.getWorld().orElse(user.getWorld()), user.getUniqueId()); if (island == null || !island.getOwner().equals(user.getUniqueId())) { user.sendMessage("general.errors.not-owner"); user.getPlayer().playSound(user.getLocation(), Sound.BLOCK_METAL_HIT, 1F, 1F); @@ -65,24 +65,24 @@ public class CommandRankClickListener implements ClickHandler { if (panel.getName().equals(panelName)) { // This is a click on the panel // Slot relates to the command - String c = getCommands(user.getWorld()).get(slot); + String c = getCommands(panel.getWorld().orElse(user.getWorld())).get(slot); // Apply change to panel - panel.getInventory().setItem(slot, getPanelItem(c, user).getItem()); + panel.getInventory().setItem(slot, getPanelItem(c, user, panel.getWorld().orElse(user.getWorld())).getItem()); } else { // Open the Sub Settings panel - openPanel(user, panelName); + openPanel(user, panelName, panel.getWorld().orElse(user.getWorld())); } return true; } - private void openPanel(User user, String panelName) { + private void openPanel(User user, String panelName, World world) { // Close the current panel user.closeInventory(); // Open a new panel PanelBuilder pb = new PanelBuilder(); - pb.user(user).name(panelName); + pb.user(user).name(panelName).world(world); // Make panel items - getCommands(user.getWorld()).forEach(c -> pb.item(getPanelItem(c, user))); + getCommands(world).forEach(c -> pb.item(getPanelItem(c, user, world))); pb.build(); } @@ -91,10 +91,11 @@ public class CommandRankClickListener implements ClickHandler { * Gets the rank command panel item * @param c - rank string * @param user - user + * @param world - world for this panel * @return panel item for this command */ - public PanelItem getPanelItem(String c, User user) { - Island island = plugin.getIslands().getIsland(user.getWorld(), user); + public PanelItem getPanelItem(String c, User user, World world) { + Island island = plugin.getIslands().getIsland(world, user); PanelItemBuilder pib = new PanelItemBuilder(); pib.name(c); pib.clickHandler(new CommandCycleClick(this, c)); diff --git a/src/main/java/world/bentobox/bentobox/listeners/flags/clicklisteners/GeoLimitClickListener.java b/src/main/java/world/bentobox/bentobox/listeners/flags/clicklisteners/GeoLimitClickListener.java index 1d1d286b0..98e1495a7 100644 --- a/src/main/java/world/bentobox/bentobox/listeners/flags/clicklisteners/GeoLimitClickListener.java +++ b/src/main/java/world/bentobox/bentobox/listeners/flags/clicklisteners/GeoLimitClickListener.java @@ -1,6 +1,7 @@ package world.bentobox.bentobox.listeners.flags.clicklisteners; import org.bukkit.Sound; +import org.bukkit.World; import org.bukkit.event.inventory.ClickType; import world.bentobox.bentobox.BentoBox; @@ -26,8 +27,9 @@ public class GeoLimitClickListener implements ClickHandler { user.sendMessage("general.errors.wrong-world"); return true; } + World world = panel.getWorld().orElse(user.getWorld()); IslandWorldManager iwm = BentoBox.getInstance().getIWM(); - String reqPerm = iwm.getPermissionPrefix(Util.getWorld(user.getWorld())) + "admin.settings.GEO_LIMIT_MOBS"; + String reqPerm = iwm.getPermissionPrefix(Util.getWorld(world)) + "admin.settings.GEO_LIMIT_MOBS"; if (!user.hasPermission(reqPerm)) { user.sendMessage("general.errors.no-permission", "[permission]", reqPerm); user.getPlayer().playSound(user.getLocation(), Sound.BLOCK_METAL_HIT, 1F, 1F); @@ -35,19 +37,19 @@ public class GeoLimitClickListener implements ClickHandler { } // Open the Sub Settings panel - openPanel(user); + openPanel(user, world); return true; } - private void openPanel(User user) { + private void openPanel(User user, World world) { // Close the current panel user.closeInventory(); // Open a new panel new TabbedPanelBuilder() .user(user) - .world(user.getWorld()) - .tab(1, new GeoMobLimitTab(user, EntityLimitTabType.GEO_LIMIT)) + .world(world) + .tab(1, new GeoMobLimitTab(user, EntityLimitTabType.GEO_LIMIT, world)) .startingSlot(1) .size(54) .build().openPanel(); diff --git a/src/main/java/world/bentobox/bentobox/listeners/flags/clicklisteners/GeoMobLimitTab.java b/src/main/java/world/bentobox/bentobox/listeners/flags/clicklisteners/GeoMobLimitTab.java index 3f685e5fa..0cb242f8c 100644 --- a/src/main/java/world/bentobox/bentobox/listeners/flags/clicklisteners/GeoMobLimitTab.java +++ b/src/main/java/world/bentobox/bentobox/listeners/flags/clicklisteners/GeoMobLimitTab.java @@ -7,11 +7,13 @@ import java.util.List; import java.util.stream.Collectors; import org.bukkit.Material; +import org.bukkit.World; import org.bukkit.entity.EntityType; import org.bukkit.event.inventory.ClickType; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; + import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.api.addons.GameModeAddon; import world.bentobox.bentobox.api.panels.Panel; @@ -47,43 +49,45 @@ public class GeoMobLimitTab implements Tab, ClickHandler { private final BentoBox plugin = BentoBox.getInstance(); private final User user; private final EntityLimitTabType type; + private final World world; /** * @param user - user viewing the tab * @param type - type of tab to show - Geo limit or Mob limit + * @param world - world where this tab is being used */ - public GeoMobLimitTab(@NonNull User user, @NonNull EntityLimitTabType type) { + public GeoMobLimitTab(@NonNull User user, @NonNull EntityLimitTabType type, World world) { super(); this.user = user; this.type = type; + this.world = world; } @Override public boolean onClick(Panel panel, User user, ClickType clickType, int slot) { - // This is a click on the mob limit panel // Case panel to Tabbed Panel to get the active page TabbedPanel tp = (TabbedPanel)panel; // Convert the slot and active page to an index int index = tp.getActivePage() * 36 + slot - 9; EntityType c = LIVING_ENTITY_TYPES.get(index); if (type == EntityLimitTabType.MOB_LIMIT) { - if (plugin.getIWM().getMobLimitSettings(user.getWorld()).contains(c.name())) { - plugin.getIWM().getMobLimitSettings(user.getWorld()).remove(c.name()); + if (plugin.getIWM().getMobLimitSettings(world).contains(c.name())) { + plugin.getIWM().getMobLimitSettings(world).remove(c.name()); } else { - plugin.getIWM().getMobLimitSettings(user.getWorld()).add(c.name()); + plugin.getIWM().getMobLimitSettings(world).add(c.name()); } } else { - if (plugin.getIWM().getGeoLimitSettings(user.getWorld()).contains(c.name())) { - plugin.getIWM().getGeoLimitSettings(user.getWorld()).remove(c.name()); + if (plugin.getIWM().getGeoLimitSettings(world).contains(c.name())) { + plugin.getIWM().getGeoLimitSettings(world).remove(c.name()); } else { - plugin.getIWM().getGeoLimitSettings(user.getWorld()).add(c.name()); + plugin.getIWM().getGeoLimitSettings(world).add(c.name()); } } // Apply change to panel panel.getInventory().setItem(slot, getPanelItem(c, user).getItem()); // Save settings - plugin.getIWM().getAddon(Util.getWorld(user.getWorld())).ifPresent(GameModeAddon::saveWorldSettings); + plugin.getIWM().getAddon(Util.getWorld(world)).ifPresent(GameModeAddon::saveWorldSettings); return true; } @@ -119,7 +123,7 @@ public class GeoMobLimitTab implements Tab, ClickHandler { pib.name(Util.prettifyText(c.toString())); pib.clickHandler(this); if (type == EntityLimitTabType.MOB_LIMIT) { - if (!BentoBox.getInstance().getIWM().getMobLimitSettings(user.getWorld()).contains(c.name())) { + if (!BentoBox.getInstance().getIWM().getMobLimitSettings(world).contains(c.name())) { pib.icon(Material.GREEN_SHULKER_BOX); pib.description(user.getTranslation("protection.flags.LIMIT_MOBS.can")); } else { @@ -127,7 +131,7 @@ public class GeoMobLimitTab implements Tab, ClickHandler { pib.description(user.getTranslation("protection.flags.LIMIT_MOBS.cannot")); } } else { - if (BentoBox.getInstance().getIWM().getGeoLimitSettings(user.getWorld()).contains(c.name())) { + if (BentoBox.getInstance().getIWM().getGeoLimitSettings(world).contains(c.name())) { pib.icon(Material.GREEN_SHULKER_BOX); pib.description(user.getTranslation("protection.panel.flag-item.setting-active")); } else { diff --git a/src/main/java/world/bentobox/bentobox/listeners/flags/clicklisteners/MobLimitClickListener.java b/src/main/java/world/bentobox/bentobox/listeners/flags/clicklisteners/MobLimitClickListener.java index b4733011a..07b7c7ec9 100644 --- a/src/main/java/world/bentobox/bentobox/listeners/flags/clicklisteners/MobLimitClickListener.java +++ b/src/main/java/world/bentobox/bentobox/listeners/flags/clicklisteners/MobLimitClickListener.java @@ -1,6 +1,7 @@ package world.bentobox.bentobox.listeners.flags.clicklisteners; import org.bukkit.Sound; +import org.bukkit.World; import org.bukkit.event.inventory.ClickType; import world.bentobox.bentobox.BentoBox; @@ -26,8 +27,9 @@ public class MobLimitClickListener implements ClickHandler { user.sendMessage("general.errors.wrong-world"); return true; } + World world = panel.getWorld().orElse(user.getWorld()); IslandWorldManager iwm = BentoBox.getInstance().getIWM(); - String reqPerm = iwm.getPermissionPrefix(Util.getWorld(user.getWorld())) + "admin.settings.LIMIT_MOBS"; + String reqPerm = iwm.getPermissionPrefix(Util.getWorld(world)) + "admin.settings.LIMIT_MOBS"; if (!user.hasPermission(reqPerm)) { user.sendMessage("general.errors.no-permission", "[permission]", reqPerm); user.getPlayer().playSound(user.getLocation(), Sound.BLOCK_METAL_HIT, 1F, 1F); @@ -35,19 +37,19 @@ public class MobLimitClickListener implements ClickHandler { } // Open the Sub Settings panel - openPanel(user); + openPanel(user, world); return true; } - private void openPanel(User user) { + private void openPanel(User user, World world) { // Close the current panel user.closeInventory(); // Open a new panel new TabbedPanelBuilder() .user(user) - .world(user.getWorld()) - .tab(1, new GeoMobLimitTab(user, EntityLimitTabType.MOB_LIMIT)) + .world(world) + .tab(1, new GeoMobLimitTab(user, EntityLimitTabType.MOB_LIMIT, world)) .startingSlot(1) .size(54) .build().openPanel(); diff --git a/src/main/java/world/bentobox/bentobox/panels/settings/SettingsTab.java b/src/main/java/world/bentobox/bentobox/panels/settings/SettingsTab.java index dcca84b41..9e9e2d97c 100644 --- a/src/main/java/world/bentobox/bentobox/panels/settings/SettingsTab.java +++ b/src/main/java/world/bentobox/bentobox/panels/settings/SettingsTab.java @@ -47,16 +47,15 @@ public class SettingsTab implements Tab, ClickHandler { /** * Show a tab of settings - * @param world - world * @param user - user who is viewing the tab * @param island - the island * @param type - flag type */ - public SettingsTab(World world, User user, Island island, Type type) { - this.world = world; + public SettingsTab(User user, Island island, Type type) { this.user = user; this.island = island; this.type = type; + this.world = island.getWorld(); } /** diff --git a/src/main/java/world/bentobox/bentobox/util/heads/HeadGetter.java b/src/main/java/world/bentobox/bentobox/util/heads/HeadGetter.java index 9ad31e557..e171b9c79 100644 --- a/src/main/java/world/bentobox/bentobox/util/heads/HeadGetter.java +++ b/src/main/java/world/bentobox/bentobox/util/heads/HeadGetter.java @@ -2,8 +2,8 @@ package world.bentobox.bentobox.util.heads; import java.io.BufferedReader; import java.io.InputStreamReader; +import java.net.HttpURLConnection; import java.net.URL; -import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -14,6 +14,7 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.stream.Collectors; import org.bukkit.Bukkit; +import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; import com.google.gson.Gson; @@ -114,48 +115,96 @@ public class HeadGetter { /** - * This is main task that runs once every 20 ticks and tries to get a player head. + * This is main task that runs once every Settings#ticksBetweenCalls ticks and tries to get + * Settings#headsPerCall player heads at once. + * * @since 1.14.1 */ private void runPlayerHeadGetter() { Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, () -> { synchronized (HeadGetter.names) { - if (!HeadGetter.names.isEmpty()) + int counter = 0; + + while (!HeadGetter.names.isEmpty() && counter < plugin.getSettings().getHeadsPerCall()) { Pair elementEntry = HeadGetter.names.poll(); - - // TODO: In theory BentoBox could use User instance to find existing user UUID's. - // It would avoid one API call. final String userName = elementEntry.getKey(); - // Use cached userId as userId will not change :) - UUID userId = HeadGetter.cachedHeads.containsKey(userName) ? - HeadGetter.cachedHeads.get(userName).getUserId() : - HeadGetter.getUserIdFromName(userName); + // Hmm, task in task in task. That is a weird structure. + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + // Check if we can get user Id. + UUID userId; - // Create new cache object. - HeadCache cache = new HeadCache(userName, - userId, - HeadGetter.getTextureFromUUID(userId)); - - // Save in cache - HeadGetter.cachedHeads.put(userName, cache); - - // Tell requesters the head came in - if (HeadGetter.headRequesters.containsKey(userName)) - { - for (HeadRequester req : HeadGetter.headRequesters.get(userName)) + if (HeadGetter.cachedHeads.containsKey(userName)) { - elementEntry.getValue().setHead(cache.getPlayerHead()); - - Bukkit.getServer().getScheduler().runTaskAsynchronously(this.plugin, - () -> req.setHead(elementEntry.getValue())); + // If cache contains userName, it means that it was already stored. + // We can reuse stored data, as they should not be changed. + userId = HeadGetter.cachedHeads.get(userName).getUserId(); } - } + else if (Bukkit.getServer().getOnlineMode()) + { + // If server is in online mode we can relay that UUID is correct. + // So we use thing that is stored in BentoBox players data. + userId = plugin.getPlayers().getUUID(userName); + } + else + { + // Assign null for later check, as I do not want to write ifs inside + // previous 2 checks. + userId = null; + } + + HeadCache cache; + + if (plugin.getSettings().isUseCacheServer()) + { + // Cache server has an implementation to get a skin just from player name. + Pair playerSkin = HeadGetter.getTextureFromName(userName, userId); + + // Create new cache object. + cache = new HeadCache(userName, + playerSkin.getKey(), + playerSkin.getValue()); + } + else + { + if (userId == null) + { + // Use MojangAPI to get userId from userName. + userId = HeadGetter.getUserIdFromName(userName); + } + + // Create new cache object. + cache = new HeadCache(userName, + userId, + HeadGetter.getTextureFromUUID(userId)); + } + + // Save in cache + HeadGetter.cachedHeads.put(userName, cache); + + // Tell requesters the head came in, but only if the texture is usable. + if (cache.encodedTextureLink != null && HeadGetter.headRequesters.containsKey(userName)) + { + for (HeadRequester req : HeadGetter.headRequesters.get(userName)) + { + elementEntry.getValue().setHead(cache.getPlayerHead()); + + if (!plugin.isShutdown()) + { + // Do not run task if plugin is shutting down. + Bukkit.getScheduler().runTaskAsynchronously(this.plugin, + () -> req.setHead(elementEntry.getValue())); + } + } + } + }); + + counter++; } } - }, 0L, 10L); + }, 0, plugin.getSettings().getTicksBetweenCalls()); } @@ -189,8 +238,7 @@ public class HeadGetter { // UUID just looks more fancy :) String userIdString = jsonObject.get("id").toString(). replace("\"", ""). - replaceFirst("([0-9a-fA-F]{8})([0-9a-fA-F]{4})([0-9a-fA-F]{4})([0-9a-fA-F]{4})([0-9a-fA-F]+)", - "$1-$2-$3-$4-$5"); + replaceFirst("(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})", "$1-$2-$3-$4-$5"); userId = UUID.fromString(userIdString); } @@ -260,6 +308,70 @@ public class HeadGetter { } + /** + * This method gets and returns base64 encoded link to player skin texture from mc-heads.net. + * It tries to use UUID if it is a valid, otherwise it uses given username. + * + * @param userName userName + * @param userId UUID for the user. + * @return Encoded player skin texture or null. + * @since 1.16.0 + */ + private static @NonNull Pair getTextureFromName(String userName, @Nullable UUID userId) { + try + { + Gson gsonReader = new Gson(); + + // Get user encoded texture value. + // mc-heads returns correct skin with providing just a name, unlike mojang api, which + // requires UUID. + JsonObject jsonObject = gsonReader.fromJson( + HeadGetter.getURLContent("https://mc-heads.net/minecraft/profile/" + (userId == null ? userName : userId.toString())), + JsonObject.class); + + /* + * Returned Json Object: + { + id: USER_ID, + name: USER_NAME, + properties: [ + { + name: "textures", + value: ENCODED_BASE64_TEXTURE + } + ] + } + */ + + String decodedTexture = ""; + + String userIdString = jsonObject.get("id").toString(). + replace("\"", ""). + replaceFirst("(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})", "$1-$2-$3-$4-$5"); + + for (JsonElement element : jsonObject.getAsJsonArray("properties")) + { + JsonObject object = element.getAsJsonObject(); + + if (object.has("name") && + object.get("name").getAsString().equals("textures")) + { + decodedTexture = object.get("value").getAsString(); + break; + } + } + + return new Pair<>(UUID.fromString(userIdString), decodedTexture); + } + catch (Exception ignored) + { + } + + // return random uuid and null, to assign some values for cache. + return new Pair<>(userId, null); + } + + /** * This method gets page content of requested url * @@ -270,12 +382,15 @@ public class HeadGetter { private static String getURLContent(String requestedUrl) { String returnValue; - try (BufferedReader reader = new BufferedReader( - new InputStreamReader(new URL(requestedUrl).openStream(), StandardCharsets.UTF_8))) + try { - returnValue = reader.lines().collect(Collectors.joining()); + URL url = new URL(requestedUrl); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream())); + returnValue = br.lines().collect(Collectors.joining()); + br.close(); } - catch (Exception ignored) + catch (Exception e) { returnValue = ""; } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 5060f5839..37b6f67b2 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -82,11 +82,25 @@ panel: # Defines the Material of the item that fills the gaps (in the header, etc.) of most panels. # Added since 1.14.0. filler-material: LIGHT_BLUE_STAINED_GLASS_PANE + # Toggle whether player head texture should be gathered from Mojang API or mc-heads.net cache server. + # Mojang API sometime may be slow and may limit requests to the player data, so this will allow to + # get player heads a bit faster then Mojang API. + # Added since 1.16.0. + use-cache-server: true # Defines how long player skin texture link is stored into local cache before it is requested again. # Defined value is in the minutes. # Value 0 will not clear cache until server restart. # Added since 1.14.1. head-cache-time: 60 + # Defines a number of player heads requested per tasks. + # Setting it too large may lead to temporarily being blocked from head gatherer API. + # Added since 1.16.0. + heads-per-call: 9 + # Defines a number of ticks between each player head request task. + # Setting it too large may lead to temporarily being blocked from head gatherer API. + # Added since 1.16.0. + # /!\ In order to apply the changes made to this option, you must restart your server. Reloading BentoBox or the server won't work. + ticks-between-calls: 10 logs: # Toggle whether superflat chunks regeneration should be logged in the server logs or not. # It can be spammy if there are a lot of superflat chunks to regenerate. diff --git a/src/test/java/world/bentobox/bentobox/listeners/flags/clicklisteners/GeoMobLimitTabTest.java b/src/test/java/world/bentobox/bentobox/listeners/flags/clicklisteners/GeoMobLimitTabTest.java index d59b6a127..818de36b4 100644 --- a/src/test/java/world/bentobox/bentobox/listeners/flags/clicklisteners/GeoMobLimitTabTest.java +++ b/src/test/java/world/bentobox/bentobox/listeners/flags/clicklisteners/GeoMobLimitTabTest.java @@ -15,6 +15,7 @@ import java.util.Optional; import org.bukkit.Bukkit; import org.bukkit.Material; +import org.bukkit.World; import org.bukkit.event.inventory.ClickType; import org.bukkit.inventory.Inventory; import org.eclipse.jdt.annotation.NonNull; @@ -38,18 +39,21 @@ import world.bentobox.bentobox.api.panels.TabbedPanel; import world.bentobox.bentobox.api.user.User; import world.bentobox.bentobox.listeners.flags.clicklisteners.GeoMobLimitTab.EntityLimitTabType; import world.bentobox.bentobox.managers.IslandWorldManager; +import world.bentobox.bentobox.util.Util; /** * @author tastybento * */ @RunWith(PowerMockRunner.class) -@PrepareForTest({Bukkit.class, BentoBox.class}) +@PrepareForTest({Bukkit.class, BentoBox.class, Util.class}) public class GeoMobLimitTabTest { @Mock private User user; @Mock + private World world; + @Mock private TabbedPanel panel; @Mock private BentoBox plugin; @@ -82,6 +86,9 @@ public class GeoMobLimitTabTest { when(panel.getInventory()).thenReturn(inv); // User when(user.getTranslation(anyString())).thenAnswer((Answer) invocation -> invocation.getArgument(0, String.class)); + // Util + PowerMockito.mockStatic(Util.class, Mockito.CALLS_REAL_METHODS); + when(Util.getWorld(any())).thenReturn(world); } @After @@ -94,7 +101,7 @@ public class GeoMobLimitTabTest { */ @Test public void testOnClick() { - GeoMobLimitTab tab = new GeoMobLimitTab(user, EntityLimitTabType.GEO_LIMIT); + GeoMobLimitTab tab = new GeoMobLimitTab(user, EntityLimitTabType.GEO_LIMIT, world); // BAT and COW in list assertEquals(2, list.size()); assertEquals("COW", list.get(1)); @@ -116,7 +123,7 @@ public class GeoMobLimitTabTest { */ @Test public void testOnClickMobLimit() { - GeoMobLimitTab tab = new GeoMobLimitTab(user, EntityLimitTabType.MOB_LIMIT); + GeoMobLimitTab tab = new GeoMobLimitTab(user, EntityLimitTabType.MOB_LIMIT, world); // BAT and COW in list assertEquals(2, list.size()); assertEquals("COW", list.get(1)); @@ -138,7 +145,7 @@ public class GeoMobLimitTabTest { */ @Test public void testGetIcon() { - GeoMobLimitTab tab = new GeoMobLimitTab(user, EntityLimitTabType.MOB_LIMIT); + GeoMobLimitTab tab = new GeoMobLimitTab(user, EntityLimitTabType.MOB_LIMIT, world); PanelItem icon = tab.getIcon(); assertEquals("protection.flags.LIMIT_MOBS.name", icon.getName()); assertEquals(Material.IRON_BOOTS, icon.getItem().getType()); @@ -149,7 +156,7 @@ public class GeoMobLimitTabTest { */ @Test public void testGetIconGeoLimit() { - GeoMobLimitTab tab = new GeoMobLimitTab(user, EntityLimitTabType.GEO_LIMIT); + GeoMobLimitTab tab = new GeoMobLimitTab(user, EntityLimitTabType.GEO_LIMIT, world); PanelItem icon = tab.getIcon(); assertEquals("protection.flags.GEO_LIMIT_MOBS.name", icon.getName()); assertEquals(Material.CHAINMAIL_CHESTPLATE, icon.getItem().getType()); @@ -160,9 +167,9 @@ public class GeoMobLimitTabTest { */ @Test public void testGetName() { - GeoMobLimitTab tab = new GeoMobLimitTab(user, EntityLimitTabType.MOB_LIMIT); + GeoMobLimitTab tab = new GeoMobLimitTab(user, EntityLimitTabType.MOB_LIMIT, world); assertEquals("protection.flags.LIMIT_MOBS.name", tab.getName()); - tab = new GeoMobLimitTab(user, EntityLimitTabType.GEO_LIMIT); + tab = new GeoMobLimitTab(user, EntityLimitTabType.GEO_LIMIT, world); assertEquals("protection.flags.GEO_LIMIT_MOBS.name", tab.getName()); } @@ -171,14 +178,14 @@ public class GeoMobLimitTabTest { */ @Test public void testGetPanelItemsMobLimit() { - GeoMobLimitTab tab = new GeoMobLimitTab(user, EntityLimitTabType.MOB_LIMIT); + GeoMobLimitTab tab = new GeoMobLimitTab(user, EntityLimitTabType.MOB_LIMIT, world); List<@Nullable PanelItem> items = tab.getPanelItems(); assertFalse(items.isEmpty()); items.forEach(i -> { if (i.getName().equals("Cow") || i.getName().equals("Bat")) { - assertEquals(Material.RED_SHULKER_BOX, i.getItem().getType()); + assertEquals("Name : " + i.getName(), Material.RED_SHULKER_BOX, i.getItem().getType()); } else { - assertEquals(Material.GREEN_SHULKER_BOX, i.getItem().getType()); + assertEquals("Name : " + i.getName(), Material.GREEN_SHULKER_BOX, i.getItem().getType()); } }); } @@ -188,14 +195,14 @@ public class GeoMobLimitTabTest { */ @Test public void testGetPanelItemsGeoLimit() { - GeoMobLimitTab tab = new GeoMobLimitTab(user, EntityLimitTabType.GEO_LIMIT); + GeoMobLimitTab tab = new GeoMobLimitTab(user, EntityLimitTabType.GEO_LIMIT, world); List<@Nullable PanelItem> items = tab.getPanelItems(); assertFalse(items.isEmpty()); items.forEach(i -> { if (i.getName().equals("Cow") || i.getName().equals("Bat")) { - assertEquals(Material.GREEN_SHULKER_BOX, i.getItem().getType()); + assertEquals("Name : " + i.getName(), Material.GREEN_SHULKER_BOX, i.getItem().getType()); } else { - assertEquals(Material.RED_SHULKER_BOX, i.getItem().getType()); + assertEquals("Name : " + i.getName(), Material.RED_SHULKER_BOX, i.getItem().getType()); } }); } @@ -205,7 +212,7 @@ public class GeoMobLimitTabTest { */ @Test public void testGetPermission() { - GeoMobLimitTab tab = new GeoMobLimitTab(user, EntityLimitTabType.GEO_LIMIT); + GeoMobLimitTab tab = new GeoMobLimitTab(user, EntityLimitTabType.GEO_LIMIT, world); assertTrue(tab.getPermission().isEmpty()); }