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