001/* 002 * PlotSquared, a land and world management plugin for Minecraft. 003 * Copyright (C) IntellectualSites <https://intellectualsites.com> 004 * Copyright (C) IntellectualSites team and contributors 005 * 006 * This program is free software: you can redistribute it and/or modify 007 * it under the terms of the GNU General Public License as published by 008 * the Free Software Foundation, either version 3 of the License, or 009 * (at your option) any later version. 010 * 011 * This program is distributed in the hope that it will be useful, 012 * but WITHOUT ANY WARRANTY; without even the implied warranty of 013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 014 * GNU General Public License for more details. 015 * 016 * You should have received a copy of the GNU General Public License 017 * along with this program. If not, see <https://www.gnu.org/licenses/>. 018 */ 019package com.plotsquared.core.util; 020 021import com.google.common.cache.Cache; 022import com.google.common.cache.CacheBuilder; 023import com.plotsquared.core.PlotSquared; 024import com.plotsquared.core.command.Command; 025import com.plotsquared.core.command.CommandCategory; 026import com.plotsquared.core.command.RequiredType; 027import com.plotsquared.core.configuration.Settings; 028import com.plotsquared.core.player.ConsolePlayer; 029import com.plotsquared.core.player.PlotPlayer; 030import com.plotsquared.core.plot.Plot; 031import com.plotsquared.core.plot.PlotArea; 032import com.plotsquared.core.uuid.UUIDMapping; 033import org.checkerframework.checker.nullness.qual.NonNull; 034 035import java.util.ArrayList; 036import java.util.Arrays; 037import java.util.Collection; 038import java.util.Collections; 039import java.util.List; 040import java.util.Locale; 041import java.util.UUID; 042import java.util.concurrent.TimeUnit; 043import java.util.function.Predicate; 044import java.util.stream.Collectors; 045 046/** 047 * Tab completion utilities 048 */ 049public final class TabCompletions { 050 051 private static final Cache<String, List<String>> cachedCompletionValues = 052 CacheBuilder.newBuilder() 053 .expireAfterWrite(Settings.Tab_Completions.CACHE_EXPIRATION, TimeUnit.SECONDS) 054 .build(); 055 056 private static final Command booleanTrueCompletion = new Command(null, false, "true", "", 057 RequiredType.NONE, null 058 ) { 059 }; 060 private static final Command booleanFalseCompletion = new Command(null, false, "false", "", 061 RequiredType.NONE, null 062 ) { 063 }; 064 065 private TabCompletions() { 066 throw new UnsupportedOperationException( 067 "This is a utility class and cannot be instantiated"); 068 } 069 070 /** 071 * Get a list of tab completions corresponding to player names. This uses the UUID pipeline 072 * cache, so it will complete will all names known to PlotSquared 073 * 074 * @param input Command input 075 * @param existing Players that should not be included in completions 076 * @return List of completions 077 * @deprecated In favor {@link #completePlayers(PlotPlayer, String, List)} 078 */ 079 @Deprecated(forRemoval = true, since = "6.1.3") 080 public static @NonNull List<Command> completePlayers( 081 final @NonNull String input, 082 final @NonNull List<String> existing 083 ) { 084 return completePlayers(ConsolePlayer.getConsole(), input, existing); 085 } 086 087 /** 088 * Get a list of tab completions corresponding to player names. This uses the UUID pipeline 089 * cache, so it will complete will all names known to PlotSquared 090 * 091 * @param input Command input 092 * @param issuer The player who issued the tab completion 093 * @param existing Players that should not be included in completions 094 * @return List of completions 095 * @since 6.1.3 096 */ 097 public static @NonNull List<Command> completePlayers( 098 final @NonNull PlotPlayer<?> issuer, 099 final @NonNull String input, 100 final @NonNull List<String> existing 101 ) { 102 return completePlayers("players", issuer, input, existing, uuid -> true); 103 } 104 105 /** 106 * Get a list of tab completions corresponding to player names added to the given plot. 107 * 108 * @param plot Plot to complete added players for 109 * @param input Command input 110 * @param existing Players that should not be included in completions 111 * @return List of completions 112 * 113 * @deprecated In favor {@link #completeAddedPlayers(PlotPlayer, Plot, String, List)} 114 */ 115 @Deprecated(forRemoval = true, since = "6.1.3") 116 public static @NonNull List<Command> completeAddedPlayers( 117 final @NonNull Plot plot, 118 final @NonNull String input, final @NonNull List<String> existing 119 ) { 120 return completeAddedPlayers(ConsolePlayer.getConsole(), plot, input, existing); 121 } 122 123 /** 124 * Get a list of tab completions corresponding to player names added to the given plot. 125 * 126 * @param issuer The player who issued the tab completion 127 * @param plot Plot to complete added players for 128 * @param input Command input 129 * @param existing Players that should not be included in completions 130 * @return List of completions 131 * @since 6.1.3 132 */ 133 public static @NonNull List<Command> completeAddedPlayers( 134 final @NonNull PlotPlayer<?> issuer, 135 final @NonNull Plot plot, 136 final @NonNull String input, final @NonNull List<String> existing 137 ) { 138 return completePlayers("added" + plot, issuer, input, existing, 139 uuid -> plot.getMembers().contains(uuid) 140 || plot.getTrusted().contains(uuid) 141 || plot.getDenied().contains(uuid) 142 ); 143 } 144 145 public static @NonNull List<Command> completePlayersInPlot( 146 final @NonNull Plot plot, 147 final @NonNull String input, final @NonNull List<String> existing 148 ) { 149 List<String> players = cachedCompletionValues.getIfPresent("inPlot" + plot); 150 if (players == null) { 151 final List<PlotPlayer<?>> inPlot = plot.getPlayersInPlot(); 152 players = new ArrayList<>(inPlot.size()); 153 for (PlotPlayer<?> player : inPlot) { 154 players.add(player.getName()); 155 } 156 cachedCompletionValues.put("inPlot" + plot, players); 157 } 158 return filterCached(players, input, existing); 159 } 160 161 /** 162 * Get a list of completions corresponding to WorldEdit(/FastAsyncWorldEdit) patterns. This uses 163 * WorldEdit's pattern completer internally. 164 * 165 * @param input Command input 166 * @return List of completions 167 */ 168 public static @NonNull List<Command> completePatterns(final @NonNull String input) { 169 return PatternUtil.getSuggestions(input.trim()).stream() 170 .map(value -> value.toLowerCase(Locale.ENGLISH).replace("minecraft:", "")) 171 .filter(value -> value.startsWith(input.toLowerCase(Locale.ENGLISH))) 172 .map(value -> new Command(null, false, value, "", RequiredType.NONE, null) { 173 }).collect(Collectors.toList()); 174 } 175 176 public static @NonNull List<Command> completeBoolean(final @NonNull String input) { 177 if (input.isEmpty()) { 178 return Arrays.asList(booleanTrueCompletion, booleanFalseCompletion); 179 } 180 if ("true".startsWith(input)) { 181 return Collections.singletonList(booleanTrueCompletion); 182 } 183 if ("false".startsWith(input)) { 184 return Collections.singletonList(booleanFalseCompletion); 185 } 186 return Collections.emptyList(); 187 } 188 189 /** 190 * Get a list of integer numbers matching the given input. If the input string 191 * is empty, nothing will be returned. The list is unmodifiable. 192 * 193 * @param input Input to filter with 194 * @param amountLimit Maximum amount of suggestions 195 * @param highestLimit Highest number to include 196 * @return Unmodifiable list of number completions 197 */ 198 public static @NonNull List<Command> completeNumbers( 199 final @NonNull String input, 200 final int amountLimit, final int highestLimit 201 ) { 202 if (input.isEmpty() || input.length() > highestLimit || !MathMan.isInteger(input)) { 203 return Collections.emptyList(); 204 } 205 int offset; 206 try { 207 offset = Integer.parseInt(input) * 10; 208 } catch (NumberFormatException ignored) { 209 return Collections.emptyList(); 210 } 211 final List<String> commands = new ArrayList<>(); 212 for (int i = offset; i < highestLimit && (offset - i + amountLimit) > 0; i++) { 213 commands.add(String.valueOf(i)); 214 } 215 return asCompletions(commands.toArray(new String[0])); 216 } 217 218 /** 219 * Get a list of plot areas matching the given input. 220 * The list is unmodifiable. 221 * 222 * @param input Input to filter with 223 * @return Unmodifiable list of area completions 224 */ 225 public static @NonNull List<Command> completeAreas(final @NonNull String input) { 226 final List<Command> completions = new ArrayList<>(); 227 for (final PlotArea area : PlotSquared.get().getPlotAreaManager().getAllPlotAreas()) { 228 String areaName = area.getWorldName(); 229 if (area.getId() != null) { 230 areaName += ";" + area.getId(); 231 } 232 if (!areaName.toLowerCase().startsWith(input.toLowerCase())) { 233 continue; 234 } 235 completions.add(new Command(null, false, areaName, "", 236 RequiredType.NONE, null 237 ) { 238 }); 239 } 240 return Collections.unmodifiableList(completions); 241 } 242 243 public static @NonNull List<Command> asCompletions(String... toFilter) { 244 final List<Command> completions = new ArrayList<>(); 245 for (String completion : toFilter) { 246 completions.add(new Command(null, false, completion, "", 247 RequiredType.NONE, null 248 ) { 249 }); 250 } 251 return Collections.unmodifiableList(completions); 252 } 253 254 /** 255 * @param cacheIdentifier Cache key 256 * @param input Command input 257 * @param existing Players that should not be included in completions 258 * @param uuidFilter Filter applied before caching values 259 * @return List of completions 260 * @deprecated In favor {@link #completePlayers(String, PlotPlayer, String, List, Predicate)} 261 */ 262 @SuppressWarnings("unused") 263 @Deprecated(forRemoval = true, since = "6.1.3") 264 private static List<Command> completePlayers( 265 final @NonNull String cacheIdentifier, 266 final @NonNull String input, final @NonNull List<String> existing, 267 final @NonNull Predicate<UUID> uuidFilter 268 ) { 269 return completePlayers(cacheIdentifier, ConsolePlayer.getConsole(), input, existing, uuidFilter); 270 } 271 272 /** 273 * @param cacheIdentifier Cache key 274 * @param issuer The player who issued the tab completion 275 * @param input Command input 276 * @param existing Players that should not be included in completions 277 * @param uuidFilter Filter applied before caching values 278 * @return List of completions 279 * @since 6.1.3 280 */ 281 private static List<Command> completePlayers( 282 final @NonNull String cacheIdentifier, 283 final @NonNull PlotPlayer<?> issuer, 284 final @NonNull String input, final @NonNull List<String> existing, 285 final @NonNull Predicate<UUID> uuidFilter 286 ) { 287 List<String> players; 288 if (Settings.Enabled_Components.EXTENDED_USERNAME_COMPLETION) { 289 players = cachedCompletionValues.getIfPresent(cacheIdentifier); 290 if (players == null) { 291 final Collection<UUIDMapping> mappings = 292 PlotSquared.get().getImpromptuUUIDPipeline().getAllImmediately(); 293 players = new ArrayList<>(mappings.size()); 294 for (final UUIDMapping mapping : mappings) { 295 if (uuidFilter.test(mapping.getUuid())) { 296 players.add(mapping.getUsername()); 297 } 298 } 299 cachedCompletionValues.put(cacheIdentifier, players); 300 } 301 } else { 302 final Collection<? extends PlotPlayer<?>> onlinePlayers = PlotSquared.platform().playerManager().getPlayers(); 303 players = new ArrayList<>(onlinePlayers.size()); 304 for (final PlotPlayer<?> player : onlinePlayers) { 305 if (!uuidFilter.test(player.getUUID())) { 306 continue; 307 } 308 if (issuer != null && !issuer.canSee(player)) { 309 continue; 310 } 311 players.add(player.getName()); 312 } 313 } 314 return filterCached(players, input, existing); 315 } 316 317 private static List<Command> filterCached( 318 Collection<String> playerNames, String input, 319 List<String> existing 320 ) { 321 final String processedInput = input.toLowerCase(Locale.ENGLISH); 322 return playerNames.stream().filter(player -> player.toLowerCase(Locale.ENGLISH).startsWith(processedInput)) 323 .filter(player -> !existing.contains(player)).map( 324 player -> new Command(null, false, player, "", RequiredType.NONE, 325 CommandCategory.INFO 326 ) { 327 }) 328 /* If there are more than 200 suggestions, just send the first 200 */ 329 .limit(200) 330 .collect(Collectors.toList()); 331 } 332 333}