Compare commits

...
Author SHA1 Message Date
Jikoo c9e17e4a12 Update dependencies, use more descriptive record 2021-11-28 20:18:01 -05:00
Jikoo 78908932c5 Progress on tab completion
Complete type, amount, and enchant options
  - Still missing player and chunk bucket completion
Update to Java 16 for main plugin
  - Min supported version 1.16 requires Java 16
Determine uniqueness based on identifier for pseudojson
2021-11-27 16:23:22 -05:00
Jikoo 5f1a613b48 Repackage and rename for clarity 2021-11-27 13:56:31 -05:00
Jikoo 18c3dbaee0 Merge branch 'master' into dev/rewrite-search 2021-11-27 07:09:00 -05:00
Jikoo 6775988347 Stash changes 2021-11-23 12:45:40 -05:00
Jikoo 2580797633 Merge branch 'master' into dev/rewrite-search
# Conflicts:
#	plugin/pom.xml
#	plugin/src/main/java/com/lishid/openinv/commands/SearchContainerCommand.java
#	plugin/src/main/java/com/lishid/openinv/commands/SearchEnchantCommand.java
#	plugin/src/main/java/com/lishid/openinv/commands/SearchInvCommand.java
#	pom.xml
2021-11-17 10:59:17 -05:00
Jikoo 28929fd61e Clean up parsing, prep for tab completion 2021-05-29 09:38:06 -04:00
Jikoo 2445e7f8b6 Disallow concurrent searches from a single user 2021-05-13 07:53:00 -04:00
Jikoo 0af6403570 Move pseudo-JSON to a separate class
This and generifying parsing will make tab completion much easier to write.
2021-04-29 16:45:12 -04:00
Jikoo 55018ca228 Fix last parameter, issues with async chunks 2021-04-24 11:37:59 -04:00
Jikoo 64bd918f32 Merge branch 'master' into dev/rewrite-search
# Conflicts:
#	README.MD
#	api/pom.xml
#	internal/v1_16_R3/pom.xml
#	plugin/pom.xml
#	pom.xml
2021-04-23 14:00:24 -04:00
Jikoo fb6a3a7bcf Simplify dependency management
Use dependencyManagement for versioning shared dependencies
Use pluginManagement for versioning and configuring plugins

Uberjar is still including classes it shouldn't, but that's not a new problem.
2021-04-21 11:07:15 -04:00
Jikoo 4d4aa86399 Simplify enchantment spec 2021-04-21 11:06:53 -04:00
Jikoo 1dce65cecb Drop PaperLib 2021-04-21 11:05:21 -04:00
Jikoo c1145ae111 Search progress
Change detail specs to JSON-esque expressions
Implement matching option parsing
Split search bucket specs into multiple arguments
2021-04-16 12:51:19 -04:00
Jikoo 19f029f17f Search progress
Allow multi-world searches
Search player crafting inventory if open
2021-04-13 20:36:41 -04:00
Jikoo 916a8c0c55 Start reworking search 2021-04-11 17:35:15 -04:00
37 changed files with 2287 additions and 448 deletions

No files matched your search

