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.PlotPlayer; 029import com.plotsquared.core.plot.Plot; 030import com.plotsquared.core.plot.PlotArea; 031import com.plotsquared.core.uuid.UUIDMapping; 032import org.checkerframework.checker.nullness.qual.NonNull; 033 034import java.util.ArrayList; 035import java.util.Arrays; 036import java.util.Collection; 037import java.util.Collections; 038import java.util.List; 039import java.util.Locale; 040import java.util.UUID; 041import java.util.concurrent.TimeUnit; 042import java.util.function.Predicate; 043import java.util.stream.Collectors; 044 045/** 046 * Tab completion utilities 047 */ 048public final class TabCompletions { 049 050 private static final Cache<String, List<String>> cachedCompletionValues = 051 CacheBuilder.newBuilder() 052 .expireAfterWrite(Settings.Tab_Completions.CACHE_EXPIRATION, TimeUnit.SECONDS) 053 .build(); 054 055 private static final Command booleanTrueCompletion = new Command(null, false, "true", "", 056 RequiredType.NONE, null 057 ) { 058 }; 059 private static final Command booleanFalseCompletion = new Command(null, false, "false", "", 060 RequiredType.NONE, null 061 ) { 062 }; 063 064 private TabCompletions() { 065 throw new UnsupportedOperationException( 066 "This is a utility class and cannot be instantiated"); 067 } 068 069 /** 070 * Get a list of tab completions corresponding to player names. This uses the UUID pipeline 071 * cache, so it will complete will all names known to PlotSquared 072 * 073 * @param input Command input 074 * @param issuer The player who issued the tab completion 075 * @param existing Players that should not be included in completions 076 * @return List of completions 077 * @since 6.1.3 078 */ 079 public static @NonNull List<Command> completePlayers( 080 final @NonNull PlotPlayer<?> issuer, 081 final @NonNull String input, 082 final @NonNull List<String> existing 083 ) { 084 return completePlayers("players", issuer, input, existing, uuid -> true); 085 } 086 087 /** 088 * Get a list of tab completions corresponding to player names added to the given plot. 089 * 090 * @param issuer The player who issued the tab completion 091 * @param plot Plot to complete added players for 092 * @param input Command input 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> completeAddedPlayers( 098 final @NonNull PlotPlayer<?> issuer, 099 final @NonNull Plot plot, 100 final @NonNull String input, final @NonNull List<String> existing 101 ) { 102 return completePlayers("added" + plot, issuer, input, existing, 103 uuid -> plot.getMembers().contains(uuid) 104 || plot.getTrusted().contains(uuid) 105 || plot.getDenied().contains(uuid) 106 ); 107 } 108 109 public static @NonNull List<Command> completePlayersInPlot( 110 final @NonNull PlotPlayer<?> issuer, 111 final @NonNull Plot plot, 112 final @NonNull String input, final @NonNull List<String> existing 113 ) { 114 List<String> players = cachedCompletionValues.getIfPresent("inPlot" + plot); 115 if (players == null) { 116 final List<PlotPlayer<?>> inPlot = plot.getPlayersInPlot(); 117 players = new ArrayList<>(inPlot.size()); 118 for (PlotPlayer<?> player : inPlot) { 119 if (issuer.canSee(player)) { 120 players.add(player.getName()); 121 } 122 } 123 cachedCompletionValues.put("inPlot" + plot, players); 124 } 125 return filterCached(players, input, existing); 126 } 127 128 /** 129 * Get a list of completions corresponding to WorldEdit(/FastAsyncWorldEdit) patterns. This uses 130 * WorldEdit's pattern completer internally. 131 * 132 * @param input Command input 133 * @return List of completions 134 */ 135 public static @NonNull List<Command> completePatterns(final @NonNull String input) { 136 return PatternUtil.getSuggestions(input.trim()).stream() 137 .map(value -> value.toLowerCase(Locale.ENGLISH).replace("minecraft:", "")) 138 .filter(value -> value.startsWith(input.toLowerCase(Locale.ENGLISH))) 139 .map(value -> new Command(null, false, value, "", RequiredType.NONE, null) { 140 }).collect(Collectors.toList()); 141 } 142 143 public static @NonNull List<Command> completeBoolean(final @NonNull String input) { 144 if (input.isEmpty()) { 145 return Arrays.asList(booleanTrueCompletion, booleanFalseCompletion); 146 } 147 if ("true".startsWith(input)) { 148 return Collections.singletonList(booleanTrueCompletion); 149 } 150 if ("false".startsWith(input)) { 151 return Collections.singletonList(booleanFalseCompletion); 152 } 153 return Collections.emptyList(); 154 } 155 156 /** 157 * Get a list of integer numbers matching the given input. If the input string 158 * is empty, nothing will be returned. The list is unmodifiable. 159 * 160 * @param input Input to filter with 161 * @param amountLimit Maximum amount of suggestions 162 * @param highestLimit Highest number to include 163 * @return Unmodifiable list of number completions 164 */ 165 public static @NonNull List<Command> completeNumbers( 166 final @NonNull String input, 167 final int amountLimit, final int highestLimit 168 ) { 169 if (input.isEmpty() || input.length() > highestLimit || !MathMan.isInteger(input)) { 170 return Collections.emptyList(); 171 } 172 int offset; 173 try { 174 offset = Integer.parseInt(input) * 10; 175 } catch (NumberFormatException ignored) { 176 return Collections.emptyList(); 177 } 178 final List<String> commands = new ArrayList<>(); 179 for (int i = offset; i <= highestLimit && (offset - i + amountLimit) > 0; i++) { 180 commands.add(String.valueOf(i)); 181 } 182 return asCompletions(commands.toArray(new String[0])); 183 } 184 185 /** 186 * Get a list of plot areas matching the given input. 187 * The list is unmodifiable. 188 * 189 * @param input Input to filter with 190 * @return Unmodifiable list of area completions 191 */ 192 public static @NonNull List<Command> completeAreas(final @NonNull String input) { 193 final List<Command> completions = new ArrayList<>(); 194 for (final PlotArea area : PlotSquared.get().getPlotAreaManager().getAllPlotAreas()) { 195 String areaName = area.getWorldName(); 196 if (area.getId() != null) { 197 areaName += ";" + area.getId(); 198 } 199 if (!areaName.toLowerCase().startsWith(input.toLowerCase())) { 200 continue; 201 } 202 completions.add(new Command(null, false, areaName, "", 203 RequiredType.NONE, null 204 ) { 205 }); 206 } 207 return Collections.unmodifiableList(completions); 208 } 209 210 public static @NonNull List<Command> asCompletions(String... toFilter) { 211 final List<Command> completions = new ArrayList<>(); 212 for (String completion : toFilter) { 213 completions.add(new Command(null, false, completion, "", 214 RequiredType.NONE, null 215 ) { 216 }); 217 } 218 return Collections.unmodifiableList(completions); 219 } 220 221 /** 222 * @param cacheIdentifier Cache key 223 * @param issuer The player who issued the tab completion 224 * @param input Command input 225 * @param existing Players that should not be included in completions 226 * @param uuidFilter Filter applied before caching values 227 * @return List of completions 228 * @since 6.1.3 229 */ 230 private static List<Command> completePlayers( 231 final @NonNull String cacheIdentifier, 232 final @NonNull PlotPlayer<?> issuer, 233 final @NonNull String input, final @NonNull List<String> existing, 234 final @NonNull Predicate<UUID> uuidFilter 235 ) { 236 List<String> players; 237 if (Settings.Enabled_Components.EXTENDED_USERNAME_COMPLETION) { 238 players = cachedCompletionValues.getIfPresent(cacheIdentifier); 239 if (players == null) { 240 final Collection<UUIDMapping> mappings = 241 PlotSquared.get().getImpromptuUUIDPipeline().getAllImmediately(); 242 players = new ArrayList<>(mappings.size()); 243 for (final UUIDMapping mapping : mappings) { 244 if (uuidFilter.test(mapping.uuid())) { 245 players.add(mapping.username()); 246 } 247 } 248 cachedCompletionValues.put(cacheIdentifier, players); 249 } 250 } else { 251 final Collection<? extends PlotPlayer<?>> onlinePlayers = PlotSquared.platform().playerManager().getPlayers(); 252 players = new ArrayList<>(onlinePlayers.size()); 253 for (final PlotPlayer<?> player : onlinePlayers) { 254 if (!uuidFilter.test(player.getUUID())) { 255 continue; 256 } 257 if (issuer != null && !issuer.canSee(player)) { 258 continue; 259 } 260 players.add(player.getName()); 261 } 262 } 263 return filterCached(players, input, existing); 264 } 265 266 private static List<Command> filterCached( 267 Collection<String> playerNames, String input, 268 List<String> existing 269 ) { 270 final String processedInput = input.toLowerCase(Locale.ENGLISH); 271 return playerNames.stream().filter(player -> player.toLowerCase(Locale.ENGLISH).startsWith(processedInput)) 272 .filter(player -> !existing.contains(player)).map( 273 player -> new Command(null, false, player, "", RequiredType.NONE, 274 CommandCategory.INFO 275 ) { 276 }) 277 /* If there are more than 200 suggestions, just send the first 200 */ 278 .limit(200) 279 .collect(Collectors.toList()); 280 } 281 282}