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.plotsquared.core.PlotSquared; 022import com.plotsquared.core.configuration.Settings; 023import com.plotsquared.core.configuration.caption.Caption; 024import com.plotsquared.core.configuration.caption.LocaleHolder; 025import com.plotsquared.core.configuration.caption.StaticCaption; 026import com.plotsquared.core.configuration.caption.TranslatableCaption; 027import com.plotsquared.core.database.DBFunc; 028import com.plotsquared.core.player.ConsolePlayer; 029import com.plotsquared.core.player.OfflinePlotPlayer; 030import com.plotsquared.core.player.PlotPlayer; 031import com.plotsquared.core.plot.Plot; 032import com.plotsquared.core.uuid.UUIDMapping; 033import net.kyori.adventure.text.Component; 034import net.kyori.adventure.text.ComponentLike; 035import net.kyori.adventure.text.TextComponent; 036import net.kyori.adventure.text.minimessage.MiniMessage; 037import net.kyori.adventure.text.minimessage.tag.Tag; 038import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; 039import org.checkerframework.checker.nullness.qual.NonNull; 040import org.checkerframework.checker.nullness.qual.Nullable; 041import org.jetbrains.annotations.Contract; 042 043import java.util.ArrayList; 044import java.util.Collection; 045import java.util.Collections; 046import java.util.HashMap; 047import java.util.HashSet; 048import java.util.LinkedList; 049import java.util.List; 050import java.util.Map; 051import java.util.Set; 052import java.util.UUID; 053import java.util.concurrent.CompletableFuture; 054import java.util.concurrent.TimeUnit; 055import java.util.function.BiConsumer; 056 057/** 058 * Manages player instances 059 */ 060public abstract class PlayerManager<P extends PlotPlayer<? extends T>, T> { 061 062 private static final MiniMessage MINI_MESSAGE = MiniMessage.builder().build(); 063 064 private final Map<UUID, P> playerMap = new HashMap<>(); 065 private final Object playerLock = new Object(); 066 067 public static void getUUIDsFromString( 068 final @NonNull String list, 069 final @NonNull BiConsumer<Collection<UUID>, Throwable> consumer 070 ) { 071 String[] split = list.split(","); 072 073 final Set<UUID> result = new HashSet<>(); 074 final List<String> request = new LinkedList<>(); 075 076 for (final String name : split) { 077 if (name.isEmpty()) { 078 consumer.accept(Collections.emptySet(), null); 079 return; 080 } else if ("*".equals(name)) { 081 result.add(DBFunc.EVERYONE); 082 } else if (name.length() > 16) { 083 try { 084 result.add(UUID.fromString(name)); 085 } catch (IllegalArgumentException ignored) { 086 consumer.accept(Collections.emptySet(), null); 087 return; 088 } 089 } else { 090 request.add(name); 091 } 092 } 093 094 if (request.isEmpty()) { 095 consumer.accept(result, null); 096 } else { 097 PlotSquared.get().getImpromptuUUIDPipeline() 098 .getUUIDs(request, Settings.UUID.NON_BLOCKING_TIMEOUT) 099 .whenComplete((uuids, throwable) -> { 100 if (throwable != null) { 101 consumer.accept(null, throwable); 102 } else { 103 for (final UUIDMapping uuid : uuids) { 104 result.add(uuid.uuid()); 105 } 106 consumer.accept(result, null); 107 } 108 }); 109 } 110 } 111 112 /** 113 * Get a list of names given a list of UUIDs. 114 * - Uses the format {@link TranslatableCaption#of(String)} of "info.plot_user_list" for the returned string 115 * 116 * @param uuids UUIDs 117 * @param localeHolder the localeHolder to localize the component for 118 * @return Component of name list 119 */ 120 public static @NonNull Component getPlayerList(final @NonNull Collection<UUID> uuids, LocaleHolder localeHolder) { 121 if (uuids.isEmpty()) { 122 return TranslatableCaption.of("info.none").toComponent(localeHolder).asComponent(); 123 } 124 125 final List<UUID> players = new LinkedList<>(); 126 final List<ComponentLike> users = new LinkedList<>(); 127 for (final UUID uuid : uuids) { 128 if (uuid == null) { 129 users.add(TranslatableCaption.of("info.none").toComponent(localeHolder)); 130 } else if (DBFunc.EVERYONE.equals(uuid)) { 131 users.add(TranslatableCaption.of("info.everyone").toComponent(localeHolder)); 132 } else if (DBFunc.SERVER.equals(uuid)) { 133 users.add(TranslatableCaption.of("info.console").toComponent(localeHolder)); 134 } else { 135 players.add(uuid); 136 } 137 } 138 139 try { 140 for (final UUIDMapping mapping : PlotSquared.get().getImpromptuUUIDPipeline() 141 .getNames(players).get(Settings.UUID.BLOCKING_TIMEOUT, TimeUnit.MILLISECONDS)) { 142 users.add(Component.text(mapping.username())); 143 } 144 } catch (final Exception e) { 145 e.printStackTrace(); 146 } 147 148 String c = TranslatableCaption.of("info.plot_user_list").getComponent(ConsolePlayer.getConsole()); 149 TextComponent.Builder list = Component.text(); 150 for (int x = 0; x < users.size(); x++) { 151 if (x + 1 == uuids.size()) { 152 list.append(MINI_MESSAGE.deserialize(c, TagResolver.resolver( 153 "user", 154 Tag.inserting(users.get(x)) 155 ))); 156 } else { 157 list.append(MINI_MESSAGE.deserialize(c + ", ", TagResolver.resolver( 158 "user", 159 Tag.inserting(users.get(x)) 160 ))); 161 } 162 } 163 return list.asComponent(); 164 } 165 166 /** 167 * Attempts to resolve the username by an uuid 168 * <p> 169 * <b>Note:</b> blocks the thread until the name was resolved or failed 170 * 171 * @param owner The UUID of the owner 172 * @return A caption containing either the name, {@code None}, {@code Everyone} or {@code Unknown} 173 * @see #resolveName(UUID, boolean) 174 * @since 6.4.0 175 * @deprecated Don't unnecessarily block threads and utilize playerMap - see {@link #getUsernameCaption(UUID)} 176 */ 177 @Deprecated(since = "7.1.0") 178 public static @NonNull Caption resolveName(final @Nullable UUID owner) { 179 return resolveName(owner, true); 180 } 181 182 /** 183 * Attempts to resolve the username by an uuid 184 * 185 * @param owner The UUID of the owner 186 * @param blocking If the operation should block the current thread for {@link Settings.UUID#BLOCKING_TIMEOUT} milliseconds 187 * @return A caption containing either the name, {@code None}, {@code Everyone} or {@code Unknown} 188 * @since 6.4.0 189 * @deprecated Don't unnecessarily block threads and utilize playerMap - see {@link #getUsernameCaption(UUID)} 190 */ 191 @Deprecated(since = "7.1.0") 192 public static @NonNull Caption resolveName(final @Nullable UUID owner, final boolean blocking) { 193 if (owner == null) { 194 return TranslatableCaption.of("info.none"); 195 } 196 if (owner.equals(DBFunc.EVERYONE)) { 197 return TranslatableCaption.of("info.everyone"); 198 } 199 if (owner.equals(DBFunc.SERVER)) { 200 return TranslatableCaption.of("info.server"); 201 } 202 final String name; 203 if (blocking) { 204 name = PlotSquared.get().getImpromptuUUIDPipeline() 205 .getSingle(owner, Settings.UUID.BLOCKING_TIMEOUT); 206 } else { 207 final UUIDMapping uuidMapping = 208 PlotSquared.get().getImpromptuUUIDPipeline().getImmediately(owner); 209 if (uuidMapping != null) { 210 name = uuidMapping.username(); 211 } else { 212 name = null; 213 } 214 } 215 if (name == null) { 216 return TranslatableCaption.of("info.unknown"); 217 } 218 return StaticCaption.of(name); 219 } 220 221 /** 222 * Resolves a UUID to a formatted {@link Caption} representing the player behind the UUID. 223 * Returns a {@link CompletableFuture} instead of a plain {@link UUID} as this method may query the 224 * {@link com.plotsquared.core.uuid.UUIDPipeline ImpromptuUUIDPipeline}. 225 * <br> 226 * Special Cases: 227 * <ul> 228 * <li>{@code null}: Resolves to a {@link TranslatableCaption} with the key {@code info.none}</li> 229 * <li>{@link DBFunc#EVERYONE}: Resolves to a {@link TranslatableCaption} with the key {@code info.everyone}</li> 230 * <li>{@link DBFunc#SERVER}: Resolves to a {@link TranslatableCaption} with the key {@code info.server}</li> 231 * </ul> 232 * <br> 233 * Otherwise, if the UUID is a valid UUID and not reserved by PlotSquared itself, this method first attempts to query the 234 * online players ({@link #getPlayerIfExists(UUID)}) for the specific UUID. 235 * If no online player was found for that UUID, the {@link com.plotsquared.core.uuid.UUIDPipeline ImpromptuUUIDPipeline} is 236 * queried to retrieve the known username 237 * 238 * @param uuid The UUID of the player (for example provided by {@link Plot#getOwner()} 239 * @return A CompletableFuture resolving to a Caption representing the players name of the uuid 240 * @since 7.1.0 241 */ 242 @Contract("_->!null") 243 public @NonNull CompletableFuture<Caption> getUsernameCaption(@Nullable UUID uuid) { 244 if (uuid == null) { 245 return CompletableFuture.completedFuture(TranslatableCaption.of("info.none")); 246 } 247 if (uuid.equals(DBFunc.EVERYONE)) { 248 return CompletableFuture.completedFuture(TranslatableCaption.of("info.everyone")); 249 } 250 if (uuid.equals(DBFunc.SERVER)) { 251 return CompletableFuture.completedFuture(TranslatableCaption.of("info.server")); 252 } 253 P player = getPlayerIfExists(uuid); 254 if (player != null) { 255 return CompletableFuture.completedFuture(StaticCaption.of(player.getName())); 256 } 257 return PlotSquared.get().getImpromptuUUIDPipeline().getNames(Collections.singleton(uuid)).thenApply(mapping -> { 258 if (mapping.isEmpty()) { 259 return TranslatableCaption.of("info.unknown"); 260 } 261 return StaticCaption.of(mapping.get(0).username()); 262 }); 263 } 264 265 /** 266 * Remove a player from the player map 267 * 268 * @param plotPlayer Player to remove 269 */ 270 public void removePlayer(final @NonNull PlotPlayer<?> plotPlayer) { 271 synchronized (playerLock) { 272 this.playerMap.remove(plotPlayer.getUUID()); 273 } 274 } 275 276 /** 277 * Remove a player from the player map 278 * 279 * @param uuid Player to remove 280 */ 281 public void removePlayer(final @NonNull UUID uuid) { 282 synchronized (playerLock) { 283 this.playerMap.remove(uuid); 284 } 285 } 286 287 /** 288 * Get the player from its UUID if it is stored in the player map. 289 * 290 * @param uuid Player UUID 291 * @return Player, or null 292 */ 293 public @Nullable P getPlayerIfExists(final @Nullable UUID uuid) { 294 if (uuid == null) { 295 return null; 296 } 297 return this.playerMap.get(uuid); 298 } 299 300 public @Nullable P getPlayerIfExists(final @Nullable String name) { 301 for (final P plotPlayer : this.playerMap.values()) { 302 if (plotPlayer.getName().equalsIgnoreCase(name)) { 303 return plotPlayer; 304 } 305 } 306 return null; 307 } 308 309 /** 310 * Get a plot player from a platform player object. This method requires 311 * that the caller actually knows that the player exists and is online. 312 * <p> 313 * The method will throw an exception if there is no such 314 * player online. 315 * 316 * @param object Platform player object 317 * @return Player object 318 */ 319 public @NonNull 320 abstract P getPlayer(final @NonNull T object); 321 322 /** 323 * Get a plot player from a UUID. This method requires 324 * that the caller actually knows that the player exists. 325 * <p> 326 * The method will throw an exception if there is no such 327 * player online. 328 * 329 * @param uuid Player UUID 330 * @return Player object 331 */ 332 public @NonNull P getPlayer(final @NonNull UUID uuid) { 333 synchronized (playerLock) { 334 P player = this.playerMap.get(uuid); 335 if (player == null) { 336 player = createPlayer(uuid); 337 this.playerMap.put(uuid, player); 338 } 339 return player; 340 } 341 } 342 343 public @NonNull 344 abstract P createPlayer(final @NonNull UUID uuid); 345 346 /** 347 * Get an an offline player object from the player's UUID 348 * 349 * @param uuid Player UUID 350 * @return Offline player object 351 */ 352 public @Nullable 353 abstract OfflinePlotPlayer getOfflinePlayer(final @Nullable UUID uuid); 354 355 /** 356 * Get an offline player object from the player's username 357 * 358 * @param username Player name 359 * @return Offline player object 360 */ 361 public @Nullable 362 abstract OfflinePlotPlayer getOfflinePlayer(final @NonNull String username); 363 364 /** 365 * Get all online players 366 * 367 * @return Unmodifiable collection of players 368 */ 369 public Collection<P> getPlayers() { 370 return Collections.unmodifiableCollection(new ArrayList<>(this.playerMap.values())); 371 } 372 373 374 public static final class NoSuchPlayerException extends IllegalArgumentException { 375 376 public NoSuchPlayerException(final @NonNull UUID uuid) { 377 super(String.format("There is no online player with UUID '%s'", uuid)); 378 } 379 380 } 381 382}