+1 -2
View File
@@ -60,9 +60,8 @@
<plugins>
<plugin>
<artifactId>maven-shade-plugin</artifactId>
<!-- Cannot use minimizeJar until maven-shade-plugin 3.3.0 releases; ASM used does not support Java 16. -->
<configuration>
<minimizeJar>false</minimizeJar>
<minimizeJar>true</minimizeJar>
</configuration>
</plugin>
<plugin>
+1 -2
View File
@@ -66,9 +66,8 @@
<plugins>
<plugin>
<artifactId>maven-shade-plugin</artifactId>
<!-- Cannot use minimizeJar until maven-shade-plugin 3.3.0 releases; ASM used does not support Java 17. -->
<configuration>
<minimizeJar>false</minimizeJar>
<minimizeJar>true</minimizeJar>
</configuration>
</plugin>
<plugin>
+6 -2
View File
@@ -37,8 +37,12 @@
<groupId>org.jetbrains</groupId>
</dependency>
<dependency>
<artifactId>spigot-api</artifactId>
<groupId>org.spigotmc</groupId>
<artifactId>paper-api</artifactId>
<groupId>io.papermc.paper</groupId>
</dependency>
<dependency>
<artifactId>planarwrappers</artifactId>
<groupId>com.github.jikoo</groupId>
</dependency>
</dependencies>
@@ -20,9 +20,7 @@ import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import com.lishid.openinv.commands.ContainerSettingCommand;
import com.lishid.openinv.commands.OpenInvCommand;
import com.lishid.openinv.commands.SearchContainerCommand;
import com.lishid.openinv.commands.SearchEnchantCommand;
import com.lishid.openinv.commands.SearchInvCommand;
import com.lishid.openinv.commands.SearchCommand;
import com.lishid.openinv.internal.IAnySilentContainer;
import com.lishid.openinv.internal.ISpecialEnderChest;
import com.lishid.openinv.internal.ISpecialInventory;
@@ -295,6 +293,10 @@ public class OpenInv extends JavaPlugin implements IOpenInv {
}
public void sendSystemMessage(@NotNull Player player, @NotNull String key) {
this.sendSystemMessage(player, key, new String[0]);
}
public void sendSystemMessage(@NotNull Player player, @NotNull String key, String... replacements) {
String message = this.languageManager.getValue(key, getLocale(player));
if (message == null) {
@@ -330,6 +332,14 @@ public class OpenInv extends JavaPlugin implements IOpenInv {
}
}
public int getSearchPollsPerTick() {
return this.getConfig().getInt("settings.search.polls-per-tick", 5);
}
public int getSearchResultsMax() {
return this.getConfig().getInt("settings.search.max-results", 40);
}
@Override
public void onDisable() {
@@ -367,9 +377,7 @@ public class OpenInv extends JavaPlugin implements IOpenInv {
// Register commands to their executors
this.setCommandExecutor(new OpenInvCommand(this), "openinv", "openender");
this.setCommandExecutor(new SearchContainerCommand(this), "searchcontainer");
this.setCommandExecutor(new SearchInvCommand(this), "searchinv", "searchender");
this.setCommandExecutor(new SearchEnchantCommand(this), "searchenchant");
this.setCommandExecutor(new SearchCommand(this), "search");
this.setCommandExecutor(new ContainerSettingCommand(this), "silentcontainer", "anycontainer");
} else {
@@ -0,0 +1,249 @@
/*
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.commands;
import com.lishid.openinv.OpenInv;
import com.lishid.openinv.commands.search.ChunkSearchOption;
import com.lishid.openinv.commands.search.CompletableOption;
import com.lishid.openinv.commands.search.ItemAmountOption;
import com.lishid.openinv.commands.search.ItemEnchantOption;
import com.lishid.openinv.commands.search.ItemTypeOption;
import com.lishid.openinv.commands.search.PlayerSearchOption;
import com.lishid.openinv.commands.search.PseudoOption;
import com.lishid.openinv.search.Search;
import com.lishid.openinv.search.bucket.SearchBucket;
import com.lishid.openinv.search.match.ItemMatcher;
import com.lishid.openinv.search.match.MatchableItem;
import com.lishid.openinv.search.match.MatchableMeta;
import com.lishid.openinv.util.PseudoJson;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class SearchCommand implements TabExecutor {
private final OpenInv plugin;
private final Collection<String> activeSearches = Collections.newSetFromMap(new ConcurrentHashMap<>());
private final Collection<CompletableOption<SearchBucket>> completableSearchBuckets = new HashSet<>();
private final Collection<CompletableOption<MatchableItem>> completableMatchOptions = new HashSet<>();
private final Collection<CompletableOption<MatchableMeta>> completableMatchMetaOptions = new HashSet<>();
public SearchCommand(OpenInv plugin) {
this.plugin = plugin;
this.completableSearchBuckets.add(new ChunkSearchOption());
this.completableSearchBuckets.add(new PlayerSearchOption(plugin));
this.completableMatchOptions.add(new ItemAmountOption());
this.completableMatchOptions.add(new ItemTypeOption());
this.completableMatchMetaOptions.add(new ItemEnchantOption());
}
@Override
public boolean onCommand(
@NotNull CommandSender sender,
@NotNull Command command,
@NotNull String label,
@NotNull String[] args) {
if (args.length < 2) {
return false;
}
String id = sender instanceof Player ? plugin.getPlayerID((Player) sender) : "CONSOLE";
if (activeSearches.contains(id)) {
plugin.sendMessage(sender, "messages.error.search.activeSearch");
return true;
}
Collection<PseudoJson> arguments = new HashSet<>();
for (String arg : args) {
arg = arg.toLowerCase(Locale.ROOT);
try {
arguments.add(PseudoJson.fromString(arg));
} catch (IllegalArgumentException e) {
plugin.sendMessage(sender, "messages.error.search.invalidSchema", "%value%", arg);
return true;
}
}
Collection<SearchBucket> searchBuckets = getSearchBuckets(sender, arguments);
if (searchBuckets.isEmpty()) {
plugin.sendMessage(sender, "messages.error.search.invalidInventory");
return true;
}
ItemMatcher matcher = getMatcher(sender, arguments);
if (matcher == null) {
plugin.sendMessage(sender, "messages.error.search.invalidMatcher");
return true;
}
activeSearches.add(id);
new Search(matcher, searchBuckets, () -> activeSearches.remove(id)).schedule(sender, plugin);
return true;
}
private @Nullable ItemMatcher getMatcher(@NotNull CommandSender sender, @NotNull Collection<PseudoJson> args) {
List<PseudoOption<MatchableItem>> matchItemOptions = new ArrayList<>();
List<PseudoOption<MatchableMeta>> matchMetaOptions = new ArrayList<>();
for (PseudoJson arg : args) {
addMatchOptions(completableMatchOptions, matchItemOptions, sender, arg);
addMatchOptions(completableMatchMetaOptions, matchMetaOptions, sender, arg);
}
// Must have a match option. Null entries indicate a parsing error.
if (matchItemOptions.isEmpty()
|| matchItemOptions.stream().anyMatch(Objects::isNull)
|| matchMetaOptions.stream().anyMatch(Objects::isNull)) {
return null;
}
return new ItemMatcher(
matchItemOptions.stream().map(PseudoOption::option).toList(),
matchMetaOptions.stream().map(PseudoOption::option).toList());
}
private <T> void addMatchOptions(
@NotNull Collection<CompletableOption<T>> completableOptions,
@NotNull Collection<PseudoOption<T>> options,
@NotNull CommandSender sender,
@NotNull PseudoJson arg) {
for (CompletableOption<T> option : completableOptions) {
if (option.matches(sender, arg)) {
PseudoOption<T> parsed = option.parse(sender, arg);
if (parsed == null) {
options.add(null);
continue;
}
if (option.isUnique()) {
String uniqueId = option.getName();
// Match existing options.
List<PseudoOption<T>> matches = options.stream()
.filter(Objects::nonNull)
.filter(bucket -> bucket.pseudoJson().getIdentifier().equals(uniqueId))
.collect(Collectors.toList());
// Remove all existing options.
options.removeAll(matches);
// Merge with existing options.
for (PseudoOption<T> match : matches) {
PseudoOption<T> newParsed = option.merge(sender, match, parsed);
if (newParsed != null) {
parsed = newParsed;
}
}
}
options.add(parsed);
}
}
}
private Collection<SearchBucket> getSearchBuckets(
@NotNull CommandSender sender,
@NotNull Collection<PseudoJson> args) {
List<PseudoOption<SearchBucket>> buckets = new ArrayList<>();
for (PseudoJson arg : args) {
addMatchOptions(completableSearchBuckets, buckets, sender, arg);
}
return buckets.stream().map(PseudoOption::option).collect(Collectors.toList());
}
@Override
public @Nullable List<String> onTabComplete(
@NotNull CommandSender sender,
@NotNull Command command,
@NotNull String label,
@NotNull String[] args) {
List<PseudoJson> arguments = new ArrayList<>();
for (int i = 0; i < args.length; i++) {
String arg = args[i].toLowerCase(Locale.ROOT);
try {
arguments.add(PseudoJson.fromString(arg));
} catch (IllegalArgumentException e) {
if (i == args.length - 1) {
// If last argument cannot be parsed, do not complete.
return List.of();
}
// Otherwise, ignore formatting issues during tab completion.
}
}
// Ensure an argument exists.
if (arguments.isEmpty()) {
arguments.add(new PseudoJson(""));
}
// Remove last indexed argument, it is being completed.
PseudoJson last = arguments.remove(arguments.size() - 1);
// Due to generics, can't just use generic list of options to start with.
Collection<PseudoOption<SearchBucket>> buckets = new HashSet<>();
Collection<PseudoOption<MatchableItem>> items = new HashSet<>();
Collection<PseudoOption<MatchableMeta>> metas = new HashSet<>();
for (PseudoJson arg : arguments) {
addMatchOptions(completableSearchBuckets, buckets, sender, arg);
addMatchOptions(completableMatchOptions, items, sender, arg);
addMatchOptions(completableMatchMetaOptions, metas, sender, arg);
}
Collection<PseudoOption<?>> options = new HashSet<>();
options.addAll(buckets);
options.addAll(items);
options.addAll(metas);
// Suggest all completable options' completions.
return Stream.concat(completableSearchBuckets.stream(),
Stream.concat(completableMatchOptions.stream(),
completableMatchMetaOptions.stream()))
// Don't suggest unique options that are already present.
.filter(option ->
option.isUnique() && options.stream().anyMatch(pseudoOption ->
pseudoOption.pseudoJson().getIdentifier().equals(option.getName())))
.flatMap(option -> option.suggestOptions(sender, last).stream())
.distinct()
.toList();
}
}
@@ -1,129 +0,0 @@
/*
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.commands;
import com.lishid.openinv.OpenInv;
import com.lishid.openinv.util.TabCompleter;
import java.util.Collections;
import java.util.List;
import org.bukkit.Chunk;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.BlockState;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import org.bukkit.entity.Player;
import org.bukkit.inventory.InventoryHolder;
import org.jetbrains.annotations.NotNull;
/**
* Command for searching containers in a radius of chunks.
*/
public class SearchContainerCommand implements TabExecutor {
private final OpenInv plugin;
public SearchContainerCommand(OpenInv plugin) {
this.plugin = plugin;
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (!(sender instanceof Player)) {
plugin.sendMessage(sender, "messages.error.consoleUnsupported");
return true;
}
if (args.length < 1) {
// Must supply material
return false;
}
Material material = Material.getMaterial(args[0].toUpperCase());
if (material == null) {
plugin.sendMessage(sender, "messages.error.invalidMaterial", "%target%", args[0]);
return false;
}
int radius = 5;
if (args.length > 1) {
try {
radius = Integer.parseInt(args[1]);
} catch (NumberFormatException e) {
// Invalid radius supplied
return false;
}
}
Player senderPlayer = (Player) sender;
World world = senderPlayer.getWorld();
Chunk centerChunk = senderPlayer.getLocation().getChunk();
StringBuilder locations = new StringBuilder();
for (int dX = -radius; dX <= radius; ++dX) {
for (int dZ = -radius; dZ <= radius; ++dZ) {
if (!world.loadChunk(centerChunk.getX() + dX, centerChunk.getZ() + dZ, false)) {
continue;
}
Chunk chunk = world.getChunkAt(centerChunk.getX() + dX, centerChunk.getZ() + dZ);
for (BlockState tileEntity : chunk.getTileEntities()) {
if (!(tileEntity instanceof InventoryHolder)) {
continue;
}
InventoryHolder holder = (InventoryHolder) tileEntity;
if (!holder.getInventory().contains(material)) {
continue;
}
locations.append(holder.getInventory().getType().name().toLowerCase()).append(" (")
.append(tileEntity.getX()).append(',').append(tileEntity.getY()).append(',')
.append(tileEntity.getZ()).append("), ");
}
}
}
// Matches found, delete trailing comma and space
if (locations.length() > 0) {
locations.delete(locations.length() - 2, locations.length());
} else {
plugin.sendMessage(sender, "messages.info.container.noMatches",
"%target%", material.name());
return true;
}
plugin.sendMessage(sender, "messages.info.container.matches",
"%target%", material.name(), "%detail%", locations.toString());
return true;
}
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
if (args.length < 1 || args.length > 2 || !command.testPermissionSilent(sender)) {
return Collections.emptyList();
}
String argument = args[args.length - 1];
if (args.length == 1) {
return TabCompleter.completeEnum(argument, Material.class);
} else {
return TabCompleter.completeInteger(argument);
}
}
}
@@ -1,169 +0,0 @@
/*
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.commands;
import com.lishid.openinv.OpenInv;
import com.lishid.openinv.util.TabCompleter;
import java.util.Collections;
import java.util.List;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Command adding the ability to search online players' inventories for enchantments of a specific
* type at or above the level specified.
*
* @author Jikoo
*/
public class SearchEnchantCommand implements TabExecutor {
private final OpenInv plugin;
public SearchEnchantCommand(OpenInv plugin) {
this.plugin = plugin;
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (args.length == 0) {
return false;
}
Enchantment enchant = null;
int level = 0;
for (String argument : args) {
try {
level = Integer.parseInt(argument);
continue;
} catch (NumberFormatException ignored) {}
argument = argument.toLowerCase();
int colon = argument.indexOf(':');
NamespacedKey key;
try {
if (colon > -1 && colon < argument.length() - 1) {
key = new NamespacedKey(argument.substring(0, colon), argument.substring(colon + 1));
} else {
key = NamespacedKey.minecraft(argument);
}
} catch (IllegalArgumentException ignored) {
continue;
}
Enchantment localEnchant = Enchantment.getByKey(key);
if (localEnchant != null) {
enchant = localEnchant;
}
}
// Arguments not set correctly
if (level == 0 && enchant == null) {
return false;
}
StringBuilder players = new StringBuilder();
for (Player player : plugin.getServer().getOnlinePlayers()) {
boolean flagInventory = containsEnchantment(player.getInventory(), enchant, level);
boolean flagEnder = containsEnchantment(player.getEnderChest(), enchant, level);
// No matches, continue
if (!flagInventory && !flagEnder) {
continue;
}
// Matches, append details
players.append(player.getName()).append(" (");
if (flagInventory) {
players.append("inv");
}
if (flagEnder) {
if (flagInventory) {
players.append(',');
}
players.append("ender");
}
players.append("), ");
}
if (players.length() > 0) {
// Matches found, delete trailing comma and space
players.delete(players.length() - 2, players.length());
} else {
plugin.sendMessage(sender, "messages.info.player.noMatches",
"%target%", (enchant != null ? enchant.getKey().toString() : "") + " >= " + level);
return true;
}
plugin.sendMessage(sender, "messages.info.player.matches",
"%target%", (enchant != null ? enchant.getKey().toString() : "") + " >= " + level,
"%detail%", players.toString());
return true;
}
private boolean containsEnchantment(Inventory inventory, @Nullable Enchantment enchant, int minLevel) {
for (ItemStack item : inventory.getContents()) {
//noinspection ConstantConditions // Spigot improperly annotated, should be ItemStack @NotNull []
if (item == null || item.getType() == Material.AIR) {
continue;
}
if (enchant != null) {
if (item.containsEnchantment(enchant) && item.getEnchantmentLevel(enchant) >= minLevel) {
return true;
}
} else {
if (!item.hasItemMeta()) {
continue;
}
ItemMeta meta = item.getItemMeta();
if (meta == null || !meta.hasEnchants()) {
continue;
}
for (int enchLevel : meta.getEnchants().values()) {
if (enchLevel >= minLevel) {
return true;
}
}
}
}
return false;
}
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (!command.testPermissionSilent(sender) || args.length < 1 || args.length > 2) {
return Collections.emptyList();
}
if (args.length == 1) {
return TabCompleter.completeObject(args[0], enchantment -> enchantment.getKey().toString(), Enchantment.values());
} else {
return TabCompleter.completeInteger(args[1]);
}
}
}
@@ -1,101 +0,0 @@
/*
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.commands;
import com.lishid.openinv.OpenInv;
import com.lishid.openinv.util.TabCompleter;
import java.util.Collections;
import java.util.List;
import org.bukkit.Material;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import org.jetbrains.annotations.NotNull;
public class SearchInvCommand implements TabExecutor {
private final OpenInv plugin;
public SearchInvCommand(OpenInv plugin) {
this.plugin = plugin;
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
Material material = null;
if (args.length >= 1) {
material = Material.getMaterial(args[0].toUpperCase());
}
if (material == null) {
plugin.sendMessage(sender, "messages.error.invalidMaterial", "%target%", args.length > 0 ? args[0] : "null");
return false;
}
int count = 1;
if (args.length >= 2) {
try {
count = Integer.parseInt(args[1]);
} catch (NumberFormatException ex) {
plugin.sendMessage(sender, "messages.error.invalidNumber", "%target%", args[1]);
return false;
}
}
StringBuilder players = new StringBuilder();
boolean searchInv = command.getName().equals("searchinv");
for (Player player : plugin.getServer().getOnlinePlayers()) {
Inventory inventory = searchInv ? player.getInventory() : player.getEnderChest();
if (inventory.contains(material, count)) {
players.append(player.getName()).append(", ");
}
}
// Matches found, delete trailing comma and space
if (players.length() > 0) {
players.delete(players.length() - 2, players.length());
} else {
plugin.sendMessage(sender, "messages.info.player.noMatches",
"%target%", material.name());
return true;
}
plugin.sendMessage(sender, "messages.info.player.matches",
"%target%", material.name(), "%detail%", players.toString());
return true;
}
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (args.length < 1 || args.length > 2 || !command.testPermissionSilent(sender)) {
return Collections.emptyList();
}
String argument = args[args.length - 1];
if (args.length == 1) {
return TabCompleter.completeEnum(argument, Material.class);
} else {
return TabCompleter.completeInteger(argument);
}
}
}
@@ -0,0 +1,125 @@
/*
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.commands.search;
import com.lishid.openinv.search.bucket.ChunkBucket;
import com.lishid.openinv.search.bucket.SearchBucket;
import com.lishid.openinv.util.Permissions;
import com.lishid.openinv.util.PseudoJson;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.World;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class ChunkSearchOption implements CompletableOption<SearchBucket> {
@Override
public @NotNull String getName() {
return "chunk";
}
@Override
public boolean matches(@NotNull CommandSender sender, @NotNull PseudoJson pseudoJson) {
return CompletableOption.super.matches(sender, pseudoJson)
&& Permissions.SEARCH_CHUNKS_LOADED.hasPermission(sender);
}
@Override
public @Nullable PseudoOption<SearchBucket> parse(@NotNull CommandSender sender, @NotNull PseudoJson pseudoJson) {
World world = null;
Integer chunkX = null;
Integer chunkZ = null;
int chunkRadius = 5;
boolean chunkLoad = false;
Map<String, String> mappings = pseudoJson.getMappings();
if (!(sender instanceof Player senderPlayer)) {
if (mappings.size() < 3) {
// Console must specify world, x center, z center.
return null;
}
} else {
world = senderPlayer.getWorld();
Chunk senderChunk = senderPlayer.getLocation().getChunk();
chunkX = senderChunk.getX();
chunkZ = senderChunk.getZ();
}
for (Map.Entry<String, String> entry : mappings.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
if (key.indexOf('w') >= 0) {
world = null;
// Name should always be lower case by this point. Ignore case when selecting world.
for (World loaded : Bukkit.getWorlds()) {
if (loaded.getName().equalsIgnoreCase(value)) {
world = loaded;
break;
}
}
} else if (key.indexOf('x') >= 0) {
try {
chunkX = Integer.parseInt(value);
} catch (NumberFormatException e) {
chunkX = null;
}
} else if (key.indexOf('z') >= 0) {
try {
chunkZ = Integer.parseInt(value);
} catch (NumberFormatException e) {
chunkZ = null;
}
} else if (key.indexOf('r') >= 0) {
// R is for radius. Needs to be checked later so that "world" doesn't result in a false positive.
try {
chunkRadius = Integer.parseInt(value);
} catch (NumberFormatException e) {
// Fall through to default radius.
}
} else if (key.indexOf('l') >= 0) {
// L is for loaded. Needs to be checked later so that "world" doesn't result in a false positive.
chunkLoad = Permissions.SEARCH_CHUNKS_UNLOADED.hasPermission(sender) && Boolean.parseBoolean(value);
}
}
if (world == null || chunkX == null || chunkZ == null || chunkRadius < 0) {
// Invalid data provided.
return null;
}
return new PseudoOption<>(pseudoJson, new ChunkBucket(world, chunkX, chunkZ, chunkRadius, chunkLoad));
}
@Override
public @NotNull Collection<String> suggestOptions(@NotNull CommandSender sender, @NotNull PseudoJson pseudoJson) {
// TODO
return Set.of();
}
@Override
public boolean isUnique() {
return false;
}
}
@@ -0,0 +1,110 @@
/*
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.commands.search;
import com.lishid.openinv.util.PseudoJson;
import java.util.Collection;
import java.util.Locale;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Interface defining behavior for an option.
*
* @param <T> the type of option represented
*/
public interface CompletableOption<T> {
/**
* Get unique identifier representing the option.
*
* @return the unique identifier
*/
@NotNull String getName();
/**
* Check if the <code>CompletableOption</code> matches the given <code>PseudoJson</code>.
*
* @param sender the individual using the option
* @param pseudoJson the pseudo-JSON element
* @return true if the CompletableOption matches
*/
default boolean matches(@NotNull CommandSender sender, @NotNull PseudoJson pseudoJson) {
return pseudoJson.getIdentifier().toLowerCase(Locale.ROOT).startsWith(getName());
}
/**
* Parse the option represented from the given <code>PseudoJson</code>.
*
* @param sender the individual using the option
* @param pseudoJson the pseudo-JSON element
* @return the parsed options
*/
@Nullable PseudoOption<T> parse(@NotNull CommandSender sender, @NotNull PseudoJson pseudoJson);
/**
* Form a collection of viable permutations to suggest for the given <code>PseudoJson</code>.
*
* @param sender the individual to suggest options for
* @param pseudoJson the parsed pseudo-JSON
* @return the suggested permutations
*/
@NotNull Collection<String> suggestOptions(
@NotNull CommandSender sender,
@NotNull PseudoJson pseudoJson);
/**
* Get whether or not multiple different copies of this <code>CompletableOption</code> are allowed.
*
* @return true if only a single instance is allowed
*/
boolean isUnique();
/**
* Merge similar elements. Used to enforce {@link #isUnique()} despite multiples being provided.
*
* @param sender the individual using the options
* @param first the first parsed option
* @param second the second parsed option
* @return a single element containing a best-effort merge of both
*/
default @Nullable PseudoOption<T> merge(
@NotNull CommandSender sender,
@NotNull PseudoOption<?> first,
@NotNull PseudoOption<?> second) {
PseudoJson merge = new PseudoJson(first.pseudoJson().getIdentifier());
// Use mappings of first for our base.
first.pseudoJson().getMappings().forEach(merge::put);
// Overwrite with second.
second.pseudoJson().getMappings().forEach(merge::put);
// Parse merged options.
PseudoOption<T> parsed = parse(sender, merge);
if (parsed != null) {
return parsed;
}
// On parsing failure, try second instead.
merge = new PseudoJson(second.pseudoJson().getIdentifier(), merge.getMappings());
return parse(sender, merge);
}
}
@@ -0,0 +1,84 @@
/*
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.commands.search;
import com.lishid.openinv.search.match.ItemMatchables;
import com.lishid.openinv.search.match.MatchableItem;
import com.lishid.openinv.util.PseudoJson;
import com.lishid.openinv.util.TabCompleter;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.bukkit.command.CommandSender;
import org.bukkit.util.StringUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class ItemAmountOption implements CompletableOption<MatchableItem> {
@Override
public @NotNull String getName() {
return "amount";
}
@Override
public @Nullable PseudoOption<MatchableItem> parse(@NotNull CommandSender sender, @NotNull PseudoJson pseudoJson) {
String minAmountString = pseudoJson.getMappings().entrySet().stream().findFirst().map(Map.Entry::getValue).orElse(null);
if (minAmountString == null) {
// Amount not correctly specified. Warn about spec.
return null;
}
int minAmount;
try {
minAmount = Integer.parseInt(minAmountString);
} catch (NumberFormatException e) {
// Amount can't be parsed. Warn about spec.
return null;
}
return new PseudoOption<>(pseudoJson, ItemMatchables.hasAmount(minAmount));
}
@Override
public @NotNull Collection<String> suggestOptions(@NotNull CommandSender sender, @NotNull PseudoJson pseudoJson) {
// TODO combine with type
if (!pseudoJson.getIdentifier().equals(getName())) {
// Suggest name while unformed.
if (StringUtil.startsWithIgnoreCase(getName(), pseudoJson.getIdentifier())) {
return Set.of(getName() + ":");
}
// If name is not a valid suggestion, suggest nothing.
return Set.of();
}
String minAmountString = pseudoJson.getMappings().entrySet().stream().findFirst().map(Map.Entry::getValue).orElse("");
// Find and suggest name in full pseudojson.
return TabCompleter.completeInteger(minAmountString).stream().map(match -> {
PseudoJson amount = new PseudoJson(getName());
amount.put(getName(), match);
return amount;
}).map(PseudoJson::asString).collect(Collectors.toSet());
}
@Override
public boolean isUnique() {
return true;
}
}
@@ -0,0 +1,137 @@
/*
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.commands.search;
import com.github.jikoo.planarwrappers.util.StringConverters;
import com.lishid.openinv.search.match.ItemMatchables;
import com.lishid.openinv.search.match.MatchableMeta;
import com.lishid.openinv.util.PseudoJson;
import com.lishid.openinv.util.TabCompleter;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.bukkit.command.CommandSender;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.util.StringUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class ItemEnchantOption implements CompletableOption<MatchableMeta> {
@Override
public @NotNull String getName() {
return "enchant";
}
@Override
public @Nullable PseudoOption<MatchableMeta> parse(@NotNull CommandSender sender, @NotNull PseudoJson pseudoJson) {
Enchantment enchantment = null;
Integer enchantLevel = null;
for (Map.Entry<String, String> entry : pseudoJson.getMappings().entrySet()) {
if (entry.getValue().isBlank()) {
continue;
}
if (entry.getKey().indexOf('l') >= 0) {
try {
enchantLevel = Integer.parseInt(entry.getValue());
} catch (NumberFormatException e) {
// Level can't be parsed. Warn about spec.
return null;
}
}
if (entry.getKey().indexOf('t') >= 0) {
enchantment = StringConverters.toEnchant(entry.getValue());
if (enchantment == null) {
// Enchantment is specified but can't be parsed. Warn about spec.
return null;
}
}
}
return new PseudoOption<>(pseudoJson, ItemMatchables.hasEnchant(enchantment, enchantLevel));
}
@Override
public @NotNull Collection<String> suggestOptions(@NotNull CommandSender sender, @NotNull PseudoJson pseudoJson) {
if (!pseudoJson.getIdentifier().equals(getName())) {
if (!StringUtil.startsWithIgnoreCase(getName(), pseudoJson.getIdentifier())) {
// If enchant is not a valid suggestion, suggest nothing.
return Set.of();
}
// Suggest all options while unformed.
Set<String> set = new HashSet<>();
PseudoJson enchant = new PseudoJson(getName());
set.add(enchant.asString());
enchant.put("type", "");
set.add(enchant.asString());
enchant.put("level", "");
set.add(enchant.asString());
enchant.remove("type");
set.add(enchant.asString());
return set;
}
Collection<String> enchantments = null;
Integer enchantLevel = null;
for (Map.Entry<String, String> entry : pseudoJson.getMappings().entrySet()) {
if (entry.getValue().isBlank()) {
continue;
}
if (entry.getKey().indexOf('l') >= 0) {
try {
enchantLevel = Integer.parseInt(entry.getValue());
} catch (NumberFormatException e) {
// Level is present but cannot be parsed. Default 0.
enchantLevel = 0;
}
}
if (entry.getKey().indexOf('t') >= 0) {
Enchantment enchant = StringConverters.toEnchant(entry.getValue());
if (enchant == null) {
// Enchantment is specified but can't be parsed, tab complete.
enchantments = TabCompleter.completeKeyed(entry.getValue(), Enchantment.values());
} else {
enchantments = Set.of(entry.getValue());
}
}
}
PseudoJson enchant = new PseudoJson(getName());
if (enchantLevel != null) {
enchant.put("level", String.valueOf(enchantLevel));
}
if (enchantments == null) {
return Set.of(enchant.asString());
}
return enchantments.stream().map(enchantment -> {
// Reuse is fine, single-threaded.
enchant.put("type", enchantment);
return enchant;
}).map(PseudoJson::asString).collect(Collectors.toSet());
}
@Override
public boolean isUnique() {
return false;
}
}
@@ -0,0 +1,78 @@
/*
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.commands.search;
import com.github.jikoo.planarwrappers.util.StringConverters;
import com.lishid.openinv.search.match.ItemMatchables;
import com.lishid.openinv.search.match.MatchableItem;
import com.lishid.openinv.util.PseudoJson;
import com.lishid.openinv.util.TabCompleter;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.bukkit.Material;
import org.bukkit.command.CommandSender;
import org.bukkit.util.StringUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class ItemTypeOption implements CompletableOption<MatchableItem> {
@Override
public @NotNull String getName() {
return "type";
}
@Override
public @Nullable PseudoOption<MatchableItem> parse(@NotNull CommandSender sender, @NotNull PseudoJson pseudoJson) {
String typeName = pseudoJson.getMappings().entrySet().stream().findFirst().map(Map.Entry::getValue).orElse(null);
Material material = StringConverters.toMaterial(typeName);
if (material == null) {
// Invalid material, warn about matcher spec.
return null;
}
return new PseudoOption<>(pseudoJson, ItemMatchables.isType(material));
}
@Override
public @NotNull Collection<String> suggestOptions(@NotNull CommandSender sender, @NotNull PseudoJson pseudoJson) {
if (!pseudoJson.getIdentifier().equals(getName())) {
// Suggest name while unformed.
if (StringUtil.startsWithIgnoreCase(getName(), pseudoJson.getIdentifier())) {
return Set.of(getName() + ":");
}
// If name is not a valid suggestion, suggest nothing.
return Set.of();
}
String typeName = pseudoJson.getMappings().entrySet().stream().findFirst().map(Map.Entry::getValue).orElse("");
// Find and suggest matching types in full pseudojson.
return TabCompleter.completeMaterial(typeName).stream().map(match -> {
PseudoJson type = new PseudoJson(getName());
type.put(getName(), match);
return type;
}).map(PseudoJson::asString).collect(Collectors.toSet());
}
@Override
public boolean isUnique() {
return true;
}
}
@@ -0,0 +1,75 @@
/*
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.commands.search;
import com.lishid.openinv.OpenInv;
import com.lishid.openinv.search.bucket.PlayerBucket;
import com.lishid.openinv.search.bucket.SearchBucket;
import com.lishid.openinv.util.Permissions;
import com.lishid.openinv.util.PseudoJson;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class PlayerSearchOption implements CompletableOption<SearchBucket> {
private final OpenInv plugin;
public PlayerSearchOption(OpenInv plugin) {
this.plugin = plugin;
}
@Override
public @NotNull String getName() {
return "player";
}
@Override
public boolean matches(@NotNull CommandSender sender, @NotNull PseudoJson pseudoJson) {
return CompletableOption.super.matches(sender, pseudoJson)
&& Permissions.SEARCH_PLAYERS_ONLINE.hasPermission(sender);
}
@Override
public @Nullable PseudoOption<SearchBucket> parse(@NotNull CommandSender sender, @NotNull PseudoJson pseudoJson) {
Optional<Boolean> optional = pseudoJson.getMappings().entrySet().stream()
.filter(entry -> entry.getKey().contains("offline"))
.findFirst()
.map(Map.Entry::getValue)
.map(Boolean::parseBoolean);
return new PseudoOption<>(
pseudoJson, new PlayerBucket(plugin,
!(optional.orElse(false) && Permissions.SEARCH_PLAYERS_OFFLINE.hasPermission(sender))));
}
@Override
public @NotNull Collection<String> suggestOptions(@NotNull CommandSender sender, @NotNull PseudoJson pseudoJson) {
// TODO
return Set.of();
}
@Override
public boolean isUnique() {
return true;
}
}
@@ -0,0 +1,27 @@
/*
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.commands.search;
import com.lishid.openinv.util.PseudoJson;
import org.jetbrains.annotations.NotNull;
/**
* Data holder for completable options.
*
* @param <T> the type of data
*/
public record PseudoOption<T>(@NotNull PseudoJson pseudoJson, @NotNull T option) {}
@@ -0,0 +1,210 @@
/*
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.search;
import com.lishid.openinv.OpenInv;
import com.lishid.openinv.search.bucket.SearchBucket;
import com.lishid.openinv.search.match.ItemMatcher;
import com.lishid.openinv.search.match.MatchResult;
import com.lishid.openinv.search.match.Matchable;
import com.lishid.openinv.util.MessagePart;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.stream.Collectors;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.TextComponent;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitRunnable;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* A search function.
*/
public class Search {
public static final BaseComponent SEPARATOR = new TextComponent(", ");
private final Set<MessagePart> matches = new HashSet<>();
private final ItemMatcher matcher;
private final double totalMatchables;
private final Iterator<SearchBucket> bucketIterator;
private final Runnable callback;
private SearchBucket current = null;
private int polls = 0;
public Search(
@NotNull ItemMatcher matcher,
@NotNull Collection<@NotNull SearchBucket> buckets,
@Nullable Runnable callback) {
// Just in case, copy contents to new set.
Set<SearchBucket> bucketSet = new HashSet<>(buckets);
this.matcher = matcher;
this.totalMatchables = bucketSet.stream().mapToInt(SearchBucket::size).sum();
this.bucketIterator = bucketSet.iterator();
this.callback = callback;
}
private boolean isComplete(OpenInv plugin) {
if (matches.size() >= plugin.getSearchResultsMax()) {
return true;
}
if (current != null && current.hasNext()) {
return false;
}
while (bucketIterator.hasNext()) {
current = bucketIterator.next();
if (current.hasNext()) {
return false;
}
}
return true;
}
/**
* Search the configured amount of Matchables.
*
* <p>Matchables are assembled asynchronously, then
*
* @param plugin the OpenInv instance
*/
private void poll(OpenInv plugin) {
Collection<Matchable> syncMatchables = new ArrayList<>();
// Assemble matchables async, then search synchronously.
for (int i = 0; !isComplete(plugin) && i < plugin.getSearchPollsPerTick(); ++i) {
++polls;
// Calling #isComplete will advance to next InventoryProvider as necessary.
// Bucket matchables for searching synchronously.
syncMatchables.add(current.next());
}
Future<Boolean> future = plugin.getServer().getScheduler().callSyncMethod(plugin, () -> {
for (Matchable matchable : syncMatchables) {
MatchResult match = matchable.match(matcher);
if (match.isMatch()) {
matches.addAll(Arrays.asList(match.getMatch()));
}
}
return true;
});
try {
future.get();
} catch (InterruptedException | ExecutionException e) {
// Eat exception to prevent spam.
}
}
private void sendProgress(CommandSender sender, OpenInv plugin) {
if (!(sender instanceof Player) || polls % (10 * plugin.getSearchPollsPerTick()) != 1) {
return;
}
double progress = 100 * Math.min(polls, totalMatchables) / totalMatchables;
DecimalFormat percent = new DecimalFormat("00.0");
plugin.sendSystemMessage((Player) sender, "messages.info.search.progress",
"%progress%", percent.format(progress),
"%matches%", String.valueOf(matches.size()));
}
private void sendResults(CommandSender sender, OpenInv plugin) {
if (matches.isEmpty()) {
String localizedMessage = plugin.getLocalizedMessage(sender, "messages.info.search.noMatches");
if (localizedMessage == null) {
localizedMessage = "No items found with search parameters.";
}
sender.sendMessage(localizedMessage);
return;
}
String prefix = plugin.getLocalizedMessage(sender, "messages.info.search.matches");
if (prefix == null) {
prefix = "Locations of items matching parameters:";
}
if (sender instanceof Player) {
TextComponent textComponent = new TextComponent(prefix);
Iterator<MessagePart> iterator = matches.iterator();
int total = 0;
int searchResultsMax = plugin.getSearchResultsMax();
while (iterator.hasNext()) {
textComponent.addExtra(iterator.next().forPlayer());
++total;
if (total >= searchResultsMax) {
break;
}
if (iterator.hasNext()) {
textComponent.addExtra(SEPARATOR);
}
}
// Paper deprecated Spigot's message API.
// This is a risky deprecation ignore - Spigot may eventually do the sane thing
// and just add a method accepting components to the player directly.
//noinspection deprecation
sender.spigot().sendMessage(textComponent);
return;
}
String message = matches.stream()
.limit(plugin.getSearchResultsMax())
.map(MessagePart::forConsole)
.collect(Collectors.joining(", ", prefix, ""));
sender.sendMessage(message);
}
public void schedule(CommandSender sender, OpenInv plugin) {
new BukkitRunnable() {
@Override
public void run() {
// Scheduler struggles to cancel async tasks with high frequency.
if (isCancelled()) {
return;
}
poll(plugin);
if (isComplete(plugin)) {
if (isCancelled()) {
return;
}
sendResults(sender, plugin);
cancel();
if (callback != null) {
callback.run();
}
return;
}
sendProgress(sender, plugin);
}
}.runTaskTimerAsynchronously(plugin, 0L, 1L);
}
}
@@ -0,0 +1,123 @@
/*
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.search.bucket;
import com.lishid.openinv.search.match.MatchResult;
import com.lishid.openinv.search.match.Matchable;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.function.BiConsumer;
import org.bukkit.Chunk;
import org.bukkit.World;
import org.jetbrains.annotations.NotNull;
public class ChunkBucket implements SearchBucket {
private final World world;
private final boolean load;
private final List<ChunkCoord> chunks;
protected int index = -1;
private Boolean paper = null;
public ChunkBucket(@NotNull World world, int chunkX, int chunkZ, int radius, boolean load) {
this.world = world;
this.load = load;
this.chunks = new ArrayList<>();
// Order chunks based (loosely) on proximity.
spiralRectangle(radius, (deltaX, deltaZ) -> chunks.add(new ChunkCoord(chunkX + deltaX, chunkZ + deltaZ)));
}
@Override
public @NotNull Matchable next() throws IndexOutOfBoundsException {
ChunkCoord chunkCoord = chunks.get(++index);
if (!world.isChunkGenerated(chunkCoord.x(), chunkCoord.z())) {
return Matchable.EMPTY;
}
if (!load && !world.isChunkLoaded(chunkCoord.x(), chunkCoord.z())) {
return Matchable.EMPTY;
}
// If async chunk loading is not available, load chunk when checking.
if (!isPaper()) {
return new MatchableChunk(world, chunkCoord.x(), chunkCoord.z());
}
CompletableFuture<Chunk> chunkAt = world.getChunkAtAsync(chunkCoord.x(), chunkCoord.z());
Chunk chunk;
try {
chunk = chunkAt.get();
} catch (InterruptedException | ExecutionException e) {
return matcher -> MatchResult.NO_MATCH;
}
return new MatchableChunk(chunk);
}
private boolean isPaper() {
if (paper == null) {
try {
// Check for Paper.
Class.forName("com.destroystokyo.paper.PaperConfig");
paper = true;
} catch (ClassNotFoundException e) {
paper = false;
}
}
return paper;
}
@Override
public boolean hasNext() {
return index < chunks.size() - 1;
}
@Override
public int size() {
return chunks.size();
}
/**
* Produce a series of integer coordinates starting at 0, 0 that extend outward to form a centered square,
* producing closer proximity results sooner than a traditional double for loop from min to max.
*
* @param radius the radius of the square
* @param coordConsumer the method consuming the values produced
*/
private static void spiralRectangle(int radius, BiConsumer<Integer, Integer> coordConsumer) {
for (int x = 0; x <= radius; ++x) {
for (int z = 0; z <= radius; ++z) {
coordConsumer.accept(x, z);
if (x != 0) {
coordConsumer.accept(-x, z);
if (z != 0) {
coordConsumer.accept(-x, -z);
}
}
if (z != 0) {
coordConsumer.accept(x, -z);
}
}
}
}
}
@@ -0,0 +1,22 @@
/*
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.search.bucket;
/**
* A data holder for chunk coordinates.
*/
public record ChunkCoord(int x, int z) {}
@@ -0,0 +1,128 @@
/*
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.search.bucket;
import com.lishid.openinv.search.match.ItemMatcher;
import com.lishid.openinv.search.match.MatchResult;
import com.lishid.openinv.search.match.Matchable;
import com.lishid.openinv.util.MessagePart;
import java.util.ArrayList;
import java.util.List;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.ClickEvent;
import net.md_5.bungee.api.chat.HoverEvent;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.chat.hover.content.Text;
import org.bukkit.Chunk;
import org.bukkit.World;
import org.bukkit.block.BlockState;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryHolder;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
class MatchableChunk implements Matchable {
private Chunk chunk;
private World world;
private int chunkX;
private int chunkZ;
MatchableChunk(@NotNull Chunk chunk) {
this.chunk = chunk;
}
MatchableChunk(@NotNull World world, int chunkX, int chunkZ) {
this.world = world;
this.chunkX = chunkX;
this.chunkZ = chunkZ;
}
private Chunk getChunk() {
if (chunk == null) {
chunk = world.getChunkAt(chunkX, chunkZ);
}
return chunk;
}
@Override
public @NotNull MatchResult match(@NotNull ItemMatcher matcher) {
List<BlockState> blocks = new ArrayList<>();
for (BlockState tileEntity : getChunk().getTileEntities()) {
if (!(tileEntity instanceof InventoryHolder)) {
continue;
}
Inventory inventory = ((InventoryHolder) tileEntity).getInventory();
for (ItemStack content : inventory.getContents()) {
if (matcher.matches(content)) {
blocks.add(tileEntity);
break;
}
}
}
if (blocks.isEmpty()) {
return MatchResult.NO_MATCH;
}
MessagePart[] matches = new MessagePart[blocks.size()];
for (int i = 0; i < matches.length; ++i) {
BlockState state = blocks.get(i);
matches[i] = new MessagePart(() -> getComponent(state), () -> getString(state));
}
return new MatchResult(matches);
}
private static BaseComponent getComponent(BlockState state) {
TextComponent component = new TextComponent(state.getType().name());
String command = String.format(
"/execute in %s minecraft:tp %s %s %s",
state.getWorld().getName(),
state.getX(),
state.getY() + 1,
state.getZ());
component.setClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, command));
String hover = String.format(
"%s{world:%s,x:%s,y:%s,z:%s}\n\nClick to teleport.",
state.getType().getKey(),
state.getWorld().getName(),
state.getX(),
state.getY(),
state.getZ());
Text hoverText = new Text(new BaseComponent[] { new TextComponent(hover)});
component.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, hoverText));
return component;
}
private static String getString(BlockState state) {
return String.format(
"%s{world:%s,x:%s,y:%s,z:%s}",
state.getType().getKey(),
state.getWorld().getName(),
state.getX(),
state.getY(),
state.getZ());
}
}
@@ -0,0 +1,174 @@
/*
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.search.bucket;
import com.lishid.openinv.OpenInv;
import com.lishid.openinv.search.match.ItemMatcher;
import com.lishid.openinv.search.match.MatchResult;
import com.lishid.openinv.search.match.Matchable;
import com.lishid.openinv.util.MessagePart;
import java.util.UUID;
import java.util.function.Supplier;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.ClickEvent;
import net.md_5.bungee.api.chat.HoverEvent;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.chat.hover.content.Text;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.inventory.InventoryView;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
import org.jetbrains.annotations.NotNull;
public class MatchablePlayer implements Matchable {
private final OpenInv plugin;
private final OfflinePlayer offlinePlayer;
public MatchablePlayer(OpenInv plugin, OfflinePlayer offlinePlayer) {
this.plugin = plugin;
this.offlinePlayer = offlinePlayer;
}
@Override
public @NotNull MatchResult match(@NotNull ItemMatcher matcher) {
Player player;
if (offlinePlayer instanceof Player) {
player = (Player) offlinePlayer;
} else {
player = plugin.loadPlayer(offlinePlayer);
}
if (player == null) {
return MatchResult.NO_MATCH;
}
boolean matchInv = false;
boolean matchEnder = false;
PlayerInventory playerInventory = player.getInventory();
// Check inventory. Modern implementations include armor and off hand in contents.
for (ItemStack content : playerInventory.getContents()) {
if (matcher.matches(content)) {
matchInv = true;
break;
}
}
InventoryView openInventory = player.getOpenInventory();
// If no matches are found yet, check cursor.
if (!matchInv && matcher.matches(openInventory.getCursor())) {
matchInv = true;
}
// Check player crafting inventory.
if (!matchInv && openInventory.getType() == InventoryType.CRAFTING) {
for (ItemStack content : openInventory.getTopInventory().getContents()) {
if (matcher.matches(content)) {
matchInv = true;
break;
}
}
}
// Check ender chest.
for (ItemStack content : player.getEnderChest().getContents()) {
if (matcher.matches(content)) {
matchEnder = true;
break;
}
}
if (!matchInv && !matchEnder) {
return MatchResult.NO_MATCH;
}
String name = player.getName();
UUID uuid = player.getUniqueId();
if (matchInv && matchEnder) {
return new MatchResult(new MessagePart[] {
new MessagePart(() -> getBoth(name, uuid), () -> name + " (inv, ender)")
});
}
Supplier<BaseComponent> componentSupplier;
Supplier<String> stringSupplier;
if (matchInv) {
componentSupplier = () -> getSingle(name, () -> getInvClick(uuid), () -> getInvHover(name));
stringSupplier = () -> name + " (inv)";
} else {
componentSupplier = () -> getSingle(name, () -> getEnderClick(uuid), () -> getEnderHover(name));
stringSupplier = () -> name + " (ender)";
}
return new MatchResult(new MessagePart[] { new MessagePart(componentSupplier, stringSupplier) });
}
private static BaseComponent getBoth(String name, UUID uuid) {
BaseComponent component = new TextComponent(name + " (inv,");
component.setClickEvent(getInvClick(uuid));
component.setHoverEvent(getInvHover(name));
BaseComponent enderComponent = new TextComponent(" ender)");
enderComponent.setClickEvent(getEnderClick(uuid));
enderComponent.setHoverEvent(getEnderHover(name));
component.addExtra(enderComponent);
return component;
}
private static BaseComponent getSingle(String name, Supplier<ClickEvent> click, Supplier<HoverEvent> hover) {
BaseComponent component = new TextComponent(name);
component.setClickEvent(click.get());
component.setHoverEvent(hover.get());
return component;
}
private static ClickEvent getInvClick(UUID uuid) {
return getClick("/openinv:openinv ", uuid);
}
private static HoverEvent getInvHover(String name) {
return getHover("/openinv ", name);
}
private static ClickEvent getEnderClick(UUID uuid) {
return getClick("/openinv:openender ", uuid);
}
private static HoverEvent getEnderHover(String name) {
return getHover("/openender ", name);
}
private static ClickEvent getClick(String command, UUID uuid) {
// Actual command run is not exposed to user, so there's no reason not to use UUID and be precise.
return new ClickEvent(ClickEvent.Action.RUN_COMMAND, command + uuid);
}
private static HoverEvent getHover(String command, String name) {
BaseComponent[] text = new BaseComponent[] { new TextComponent(command + name) };
return new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text(text));
}
}
@@ -0,0 +1,56 @@
/*
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.search.bucket;
import com.lishid.openinv.OpenInv;
import com.lishid.openinv.search.match.Matchable;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
public class PlayerBucket implements SearchBucket {
private final OpenInv plugin;
private final OfflinePlayer[] players;
private int index = -1;
public PlayerBucket(OpenInv plugin, boolean online) {
this.plugin = plugin;
if (online) {
this.players = Bukkit.getOnlinePlayers().toArray(new Player[0]);
} else {
this.players = Bukkit.getOfflinePlayers();
}
}
@Override
public boolean hasNext() {
return index < players.length - 1;
}
@Override
public @NotNull Matchable next() {
return new MatchablePlayer(plugin, players[++index]);
}
@Override
public int size() {
return 1;
}
}
@@ -0,0 +1,30 @@
/*
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.search.bucket;
import com.lishid.openinv.search.match.Matchable;
import org.jetbrains.annotations.NotNull;
public interface SearchBucket {
boolean hasNext();
@NotNull Matchable next() throws IndexOutOfBoundsException;
int size();
}
@@ -0,0 +1,64 @@
/*
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.search.match;
import org.bukkit.Material;
import org.bukkit.enchantments.Enchantment;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class ItemMatchables {
public static MatchableItem isType(@NotNull Material material) {
return itemStack -> itemStack.getType() == material;
}
public static MatchableItem hasAmount(int minAmount) {
return itemStack -> itemStack.getAmount() >= minAmount;
}
public static MatchableMeta hasEnchant(@Nullable Enchantment enchantment, @Nullable Integer minLevel) {
return itemMeta -> {
if (minLevel == null) {
if (enchantment == null) {
// Level and enchantment not specified, any enchantment of any level matches.
return !itemMeta.getEnchants().isEmpty();
}
// Level not specified, any level of enchantment matches.
return itemMeta.hasEnchant(enchantment);
}
if (enchantment != null) {
// Level and enchantment are specified, enchantment must meet or exceed level.
return itemMeta.getEnchantLevel(enchantment) >= minLevel;
}
// Enchantment not specified, any enchantment that meets or exceeds level matches.
for (int enchantLevel : itemMeta.getEnchants().values()) {
if (enchantLevel >= minLevel) {
return true;
}
}
return false;
};
}
private ItemMatchables() {}
}
@@ -0,0 +1,79 @@
/*
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.search.match;
import java.util.Collection;
import java.util.HashSet;
import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.jetbrains.annotations.Nullable;
/**
* A collection of multiple filters used to match items.
*/
public class ItemMatcher {
private final Collection<MatchableItem> baseOptions;
private final Collection<MatchableMeta> metaOptions;
public ItemMatcher(
Collection<MatchableItem> baseOptions,
Collection<MatchableMeta> metaOptions) {
this.baseOptions = new HashSet<>(baseOptions);
this.metaOptions = new HashSet<>(metaOptions);
}
public boolean matches(@Nullable ItemStack other) {
// Ensure item exists.
if (other == null || other.getType() == Material.AIR) {
return false;
}
// Match options against base item.
for (MatchableItem baseOption : baseOptions) {
if (!baseOption.matches(other)) {
return false;
}
}
// If no meta options exist, item matches.
if (metaOptions.isEmpty()) {
return true;
}
// Require item meta for meta options.
ItemMeta meta;
if (!other.hasItemMeta() || (meta = other.getItemMeta()) == null) {
return false;
}
// Match meta-based options.
for (MatchableMeta metaOption : metaOptions) {
if (!metaOption.matches(meta)) {
return false;
}
}
return true;
}
public boolean isEmpty() {
return baseOptions.isEmpty() && metaOptions.isEmpty();
}
}
@@ -0,0 +1,45 @@
/*
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.search.match;
import com.lishid.openinv.util.MessagePart;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class MatchResult {
public static final MatchResult NO_MATCH = new MatchResult();
private final MessagePart[] result;
private MatchResult() {
result = null;
}
public MatchResult(MessagePart @NotNull [] result) {
this.result = result;
}
public boolean isMatch() {
return result != null;
}
public MessagePart @Nullable [] getMatch() {
return result;
}
}
@@ -0,0 +1,27 @@
/*
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.search.match;
import org.jetbrains.annotations.NotNull;
public interface Matchable {
Matchable EMPTY = matcher -> MatchResult.NO_MATCH;
@NotNull MatchResult match(@NotNull ItemMatcher matcher);
}
@@ -0,0 +1,26 @@
/*
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.search.match;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
public interface MatchableItem {
boolean matches(@NotNull ItemStack itemStack);
}
@@ -0,0 +1,26 @@
/*
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.search.match;
import org.bukkit.inventory.meta.ItemMeta;
import org.jetbrains.annotations.NotNull;
public interface MatchableMeta {
boolean matches(@NotNull ItemMeta itemMeta);
}
@@ -60,6 +60,9 @@ public class ConfigUpdater {
if (version < 4) {
updateConfig3To4();
}
if (version < 5) {
updateConfig4To5();
}
plugin.getServer().getScheduler().runTask(plugin, () -> {
plugin.saveConfig();
@@ -68,6 +71,14 @@ public class ConfigUpdater {
});
}
private void updateConfig4To5() {
plugin.getServer().getScheduler().runTask(plugin, () -> {
plugin.getConfig().set("settings.search.polls-per-tick", 5);
plugin.getConfig().set("settings.search.max-results", 40);
plugin.getConfig().set("config-version", 5);
});
}
private void updateConfig3To4() {
plugin.getServer().getScheduler().runTask(plugin, () -> {
plugin.getConfig().set("notify", null);
@@ -0,0 +1,80 @@
/*
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.util;
import java.util.List;
import java.util.function.Supplier;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.ClickEvent;
import net.md_5.bungee.api.chat.TextComponent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class MessagePart {
private final Supplier<BaseComponent> playerProvider;
private final Supplier<String> consoleProvider;
private @Nullable BaseComponent playerComponent;
private @Nullable String consoleComponent;
public MessagePart(@NotNull Supplier<@NotNull BaseComponent> playerProvider) {
this(playerProvider, () -> unwrapComponent(playerProvider.get()));
}
public MessagePart(
@NotNull Supplier<@NotNull BaseComponent> playerProvider,
@NotNull Supplier<@NotNull String> consoleProvider) {
this.playerProvider = playerProvider;
this.consoleProvider = consoleProvider;
}
public BaseComponent forPlayer() {
if (this.playerComponent == null) {
this.playerComponent = playerProvider.get();
}
return this.playerComponent;
}
public String forConsole() {
if (consoleComponent == null) {
consoleComponent = consoleProvider.get();
}
return consoleComponent;
}
private static String unwrapComponent(BaseComponent component) {
StringBuilder builder = new StringBuilder();
ClickEvent clickEvent = component.getClickEvent();
if (clickEvent != null) {
// Prefer click events, i.e. "Check out our GitHub" with a link -> raw link
builder.append(clickEvent.getValue());
} else if (component instanceof TextComponent) {
builder.append(((TextComponent) component).getText());
}
List<BaseComponent> extra = component.getExtra();
if (extra != null) {
for (BaseComponent extraComponent : extra) {
builder.append(unwrapComponent(extraComponent));
}
}
return builder.toString();
}
}
@@ -31,6 +31,10 @@ public enum Permissions {
ENDERCHEST("openender"),
ENDERCHEST_ALL("openenderall"),
SEARCH("search"),
SEARCH_PLAYERS_ONLINE("search.players.online"),
SEARCH_PLAYERS_OFFLINE("search.players.offline"),
SEARCH_CHUNKS_LOADED("search.chunks.loaded"),
SEARCH_CHUNKS_UNLOADED("search.chunks.unloaded"),
EDITINV("editinv"),
EDITENDER("editender"),
OPENSELF("openself"),
@@ -0,0 +1,181 @@
/*
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.util;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.jetbrains.annotations.NotNull;
/**
* A data container for JSON-esque expressions.
*
* <p>Ex: A ChunkBucket can be expressed <code>chunks{world:world_name,x:0,z:0,radius:10,load:true}</code>. The
* starting "chunks" identifies it as a ChunkBucket and the content in braces contains the construction detail.
* However, a simpler detail, such as a MatchableItem, may be specified <code>type:IRON_SWORD</code> where "type"
* is both the identifier of the MatchableItem and the key of the sole value.
*
* <p>A best-effort will be made to match other start and finish demarcation techniques.
*/
public class PseudoJson {
private static final char[] BRACE_OPEN = new char[] { '{', '[', '(' };
private static final char[] BRACE_CLOSE = new char[] { '}', ']', ')' };
private static final Set<Character> RESERVED_CHARACTERS = new HashSet<>();
static {
for (char brace : BRACE_OPEN) {
RESERVED_CHARACTERS.add(brace);
}
for (char brace : BRACE_CLOSE) {
RESERVED_CHARACTERS.add(brace);
}
RESERVED_CHARACTERS.add(',');
RESERVED_CHARACTERS.add(':');
}
private final String identifier;
private final Map<String, String> mappings;
public PseudoJson(@NotNull String identifier) {
this(identifier, new HashMap<>());
}
public PseudoJson(@NotNull String identifier, @NotNull Map<String, String> mappings) {
this.identifier = identifier;
mappings.forEach(PseudoJson::validate);
this.mappings = mappings;
}
public @NotNull String getIdentifier() {
return identifier;
}
public @NotNull Optional<String> put(@NotNull String key, @NotNull String value) {
validate(key, value);
return Optional.ofNullable(this.mappings.put(key, value));
}
public @NotNull Optional<String> get(@NotNull String key) {
return Optional.ofNullable(this.mappings.get(key));
}
public @NotNull Map<String, String> getMappings() {
return Collections.unmodifiableMap(mappings);
}
public Optional<String> remove(@NotNull String key) {
return Optional.ofNullable(this.mappings.get(key));
}
public String asString() {
StringBuilder builder = new StringBuilder(identifier);
builder.append('{');
for (Iterator<Map.Entry<String, String>> iterator = mappings.entrySet().iterator(); iterator.hasNext();) {
Map.Entry<String, String> entry = iterator.next();
builder.append(entry.getKey()).append(':').append(entry.getValue());
if (iterator.hasNext()) {
builder.append(',');
}
}
builder.append('}');
return builder.toString();
}
@Override
public String toString() {
return asString();
}
private static void validate(String key, String value) {
if (RESERVED_CHARACTERS.stream().anyMatch(character -> key.indexOf(character) >= 0 || value.indexOf(character) >= 0)) {
throw new IllegalArgumentException(String.format("Primitive pseudo-JSON reserves characters %s", RESERVED_CHARACTERS));
}
}
public static PseudoJson fromString(String pseudoJson) {
int open = -1;
for (char brace : BRACE_OPEN) {
open = pseudoJson.indexOf(brace);
if (open >= 0) {
break;
}
}
String identifier = getIdentifier(pseudoJson, open);
int close = -1;
for (char brace : BRACE_CLOSE) {
close = pseudoJson.lastIndexOf(brace);
if (close >= 0) {
break;
}
}
if (open >= 0) {
if (close <= open) {
// Ignore bad ending brace.
pseudoJson = pseudoJson.substring(open + 1);
} else {
// Use only bracketed content
pseudoJson = pseudoJson.substring(open + 1, close);
}
}
return new PseudoJson(identifier, getMappings(pseudoJson));
}
private static String getIdentifier(String pseudoJson, int braceOpen) {
int identifierEnd;
if (braceOpen >= 0) {
identifierEnd = braceOpen;
} else {
// No quote, use first mapping.
identifierEnd = pseudoJson.indexOf(':');
if (identifierEnd == -1) {
// No mappings, use whole string.
return pseudoJson;
}
}
return pseudoJson.substring(0, identifierEnd);
}
private static Map<String, String> getMappings(String pseudoJson) {
String[] mappings = pseudoJson.split(",");
Map<String, String> data = new HashMap<>();
for (String mapping : mappings) {
String[] datum = mapping.split(":");
if (datum.length >= 2) {
data.put(datum[0], datum[1]);
}
}
return data;
}
}
@@ -22,9 +22,12 @@ import java.util.List;
import java.util.Locale;
import java.util.function.Function;
import org.bukkit.Bukkit;
import org.bukkit.Keyed;
import org.bukkit.Material;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.util.StringUtil;
import org.jetbrains.annotations.NotNull;
/**
* Utility class for common tab completions.
@@ -37,7 +40,7 @@ public class TabCompleter {
* @param argument the argument to complete
* @return integer options
*/
public static List<String> completeInteger(String argument) {
public static List<String> completeInteger(@NotNull String argument) {
// Ensure existing argument is actually a number
if (!argument.isEmpty()) {
try {
@@ -62,14 +65,46 @@ public class TabCompleter {
* @param enumClazz the Enum to complete for
* @return the matching Enum values
*/
public static List<String> completeEnum(String argument, Class<? extends Enum<?>> enumClazz) {
public static List<String> completeEnum(@NotNull String argument, @NotNull Class<? extends Enum<?>> enumClazz) {
return completeObject(argument, Enum::name, enumClazz.getEnumConstants());
}
/**
* Offer tab completions for {@link Material Materials}.
*
* @param argument the argument to complete
* @return the matching {@code Material} keys
*/
public static List<String> completeMaterial(@NotNull String argument) {
argument = argument.toLowerCase(Locale.ENGLISH);
List<String> completions = new ArrayList<>();
for (Enum<?> enumConstant : enumClazz.getEnumConstants()) {
String name = enumConstant.name().toLowerCase();
if (name.startsWith(argument)) {
completions.add(name);
for (Material material : Material.values()) {
if (material.name().toLowerCase(Locale.ENGLISH).startsWith(argument)
|| material.getKey().toString().startsWith(argument)
|| material.getKey().getKey().startsWith(argument)) {
completions.add(material.getKey().toString());
}
}
return completions;
}
/**
* Offer tab completions for {@link Keyed} values.
*
* @param argument the argument to complete
* @param options the {@code Keyed} values
* @return the matching keys
*/
public static List<String> completeKeyed(@NotNull String argument, @NotNull Keyed @NotNull [] options) {
argument = argument.toLowerCase(Locale.ENGLISH);
List<String> completions = new ArrayList<>();
for (Keyed keyed : options) {
if (keyed.getKey().toString().startsWith(argument)
|| keyed.getKey().getKey().startsWith(argument)) {
completions.add(keyed.getKey().toString());
}
}
@@ -83,7 +118,7 @@ public class TabCompleter {
* @param options the Strings which may be completed
* @return the matching Strings
*/
public static List<String> completeString(String argument, String[] options) {
public static List<String> completeString(@NotNull String argument, @NotNull String @NotNull [] options) {
argument = argument.toLowerCase(Locale.ENGLISH);
List<String> completions = new ArrayList<>();
@@ -103,7 +138,7 @@ public class TabCompleter {
* @param argument the argument to complete
* @return the matching Players' names
*/
public static List<String> completeOnlinePlayer(CommandSender sender, String argument) {
public static List<String> completeOnlinePlayer(@NotNull CommandSender sender, String argument) {
List<String> completions = new ArrayList<>();
Player senderPlayer = sender instanceof Player ? (Player) sender : null;
@@ -128,12 +163,12 @@ public class TabCompleter {
* @param options the Objects which may be completed
* @return the matching Strings
*/
public static <T> List<String> completeObject(String argument, Function<T, String> converter, T[] options) {
public static <T> List<String> completeObject(@NotNull String argument, @NotNull Function<T, String> converter, @NotNull T @NotNull [] options) {
argument = argument.toLowerCase(Locale.ENGLISH);
List<String> completions = new ArrayList<>();
for (T option : options) {
String optionString = converter.apply(option).toLowerCase();
String optionString = converter.apply(option).toLowerCase(Locale.ENGLISH);
if (optionString.startsWith(argument)) {
completions.add(optionString);
}
+4 -1
View File
@@ -1,4 +1,7 @@
config-version: 4
config-version: 5
settings:
search:
polls-per-tick: 5
max-results: 40
disable-saving: false
locale: 'en_us'
+9 -6
View File
@@ -2,6 +2,11 @@ messages:
error:
consoleUnsupported: 'You cannot use this command from console.'
lootNotGenerated: '&cLoot not generated! Please disable &b/silentcontainer&c.'
search:
invalidSchema: '&cUnable to parse "%value%" to determine target. Try using tab completion!'
invalidInventory: '&cUnable to determine target inventories. Try using tab completion!'
invalidMatcher: '&cUnable to determine target item parameters. Try using tab completion!'
activeSearch: '&cPlease wait for your search to complete before starting another.'
invalidMaterial: '&cInvalid material: "%target%"'
invalidNumber: '&cInvalid number: "%target%"'
invalidPlayer: '&cPlayer not found!'
@@ -17,12 +22,10 @@ messages:
containerBlockedSilent: 'You are opening a blocked container silently.'
containerSilent: 'You are opening a container silently.'
settingState: '%setting%: %state%'
player:
noMatches: 'No players found with %target%.'
matches: 'Players holding %target%: %detail%'
container:
noMatches: 'No containers found with %target%.'
matches: 'Containers holding %target%: %detail%'
search:
progress: 'Searching... %progress%% | Results: %matches%'
noMatches: 'No items found with search parameters.'
matches: 'Locations of items matching parameters: '
on: 'on'
off: 'off'
container:
+4 -15
View File
@@ -52,12 +52,12 @@ commands:
permission: OpenInv.openender
usage: |-
/<command> [Player] - Open a player's enderchest
searchinv:
aliases: [si]
description: Search and list players having a specific item
search:
aliases: [ searchinv, si, searchender, se, searchenchant, searchenchants, searchcontainer, searchchest ]
description: Search inventories for a matching item
permission: OpenInv.search
usage: |-
/<command> <Material> [MinAmount] - MinAmount is optional, the minimum amount required
/<command> <inventories> <item details>
searchender:
aliases: [se]
permission: OpenInv.search
@@ -76,14 +76,3 @@ commands:
permission: OpenInv.anychest
usage: |-
/<command> [check|on|off] - Check, toggle, or set AnyContainer
searchenchant:
aliases: [searchenchants]
description: Search and list players with a specific enchantment.
permission: OpenInv.searchenchant
usage: |-
/<command> <[Enchantment] [MinLevel]> - Enchantment is the enchantment type, MinLevel is the minimum level. One is optional
searchcontainer:
aliases: [searchchest]
description: Search and list containers with a specific material.
permission: OpenInv.searchcontainer
usage: /<command> <Material> [ChunkRadius] - ChunkRadius is optional, the length that will be searched for matching items. Default 5
+32 -5
View File
@@ -27,8 +27,8 @@
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.source>16</maven.compiler.source>
<maven.compiler.target>16</maven.compiler.target>
</properties>
<profiles>
@@ -61,6 +61,14 @@
<id>spigot-repo</id>
<url>https://hub.spigotmc.org/nexus/content/groups/public/</url>
</repository>
<repository>
<id>papermc</id>
<url>https://papermc.io/repo/repository/maven-public/</url>
</repository>
<repository>
<id>jitpack</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependencyManagement>
@@ -69,13 +77,25 @@
<artifactId>annotations</artifactId>
<groupId>org.jetbrains</groupId>
<scope>provided</scope>
<version>21.0.1</version>
<version>23.0.0</version>
</dependency>
<dependency>
<artifactId>paper-api</artifactId>
<groupId>io.papermc.paper</groupId>
<scope>provided</scope>
<version>1.17.1-R0.1-SNAPSHOT</version>
</dependency>
<dependency>
<artifactId>spigot-api</artifactId>
<groupId>org.spigotmc</groupId>
<scope>provided</scope>
<version>1.16.5-R0.1-SNAPSHOT</version>
<version>1.17.1-R0.1-SNAPSHOT</version>
</dependency>
<dependency>
<artifactId>planarwrappers</artifactId>
<groupId>com.github.jikoo</groupId>
<scope>compile</scope>
<version>2.2.4</version>
</dependency>
<dependency>
<artifactId>openinvapi</artifactId>
@@ -92,6 +112,13 @@
</dependencies>
</dependencyManagement>
<pluginRepositories>
<pluginRepository>
<id>apache-snapshot-repo</id>
<url>https://repository.apache.org/content/repositories/snapshots/</url>
</pluginRepository>
</pluginRepositories>
<build>
<pluginManagement>
<plugins>
@@ -125,7 +152,7 @@
</execution>
</executions>
<groupId>org.apache.maven.plugins</groupId>
<version>3.2.4</version>
<version>3.3.1-SNAPSHOT</version>
</plugin>
<plugin>