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}