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