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.command;
020
021import com.google.inject.Inject;
022import com.plotsquared.core.PlotSquared;
023import com.plotsquared.core.configuration.Settings;
024import com.plotsquared.core.configuration.caption.TranslatableCaption;
025import com.plotsquared.core.events.TeleportCause;
026import com.plotsquared.core.permissions.Permission;
027import com.plotsquared.core.player.PlotPlayer;
028import com.plotsquared.core.plot.Plot;
029import com.plotsquared.core.plot.PlotArea;
030import com.plotsquared.core.plot.flag.implementations.UntrustedVisitFlag;
031import com.plotsquared.core.plot.world.PlotAreaManager;
032import com.plotsquared.core.util.MathMan;
033import com.plotsquared.core.util.PlayerManager;
034import com.plotsquared.core.util.TabCompletions;
035import com.plotsquared.core.util.query.PlotQuery;
036import com.plotsquared.core.util.query.SortingStrategy;
037import com.plotsquared.core.util.task.RunnableVal2;
038import com.plotsquared.core.util.task.RunnableVal3;
039import net.kyori.adventure.text.Component;
040import net.kyori.adventure.text.minimessage.tag.Tag;
041import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
042import org.checkerframework.checker.nullness.qual.NonNull;
043
044import java.util.ArrayList;
045import java.util.Collection;
046import java.util.Collections;
047import java.util.List;
048import java.util.UUID;
049import java.util.concurrent.CompletableFuture;
050import java.util.concurrent.TimeoutException;
051
052@CommandDeclaration(command = "visit",
053        permission = "plots.visit",
054        usage = "/plot visit <player> | <alias> | <plot> [area]|[#] [#]",
055        aliases = {"v", "tp", "teleport", "goto", "warp"},
056        requiredType = RequiredType.PLAYER,
057        category = CommandCategory.TELEPORT)
058public class Visit extends Command {
059
060    private final PlotAreaManager plotAreaManager;
061
062    @Inject
063    public Visit(final @NonNull PlotAreaManager plotAreaManager) {
064        super(MainCommand.getInstance(), true);
065        this.plotAreaManager = plotAreaManager;
066    }
067
068    private void visit(
069            final @NonNull PlotPlayer<?> player, final @NonNull PlotQuery query, final PlotArea sortByArea,
070            final RunnableVal3<Command, Runnable, Runnable> confirm, final RunnableVal2<Command, CommandResult> whenDone, int page
071    ) {
072        // We get the query once,
073        // then we get it another time further on
074        final List<Plot> unsorted = query.asList();
075
076        if (unsorted.size() > 1) {
077            query.whereBasePlot();
078        }
079
080        if (page == Integer.MIN_VALUE) {
081            page = 1;
082        }
083
084        PlotArea relativeArea = sortByArea;
085        if (Settings.Teleport.PER_WORLD_VISIT && sortByArea == null) {
086            relativeArea = player.getApplicablePlotArea();
087        }
088
089        if (relativeArea != null) {
090            query.relativeToArea(relativeArea).withSortingStrategy(SortingStrategy.SORT_BY_CREATION);
091        } else {
092            query.withSortingStrategy(SortingStrategy.SORT_BY_TEMP);
093        }
094
095        final List<Plot> plots = query.asList();
096
097        if (plots.isEmpty()) {
098            player.sendMessage(TranslatableCaption.of("invalid.found_no_plots"));
099            return;
100        } else if (plots.size() < page || page < 1) {
101            player.sendMessage(
102                    TranslatableCaption.of("invalid.number_not_in_range"),
103                    TagResolver.builder()
104                            .tag("min", Tag.inserting(Component.text(1)))
105                            .tag("max", Tag.inserting(Component.text(plots.size())))
106                            .build()
107            );
108            return;
109        }
110
111        final Plot plot = plots.get(page - 1);
112        if (!plot.hasOwner()) {
113            if (!player.hasPermission(Permission.PERMISSION_VISIT_UNOWNED)) {
114                player.sendMessage(
115                        TranslatableCaption.of("permission.no_permission"),
116                        TagResolver.resolver("node", Tag.inserting(Component.text("plots.visit.unowned")))
117                );
118                return;
119            }
120        } else if (plot.isOwner(player.getUUID())) {
121            if (!player.hasPermission(Permission.PERMISSION_VISIT_OWNED) && !player.hasPermission(Permission.PERMISSION_HOME)) {
122                player.sendMessage(
123                        TranslatableCaption.of("permission.no_permission"),
124                        TagResolver.resolver("node", Tag.inserting(Component.text("plots.visit.owned")))
125                );
126                return;
127            }
128        } else if (plot.isAdded(player.getUUID())) {
129            if (!player.hasPermission(Permission.PERMISSION_SHARED)) {
130                player.sendMessage(
131                        TranslatableCaption.of("permission.no_permission"),
132                        TagResolver.resolver("node", Tag.inserting(Component.text("plots.visit.shared")))
133                );
134                return;
135            }
136        } else {
137            // allow visit, if UntrustedVisit flag is set, or if the player has either the plot.visit.other or
138            // plot.admin.visit.untrusted permission
139            if (!plot.getFlag(UntrustedVisitFlag.class) && !player.hasPermission(Permission.PERMISSION_VISIT_OTHER)
140                    && !player.hasPermission(Permission.PERMISSION_ADMIN_VISIT_UNTRUSTED)) {
141                player.sendMessage(
142                        TranslatableCaption.of("permission.no_permission"),
143                        TagResolver.resolver("node", Tag.inserting(Component.text("plots.visit.other")))
144                );
145                return;
146            }
147            if (plot.isDenied(player.getUUID())) {
148                if (!player.hasPermission(Permission.PERMISSION_VISIT_DENIED)) {
149                    player.sendMessage(
150                            TranslatableCaption.of("permission.no_permission"),
151                            TagResolver.resolver(
152                                    "node",
153                                    Tag.inserting(Permission.PERMISSION_VISIT_DENIED)
154                            )
155                    );
156                    return;
157                }
158            }
159        }
160
161        confirm.run(this, () -> plot.teleportPlayer(player, TeleportCause.COMMAND_VISIT, result -> {
162            if (result) {
163                whenDone.run(Visit.this, CommandResult.SUCCESS);
164            } else {
165                whenDone.run(Visit.this, CommandResult.FAILURE);
166            }
167        }), () -> whenDone.run(Visit.this, CommandResult.FAILURE));
168    }
169
170    @Override
171    public CompletableFuture<Boolean> execute(
172            final PlotPlayer<?> player,
173            String[] args,
174            final RunnableVal3<Command, Runnable, Runnable> confirm,
175            final RunnableVal2<Command, CommandResult> whenDone
176    ) throws CommandException {
177        if (args.length > 3) {
178            sendUsage(player);
179            return CompletableFuture.completedFuture(false);
180        }
181
182        if (args.length == 1 && args[0].contains(":")) {
183            args = args[0].split(":");
184        }
185
186        PlotArea sortByArea;
187
188        int page = Integer.MIN_VALUE;
189
190        switch (args.length) {
191            // /p v <user> <area> <page>
192            case 3:
193                if (!MathMan.isInteger(args[2])) {
194                    player.sendMessage(
195                            TranslatableCaption.of("invalid.not_valid_number"),
196                            TagResolver.resolver("value", Tag.inserting(Component.text("(1, ∞)")))
197                    );
198                    player.sendMessage(
199                            TranslatableCaption.of("commandconfig.command_syntax"),
200                            TagResolver.resolver("value", Tag.inserting(Component.text(getUsage())))
201                    );
202                    return CompletableFuture.completedFuture(false);
203                }
204                page = Integer.parseInt(args[2]);
205                // /p v <name> <area> [page]
206                // /p v <name> [page]
207            case 2:
208                if (page != Integer.MIN_VALUE || !MathMan.isInteger(args[1])) {
209                    sortByArea = this.plotAreaManager.getPlotAreaByString(args[1]);
210                    if (sortByArea == null) {
211                        player.sendMessage(
212                                TranslatableCaption.of("invalid.not_valid_number"),
213                                TagResolver.resolver("value", Tag.inserting(Component.text("(1, ∞)")))
214                        );
215                        player.sendMessage(
216                                TranslatableCaption.of("commandconfig.command_syntax"),
217                                TagResolver.resolver("value", Tag.inserting(Component.text(getUsage())))
218                        );
219                        return CompletableFuture.completedFuture(false);
220                    }
221
222                    final PlotArea finalSortByArea = sortByArea;
223                    int finalPage1 = page;
224                    PlayerManager.getUUIDsFromString(args[0], (uuids, throwable) -> {
225                        if (throwable instanceof TimeoutException) {
226                            player.sendMessage(TranslatableCaption.of("players.fetching_players_timeout"));
227                        } else if (throwable != null || uuids.size() != 1) {
228                            player.sendMessage(
229                                    TranslatableCaption.of("commandconfig.command_syntax"),
230                                    TagResolver.resolver("value", Tag.inserting(Component.text(getUsage())))
231                            );
232                        } else {
233                            final UUID uuid = uuids.toArray(new UUID[0])[0];
234                            PlotQuery query = PlotQuery.newQuery();
235                            if (Settings.Teleport.VISIT_MERGED_OWNERS) {
236                                query.whereBasePlot().ownersInclude(uuid);
237                            } else {
238                                query.whereBasePlot().ownedBy(uuid);
239                            }
240                            this.visit(
241                                    player,
242                                    query,
243                                    finalSortByArea,
244                                    confirm,
245                                    whenDone,
246                                    finalPage1
247                            );
248                        }
249                    });
250                    break;
251                }
252                try {
253                    page = Integer.parseInt(args[1]);
254                } catch (NumberFormatException ignored) {
255                    player.sendMessage(
256                            TranslatableCaption.of("invalid.not_a_number"),
257                            TagResolver.resolver("value", Tag.inserting(Component.text(args[1])))
258                    );
259                    return CompletableFuture.completedFuture(false);
260                }
261                // /p v <name> [page]
262                // /p v <uuid> [page]
263                // /p v <plot> [page]
264                // /p v <alias>
265            case 1:
266                final String[] finalArgs = args;
267                int finalPage = page;
268                if (args[0].length() >= 2 && !args[0].contains(";") && !args[0].contains(",")) {
269                    PlotSquared.get().getImpromptuUUIDPipeline().getSingle(args[0], (uuid, throwable) -> {
270                        if (throwable instanceof TimeoutException) {
271                            // The request timed out
272                            player.sendMessage(TranslatableCaption.of("players.fetching_players_timeout"));
273                        } else if (uuid != null && (Settings.Teleport.VISIT_MERGED_OWNERS
274                                ? !PlotQuery.newQuery().ownersInclude(uuid).anyMatch()
275                                : !PlotQuery.newQuery().ownedBy(uuid).anyMatch())) {
276                            // It was a valid UUID but the player has no plots
277                            player.sendMessage(TranslatableCaption.of("errors.player_no_plots"));
278                        } else if (uuid == null) {
279                            // player not found, so we assume it's an alias if no page was provided
280                            if (finalPage == Integer.MIN_VALUE) {
281                                this.visit(
282                                        player,
283                                        PlotQuery.newQuery().withAlias(finalArgs[0]),
284                                        player.getApplicablePlotArea(),
285                                        confirm,
286                                        whenDone,
287                                        1
288                                );
289                            } else {
290                                player.sendMessage(
291                                        TranslatableCaption.of("errors.invalid_player"),
292                                        TagResolver.resolver("value", Tag.inserting(Component.text(finalArgs[0])))
293                                );
294                            }
295                        } else {
296                            this.visit(
297                                    player,
298                                    Settings.Teleport.VISIT_MERGED_OWNERS
299                                            ? PlotQuery.newQuery().ownersInclude(uuid).whereBasePlot()
300                                            : PlotQuery.newQuery().ownedBy(uuid).whereBasePlot(),
301                                    null,
302                                    confirm,
303                                    whenDone,
304                                    finalPage
305                            );
306                        }
307                    });
308                } else {
309                    // Try to parse a plot
310                    final Plot plot = Plot.getPlotFromString(player, finalArgs[0], true);
311                    if (plot != null) {
312                        this.visit(player, PlotQuery.newQuery().withPlot(plot), null, confirm, whenDone, 1);
313                    }
314                }
315                break;
316            case 0:
317                // /p v is invalid
318                player.sendMessage(
319                        TranslatableCaption.of("commandconfig.command_syntax"),
320                        TagResolver.resolver("value", Tag.inserting(Component.text(getUsage())))
321                );
322                return CompletableFuture.completedFuture(false);
323            default:
324        }
325
326        return CompletableFuture.completedFuture(true);
327    }
328
329    @Override
330    public Collection<Command> tab(PlotPlayer<?> player, String[] args, boolean space) {
331        final List<Command> completions = new ArrayList<>();
332        switch (args.length - 1) {
333            case 0 -> completions.addAll(TabCompletions.completePlayers(player, args[0], Collections.emptyList()));
334            case 1 -> {
335                completions.addAll(
336                        TabCompletions.completeAreas(args[1]));
337                if (args[1].isEmpty()) {
338                    // if no input is given, only suggest 1 - 3
339                    completions.addAll(
340                            TabCompletions.asCompletions("1", "2", "3"));
341                    break;
342                }
343                completions.addAll(
344                        TabCompletions.completeNumbers(args[1], 10, 999));
345            }
346            case 2 -> {
347                if (args[2].isEmpty()) {
348                    // if no input is given, only suggest 1 - 3
349                    completions.addAll(
350                            TabCompletions.asCompletions("1", "2", "3"));
351                    break;
352                }
353                completions.addAll(
354                        TabCompletions.completeNumbers(args[2], 10, 999));
355            }
356        }
357
358        return completions;
359    }
360
361}